// SPDX-License-Identifier: MIT
// Copyright (c) 2026 AgentEval Contributors
#pragma warning disable MEAI001 // Hosted tools are inspected as coverage subjects; none is executed.
using AgentEval.MAF.Gatekeeper;
using Microsoft.Extensions.AI;
namespace AgentEval.Samples;
///
/// Gatekeeper — honest provider-hosted-tool coverage, fully offline.
///
/// A local function passes through MAF function-invocation middleware. A provider-hosted code interpreter does
/// not. Registering more local gates cannot change that execution model, and acknowledging the hosted tool is an
/// explicit risk acceptance rather than a claim that Gatekeeper intercepts it.
///
public static class GatekeeperHostedToolCoverageBoundary
{
public static Task RunAsync()
{
GatekeeperSampleContractRenderer.Print("18");
Console.WriteLine("\n=== Gatekeeper — Hosted Tool Coverage Boundary (offline) ===\n");
var localInvocations = 0;
var localTool = AIFunctionFactory.Create(
(string orderId) =>
{
localInvocations++;
return $"fake order {orderId}";
},
"lookup_order");
var hostedTool = new HostedCodeInterpreterTool();
IToolGate[] gates = [new ForbiddenToolGate("delete_account")];
AITool[] tools = [localTool, hostedTool];
var report = GatekeeperCoverageAnalyzer.Analyze(tools, gates);
var local = report.Tools.Single(entry => entry.ToolName == localTool.Name);
var hosted = report.Tools.Single(entry => entry.ToolName == hostedTool.Name);
Require(local.ExecutionModel == ToolExecutionModel.InterceptedLocalFunction, "the local function must be interceptable");
Require(local.IsGateProtected, "the registered local gate must be structurally reachable for the local function");
Require(hosted.ExecutionModel == ToolExecutionModel.ProviderHostedOpaque, "the hosted interpreter must be classified opaque");
Require(!hosted.IsGateProtected, "a local tool gate must never claim to protect provider-hosted execution");
Require(hosted.IsUnprotectedHighRisk, "an unacknowledged hosted code interpreter must fail high-risk coverage");
Require(report.EnforcementCoveragePercent == 50.0, "one of two tools should be structurally protected");
UnprotectedHighRiskToolException? refused = null;
try
{
GatekeeperCoverageAnalyzer.AnalyzeOrThrow(tools, gates);
}
catch (UnprotectedHighRiskToolException ex)
{
refused = ex;
}
Require(refused is not null, "promotion must refuse the unacknowledged high-risk hosted tool");
var refusedReport = refused?.Report
?? throw new InvalidOperationException("Hosted-tool-coverage sample invariant failed: promotion must refuse the unacknowledged high-risk hosted tool.");
Require(refusedReport.ProviderHostedOpaqueCount == 1, "the refusal must carry the honest coverage report");
Console.WriteLine("① Unacknowledged hosted code interpreter — promotion refused");
Console.WriteLine(Indent(report.Render(), " "));
var acknowledgedOptions = new AnalyzeOptions
{
AcknowledgeProviderHostedTools = new HashSet(StringComparer.Ordinal) { hostedTool.Name },
};
var acknowledged = GatekeeperCoverageAnalyzer.AnalyzeOrThrow(tools, gates, acknowledgedOptions);
var acknowledgedHosted = acknowledged.Tools.Single(entry => entry.ToolName == hostedTool.Name);
Require(!acknowledged.HasUnprotectedHighRiskTools, "explicit acknowledgment should admit promotion");
Require(!acknowledgedHosted.IsGateProtected, "acknowledgment must not fabricate local interception");
Require(acknowledged.EnforcementCoveragePercent == 50.0, "acknowledgment must not inflate structural coverage");
Console.WriteLine("② Explicit acknowledgment — admitted, but still visibly opaque");
Console.WriteLine(Indent(acknowledged.Render(), " "));
Require(localInvocations == 0, "coverage analysis must never execute the tools it classifies");
Console.WriteLine(" ✅ acknowledgment records risk acceptance; it does not turn a provider-hosted tool into a gated local function");
Console.WriteLine($" ✅ coverage analysis executed nothing: measured local-tool invocations = {localInvocations}, and no model client was ever constructed");
Console.WriteLine("\n=== Hosted Tool Coverage Boundary Complete ===");
return Task.CompletedTask;
}
private static string Indent(string value, string prefix)
=> prefix + value.Replace(Environment.NewLine, Environment.NewLine + prefix, StringComparison.Ordinal);
private static void Require(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException("Hosted-tool-coverage sample invariant failed: " + message + ".");
}
}
}