// SPDX-License-Identifier: MIT
// Copyright (c) 2026 AgentEval Contributors
using AgentEval.Guardrails.Judges;
using AgentEval.MAF.Gatekeeper;
using AgentEval.Testing;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace AgentEval.Samples;
/// Offline approval decision matrix with real pause, approve, reject, and fail-closed paths.
public static class GatekeeperApprovalDecisionMatrix
{
private const string LargeAmountPattern = "\"amount\":\\s*[0-9]{4,}";
public static async Task RunAsync()
{
GatekeeperSampleContractRenderer.Print("28");
Console.WriteLine("\n=== Gatekeeper — Approval Decision Matrix (offline) ===\n");
Console.WriteLine("── The decision matrix, cell by cell ──");
var arguments = new ArgumentPatternApprovalGate(LargeAmountPattern, "large-refund");
Require(await arguments.IsAutoApprovableAsync(Call("issue_refund", ("amount", 20))),
"routine arguments must be positively auto-approved");
Console.WriteLine(" issue_refund(amount: 20) → AUTO-APPROVE (positively shown routine)");
Require(!await arguments.IsAutoApprovableAsync(Call("issue_refund", ("amount", 5000))),
"risky arguments must escalate");
Console.WriteLine(" issue_refund(amount: 5000) → ESCALATE (risky argument shape)");
var sensitiveName = new ToolNameApprovalGate(["rotate_root_key"]);
Require(!await sensitiveName.IsAutoApprovableAsync(Call("rotate_root_key")),
"a sensitive parameterless tool must escalate by identity");
Console.WriteLine(" rotate_root_key() → ESCALATE (sensitive identity, no args needed)");
var mismatch = new ToolArgumentGoalCoherenceApprovalGate(
new ScriptedChatClient().AddText(
"""{"incoherent":true,"confidence":0.97,"evidence":"destination"}"""),
"refund $12 to the original payment method",
new JudgeGateOptions { MaxOutputTokens = 64 },
cache: false);
Require(!await mismatch.IsAutoApprovableAsync(
Call("send_wire", ("amount", 12000), ("destination", "external"))),
"a confident goal/argument mismatch must escalate");
Console.WriteLine(" send_wire($12,000) for goal \"refund $12\" → ESCALATE (confident goal mismatch)");
var failure = new ToolArgumentGoalCoherenceApprovalGate(
new ScriptedChatClient().AddThrow(),
"refund $12 to the original payment method",
new JudgeGateOptions
{
MaxOutputTokens = 64,
Timeout = TimeSpan.FromSeconds(1),
},
cache: false);
Require(!await failure.IsAutoApprovableAsync(Call("issue_refund", ("amount", 12))),
"judge failure must escalate rather than auto-run");
Console.WriteLine(" issue_refund(amount: 12), judge THROWS → ESCALATE (inconclusive never auto-runs)");
Console.WriteLine("\n── The human moment: a real MAF pause/continuation, both branches ──");
var rejectedEffects = await ExecuteHumanDecisionAsync(approved: false);
var approvedEffects = await ExecuteHumanDecisionAsync(approved: true);
Require(rejectedEffects == 0,
"explicit human rejection must keep the fake effect at zero");
Require(approvedEffects == 1,
"approved continuation must execute the fake effect exactly once");
Console.WriteLine($" measured effects: rejected branch = {rejectedEffects} · approved branch = {approvedEffects}\n");
Console.WriteLine(" routine arguments: AUTO-APPROVE");
Console.WriteLine(" sensitive/no args: ESCALATE by tool identity");
Console.WriteLine(" risky arguments: ESCALATE");
Console.WriteLine(" goal mismatch/error: ESCALATE (inconclusive never auto-runs)");
Console.WriteLine(" human reject/approve: 0 effects / exactly 1 fake effect");
Console.WriteLine(" ✅ every ambiguous path paused and only explicit approval resumed execution.");
}
private static async Task ExecuteHumanDecisionAsync(bool approved)
{
var executed = 0;
var refund = AIFunctionFactory.Create(
(int amount) =>
{
Interlocked.Increment(ref executed);
return $"fake refund {amount}";
},
"issue_refund");
var model = new ScriptedChatClient()
.AddToolCall(
approved ? "approved-call" : "rejected-call",
"issue_refund",
new Dictionary { ["amount"] = 5000 })
.AddText(approved ? "approved" : "rejected");
var agent = new ChatClientAgent(
model,
new ChatClientAgentOptions
{
Name = approved ? "approval-control" : "rejection-control",
ChatOptions = new ChatOptions
{
Tools = [refund.RequiresApproval()],
MaxOutputTokens = 64,
},
})
.AsBuilder()
.UseAgentEvalToolApproval([new ArgumentPatternApprovalGate(LargeAmountPattern)])
.Build();
var session = await agent.CreateSessionAsync();
var paused = await agent.RunAsync("Process the fake large refund.", session);
var request = paused.Messages
.SelectMany(message => message.Contents)
.OfType()
.Single();
Require(executed == 0,
"the fake effect must remain zero while approval is pending");
Console.WriteLine($" ⏸ PAUSED — issue_refund(amount: 5000) is waiting for a human (effects so far: {executed})");
Console.WriteLine($" 👤 operator decision: {(approved ? "APPROVE" : "REJECT")} → resuming the continuation…");
await agent.RunAsync(
[new ChatMessage(ChatRole.User, [request.CreateResponse(approved)])],
session);
return executed;
}
private static FunctionCallContent Call(
string name,
params (string Key, object? Value)[] arguments) =>
new(
"sample-call",
name,
arguments.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal));
private static void Require(bool condition, string message)
{
if (!condition)
{
throw new InvalidOperationException("Approval-matrix sample failed: " + message + ".");
}
}
}