// SPDX-License-Identifier: MIT
// Copyright (c) 2026 AgentEval Contributors
using AgentEval.MAF.Gatekeeper;
using AgentEval.Testing;
using AgentEval.Tracing;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using AgentTrace = AgentEval.Tracing.AgentTrace;
using RuntimeEnforcement = AgentEval.MAF.Gatekeeper.GatekeeperEnforcement;
namespace AgentEval.Samples;
///
/// Gatekeeper — secret and oversized tool-result admission, fully offline and deterministic.
///
/// The tool executes before result gates run. The sample therefore asserts the exact promise this boundary can
/// make: a fake credential is masked and excess content is truncated before the result enters model context.
/// A small clean result remains byte-for-byte useful.
///
public static class GatekeeperToolResultAdmission
{
private const int ResultLimit = 180;
public static async Task RunAsync()
{
GatekeeperSampleContractRenderer.Print("17");
Console.WriteLine("\n=== Gatekeeper — Tool Result Admission (offline) ===\n");
await RedactSecretAndTruncateOversizedResultAsync();
await PreserveCleanResultAsync();
Console.WriteLine("\n=== Tool Result Admission Complete ===");
}
private static async Task RedactSecretAndTruncateOversizedResultAsync()
{
Console.WriteLine("① Fake credential + oversized diagnostics — sanitize before model context");
var fakeToken = "ghp_" + new string('A', 36);
var rawResult = $"diagnostic-id=demo-42; token={fakeToken}; useful-status=degraded; " + new string('X', 500);
var executions = 0;
var tool = AIFunctionFactory.Create(
() =>
{
Interlocked.Increment(ref executions);
return rawResult;
},
"download_diagnostics");
var client = new ScriptedChatClient()
.AddToolCall("result-1", "download_diagnostics", new Dictionary())
.AddText("Diagnostics were sanitized and summarized.");
var trace = new AgentTrace();
var agent = new ChatClientAgent(
client,
new ChatClientAgentOptions
{
Name = "ResultAdmissionAttack",
ChatOptions = new ChatOptions { Tools = [tool], MaxOutputTokens = 256 },
})
.AsBuilder()
.UseGatekeeper(RuntimeEnforcement.ReplaceResult, options =>
{
options.Trace = trace;
// Mask first, then truncate the already-sanitized projection.
options.AddResultGate(new ToolResultSecretGate());
options.AddResultGate(new ToolResultSizeGate(ResultLimit));
})
.Build();
await agent.RunAsync("Download and summarize the fake diagnostics.");
var admitted = SingleFunctionResult(client);
Require(executions == 1, "the fake diagnostics tool should execute exactly once");
Require(!admitted.Contains(fakeToken, StringComparison.Ordinal), "the fake token must not enter model context");
Require(!admitted.Contains("ghp_", StringComparison.Ordinal), "the credential prefix must be masked");
Require(admitted.Contains('█'), "the secret gate must leave an explicit masked span");
Require(admitted.Contains("[truncated", StringComparison.Ordinal), "the size gate must add a truncation marker");
Require(admitted.Length < rawResult.Length, "the admitted projection must be smaller than the raw result");
Require(HasAction(trace, "tool-result-secret-detection", "Redact"), "secret redaction evidence must be recorded");
Require(HasAction(trace, "tool-result-size-limit", "Redact"), "size redaction evidence must be recorded");
Console.WriteLine(" ✅ fake tool executed once; result gates did not pretend to undo that effect");
Console.WriteLine(" ✅ fake credential masked before the next model turn");
Console.WriteLine(" ✅ oversized remainder truncated while the useful prefix stayed available");
GateVoice.Speak(trace, indent: " ");
}
private static async Task PreserveCleanResultAsync()
{
Console.WriteLine("\n② Clean bounded diagnostics — preserve utility");
const string cleanResult = "diagnostic-id=demo-43; useful-status=healthy";
var executions = 0;
var tool = AIFunctionFactory.Create(
() =>
{
Interlocked.Increment(ref executions);
return cleanResult;
},
"download_clean_diagnostics");
var client = new ScriptedChatClient()
.AddToolCall("result-2", "download_clean_diagnostics", new Dictionary())
.AddText("The clean diagnostics report is healthy.");
var trace = new AgentTrace();
var agent = new ChatClientAgent(
client,
new ChatClientAgentOptions
{
Name = "ResultAdmissionControl",
ChatOptions = new ChatOptions { Tools = [tool], MaxOutputTokens = 256 },
})
.AsBuilder()
.UseGatekeeper(RuntimeEnforcement.ReplaceResult, options =>
{
options.Trace = trace;
options.AddResultGate(new ToolResultSecretGate());
options.AddResultGate(new ToolResultSizeGate(ResultLimit));
})
.Build();
await agent.RunAsync("Read the clean fake diagnostics.");
var admitted = SingleFunctionResult(client);
Require(executions == 1, "the clean diagnostics tool should execute exactly once");
Require(string.Equals(admitted, cleanResult, StringComparison.Ordinal), "a clean bounded result must remain unchanged");
Require(!HasAction(trace, "tool-result-secret-detection", "Redact"), "the clean result must not be secret-redacted");
Require(!HasAction(trace, "tool-result-size-limit", "Redact"), "the clean result must not be truncated");
Console.WriteLine(" ✅ clean bounded result reached model context unchanged");
}
private static string SingleFunctionResult(ScriptedChatClient client)
{
var results = client.ReceivedMessages
.SelectMany(messages => messages)
.SelectMany(message => message.Contents.OfType())
.Select(result => result.Result?.ToString() ?? string.Empty)
.Distinct(StringComparer.Ordinal)
.ToArray();
Require(results.Length == 1, "exactly one distinct function result should reach the scripted model");
return results[0];
}
private static bool HasAction(AgentTrace trace, string policy, string action)
=> trace.Metadata?.Any(entry =>
GateMetadataReader.IsGateKey(entry.Key)
&& string.Equals(GateMetadataReader.PolicyFromKey(entry.Key), policy, StringComparison.Ordinal)
&& string.Equals(GateMetadataReader.ReadField(entry.Value, "action"), action, StringComparison.Ordinal)) == true;
private static void Require(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException("Tool-result-admission sample invariant failed: " + message + ".");
}
}
}