// SPDX-License-Identifier: MIT
// Copyright (c) 2026 AgentEval Contributors
using System.Text.RegularExpressions;
using AgentEval.MAF.Gatekeeper;
namespace AgentEval.Samples;
///
/// Offline Phase-7 design fixture for dangerous-tool contracts. These sample-local gates deliberately implement
/// narrow mock grammars; they exercise gate verdicts, fail-closed argument handling, and useful API shapes, not safety
/// for arbitrary SQL dialects, browsers, cloud CLIs, or package managers.
///
public static class GatekeeperMockedDangerousTools
{
public static async Task RunAsync()
{
GatekeeperSampleContractRenderer.Print("13");
Console.WriteLine("\n=== Gatekeeper — Mocked Dangerous-Tool Contracts ===\n");
Console.WriteLine(
" Scope: offline contract fixture only. No database, browser, cloud account, or package manager is contacted.\n");
var component = new MockDangerousToolHost();
var allowed = 0;
var blocked = 0;
await AttemptAsync(
new MockSqlReadOnlyGate(["customers", "orders"]),
component,
"sql_query",
[("statement", "SELECT id, name FROM customers WHERE id = 7")],
"SQL: allow one narrow read",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockSqlReadOnlyGate(["customers", "orders"]),
component,
"sql_query",
[("statement", "SELECT * FROM customers; DELETE FROM customers")],
"SQL: block stacked write",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockBrowserNavigationGate(["docs.example.test"]),
component,
"browser_navigate",
[("url", "https://docs.example.test/guides/gatekeeper")],
"Browser: allow exact HTTPS host",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockBrowserNavigationGate(["docs.example.test"]),
component,
"browser_navigate",
[("url", "https://docs.example.test.evil.invalid/collect")],
"Browser: block suffix-confusion host",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockCloudOperationGate([("storage", "list")]),
component,
"cloud_operation",
[("service", "storage"), ("operation", "list")],
"Cloud: allow structured operation",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockCloudOperationGate([("storage", "list")]),
component,
"cloud_operation",
[("service", "compute"), ("operation", "delete")],
"Cloud: block unapproved operation",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockPackageRestoreGate(["AgentEval", "Microsoft.Extensions.AI"]),
component,
"package_restore",
[("package", "AgentEval"), ("version", "1.0.0")],
"Package: allow approved restore",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockPackageRestoreGate(["AgentEval", "Microsoft.Extensions.AI"]),
component,
"package_restore",
[("package", "AgentEval"), ("version", "latest")],
"Package: block floating version",
result => Tally(result, ref allowed, ref blocked));
await AttemptAsync(
new MockPackageRestoreGate(["AgentEval", "Microsoft.Extensions.AI"]),
component,
"package_restore",
[("package", "unreviewed.postinstall.runner"), ("version", "latest")],
"Package: block unapproved package",
result => Tally(result, ref allowed, ref blocked));
Console.WriteLine(
$"\n Result: {allowed} allowed and executed by the mock host; {blocked} blocked before execution.");
Console.WriteLine($" Mock executions observed: {component.ExecutionCount}");
if (allowed != 4 || blocked != 5 || component.ExecutionCount != 4)
{
throw new InvalidOperationException("The mocked contract fixture produced an unexpected verdict count.");
}
Console.WriteLine(
"\n Design conclusion: structured cloud/package APIs are substantially safer to contract than raw CLI text. " +
"Production SQL and browser predicates still need dialect/runtime-specific corpora.");
Console.WriteLine("\n=== Gatekeeper — Mocked Dangerous-Tool Contracts Complete ===");
}
private static async Task AttemptAsync(
IToolGate gate,
MockDangerousToolHost component,
string functionName,
(string Name, object? Value)[] arguments,
string label,
Action tally)
{
var call = new GatedToolCall(
functionName,
arguments.ToDictionary(item => item.Name, item => item.Value),
"phase-7-mock-agent",
Iteration: 0,
FunctionCallIndex: 0,
FunctionCount: 1,
IsStreaming: false,
Messages: null);
var verdict = await gate.InspectAsync(call);
var allowed = verdict.Action == ToolGateAction.Allow;
if (allowed)
{
component.Execute(functionName);
}
tally(allowed);
Console.WriteLine(
$" {(allowed ? "✅ ALLOW" : "🛑 BLOCK")} {label} " +
$"({(allowed ? "mock body executed" : verdict.Reason)})");
}
private static void Tally(bool allowed, ref int allowedCount, ref int blockedCount)
{
if (allowed)
{
allowedCount++;
}
else
{
blockedCount++;
}
}
private static bool TryGetRequiredString(
GatedToolCall call,
string argument,
int maxLength,
out string value)
{
value = string.Empty;
if (call.Arguments is null ||
!call.Arguments.TryGetValue(argument, out var raw) ||
raw is not string text)
{
return false;
}
text = text.Trim();
if (text.Length is 0 || text.Length > maxLength || text.Any(char.IsControl))
{
return false;
}
value = text;
return true;
}
private sealed class MockDangerousToolHost
{
public int ExecutionCount { get; private set; }
public void Execute(string functionName)
{
_ = functionName;
ExecutionCount++;
}
}
private sealed class MockSqlReadOnlyGate(IEnumerable allowedTables) : IToolGate
{
private static readonly Regex TableReferencePattern = new(
@"\b(?:FROM|JOIN)\s+(?[A-Za-z_][A-Za-z0-9_.]*)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100));
private static readonly Regex ForbiddenTokenPattern = new(
@"\b(?:ALTER|CALL|COPY|CREATE|DELETE|DROP|EXEC|EXECUTE|GRANT|INSERT|INTO|MERGE|REVOKE|TRUNCATE|UNION|UPDATE|WITH)\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100));
private readonly HashSet _allowedTables =
new(allowedTables, StringComparer.OrdinalIgnoreCase);
public string PolicyName => "sample.mock-sql-read-only";
public GateCost Cost => GateCost.Bounded;
public ToolGatePolicy MinimumPolicy => ToolGatePolicy.ReplaceResult;
public ValueTask InspectAsync(
GatedToolCall call,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.Equals(call.FunctionName, "sql_query", StringComparison.OrdinalIgnoreCase))
{
return ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName));
}
if (!TryGetRequiredString(call, "statement", 4096, out var statement))
{
return Block("missing, malformed, or oversized SQL statement");
}
try
{
if (!statement.StartsWith("SELECT ", StringComparison.OrdinalIgnoreCase) ||
statement.Contains(';', StringComparison.Ordinal) ||
statement.Contains("--", StringComparison.Ordinal) ||
statement.Contains("/*", StringComparison.Ordinal) ||
statement.Contains("*/", StringComparison.Ordinal) ||
ForbiddenTokenPattern.IsMatch(statement))
{
return Block("statement is outside the sample's single-SELECT subset");
}
var tables = TableReferencePattern.Matches(statement)
.Select(match => match.Groups["table"].Value)
.ToArray();
if (tables.Length == 0 || tables.Any(table => !_allowedTables.Contains(table)))
{
return Block("statement references no table or a table outside the sample allow-list");
}
return ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName));
}
catch (RegexMatchTimeoutException)
{
return Block("SQL inspection exceeded its bounded time budget");
}
}
private ValueTask Block(string reason) =>
ValueTask.FromResult(ToolGateVerdict.Block(PolicyName, reason));
}
private sealed class MockBrowserNavigationGate(IEnumerable allowedHosts) : IToolGate
{
private readonly HashSet _allowedHosts =
new(allowedHosts.Select(host => host.Trim().ToLowerInvariant()), StringComparer.Ordinal);
public string PolicyName => "sample.mock-browser-navigation";
public GateCost Cost => GateCost.Bounded;
public ToolGatePolicy MinimumPolicy => ToolGatePolicy.ReplaceResult;
public ValueTask InspectAsync(
GatedToolCall call,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.Equals(call.FunctionName, "browser_navigate", StringComparison.OrdinalIgnoreCase))
{
return ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName));
}
if (!TryGetRequiredString(call, "url", 2048, out var text) ||
!Uri.TryCreate(text, UriKind.Absolute, out var uri) ||
uri.Scheme != Uri.UriSchemeHttps ||
!string.IsNullOrEmpty(uri.UserInfo) ||
!uri.IsDefaultPort)
{
return Block("URL must be an absolute default-port HTTPS URI without user information");
}
string host;
try
{
host = uri.IdnHost.ToLowerInvariant();
}
catch (UriFormatException)
{
return Block("URL host is malformed");
}
return _allowedHosts.Contains(host)
? ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName))
: Block("URL host is outside the sample allow-list");
}
private ValueTask Block(string reason) =>
ValueTask.FromResult(ToolGateVerdict.Block(PolicyName, reason));
}
private sealed class MockCloudOperationGate(IEnumerable<(string Service, string Operation)> allowed) : IToolGate
{
private readonly HashSet _allowed = new(
allowed.Select(item => Key(item.Service, item.Operation)),
StringComparer.OrdinalIgnoreCase);
public string PolicyName => "sample.mock-cloud-operation";
public GateCost Cost => GateCost.PureCode;
public ToolGatePolicy MinimumPolicy => ToolGatePolicy.ReplaceResult;
public ValueTask InspectAsync(
GatedToolCall call,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.Equals(call.FunctionName, "cloud_operation", StringComparison.OrdinalIgnoreCase))
{
return ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName));
}
if (!TryGetRequiredString(call, "service", 128, out var service) ||
!TryGetRequiredString(call, "operation", 128, out var operation))
{
return Block("service and operation must be bounded strings");
}
return _allowed.Contains(Key(service, operation))
? ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName))
: Block("structured cloud operation is outside the sample allow-list");
}
private static string Key(string service, string operation) => service + "\0" + operation;
private ValueTask Block(string reason) =>
ValueTask.FromResult(ToolGateVerdict.Block(PolicyName, reason));
}
private sealed class MockPackageRestoreGate(IEnumerable allowedPackages) : IToolGate
{
private readonly HashSet _allowedPackages =
new(allowedPackages, StringComparer.OrdinalIgnoreCase);
public string PolicyName => "sample.mock-package-restore";
public GateCost Cost => GateCost.PureCode;
public ToolGatePolicy MinimumPolicy => ToolGatePolicy.ReplaceResult;
public ValueTask InspectAsync(
GatedToolCall call,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
if (!string.Equals(call.FunctionName, "package_restore", StringComparison.OrdinalIgnoreCase))
{
return ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName));
}
if (!TryGetRequiredString(call, "package", 256, out var package) ||
!TryGetRequiredString(call, "version", 128, out var version))
{
return Block("package and version must be bounded strings");
}
return _allowedPackages.Contains(package) && IsPinnedVersion(version)
? ValueTask.FromResult(ToolGateVerdict.Allow(PolicyName))
: Block("package is outside the sample allow-list or its version is not pinned");
}
private static bool IsPinnedVersion(string version) =>
version.Split('.', StringSplitOptions.None) is [var major, var minor, var patch] &&
new[] { major, minor, patch }.All(
part => part.Length > 0 && part.All(char.IsAsciiDigit));
private ValueTask Block(string reason) =>
ValueTask.FromResult(ToolGateVerdict.Block(PolicyName, reason));
}
}