// SPDX-License-Identifier: MIT // Copyright (c) 2026 AgentEval Contributors #pragma warning disable AGENTEVAL_GATEKEEPER_PREVIEW001 // Sample intentionally demonstrates the preview identity-drift gate. using System.Text.Json; using AgentEval.Guardrails; using AgentEval.MAF.Gatekeeper; using AgentEval.Testing; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; namespace AgentEval.Samples; /// Offline weak-object versus stable logical-session identity takeover demonstration. public static class GatekeeperSessionIdentityTakeover { private const string SessionIdKey = "sample.session.id"; public static async Task RunAsync() { GatekeeperSampleContractRenderer.Print("26"); Console.WriteLine("\n=== Gatekeeper — Session Identity Takeover + Reload (offline) ===\n"); await WeakObjectIdentityBoundaryAsync(); await StableReloadAndPoisoningDefenseAsync(); await ConcurrentTakeoverAsync(); Console.WriteLine(" object identity only: reload resets the binding — honest limitation"); Console.WriteLine(" stable logical key: actor drift after reload blocks without identity disclosure"); Console.WriteLine(" baseline integrity: unauthorized first use cannot poison the admitted actor"); Console.WriteLine(" concurrent takeover: exactly one conflicting actor wins the atomic first binding"); Console.WriteLine(" ✅ host-attested logical identity closed the reload and race bypasses."); } private static async Task WeakObjectIdentityBoundaryAsync() { var gate = new SessionIdentityDriftGate(["alice", "bob"]); var first = await NewSessionAsync(); var reloaded = await NewSessionAsync(); SetOperator(first, "alice"); SetOperator(reloaded, "bob"); Require((await InspectAsync(gate, first)).Action == GateAction.Allow, "the first object-scoped actor must establish its baseline"); Require((await InspectAsync(gate, reloaded)).Action == GateAction.Allow, "a new object has no durable identity link and must expose that honest boundary"); } private static async Task StableReloadAndPoisoningDefenseAsync() { var gate = new SessionIdentityDriftGate( ["alice", "bob"], sessionKeySelector: SessionIdentity.FromStateBag(SessionIdKey)); var first = await NewSessionAsync(); var reloaded = await NewSessionAsync(); SetSessionId(first, "logical-42"); SetSessionId(reloaded, "logical-42"); SetOperator(first, "alice"); SetOperator(reloaded, "bob"); Require((await InspectAsync(gate, first)).Action == GateAction.Allow, "the admitted actor must establish the stable logical binding"); Require((await InspectAsync(gate, first)).Action == GateAction.Allow, "the same admitted actor must remain allowed on the stable logical session"); var drift = await InspectAsync(gate, reloaded); Require( drift.Action == GateAction.Block && drift.Reason == "session_identity_drift:operator_changed" && !drift.Reason.Contains("alice", StringComparison.Ordinal) && !drift.Reason.Contains("bob", StringComparison.Ordinal), "reload drift must block with a content-free reason"); var poisoningGate = new SessionIdentityDriftGate( ["alice"], sessionKeySelector: SessionIdentity.FromStateBag(SessionIdKey)); var poisoned = await NewSessionAsync(); SetSessionId(poisoned, "logical-43"); SetOperator(poisoned, "mallory"); Require( (await InspectAsync(poisoningGate, poisoned)).Reason == "session_identity_drift:unauthorized_operator", "an unauthorized actor must be rejected before baseline admission"); SetOperator(poisoned, "alice"); Require((await InspectAsync(poisoningGate, poisoned)).Action == GateAction.Allow, "the rejected actor must not poison the future admitted baseline"); } private static async Task ConcurrentTakeoverAsync() { var gate = new SessionIdentityDriftGate( ["alice", "bob"], sessionKeySelector: SessionIdentity.FromStateBag(SessionIdKey)); var alice = await NewSessionAsync(); var bob = await NewSessionAsync(); SetSessionId(alice, "logical-race"); SetSessionId(bob, "logical-race"); SetOperator(alice, "alice"); SetOperator(bob, "bob"); var start = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var aliceTask = Task.Run(async () => { await start.Task; return await InspectAsync(gate, alice); }); var bobTask = Task.Run(async () => { await start.Task; return await InspectAsync(gate, bob); }); start.TrySetResult(); var verdicts = await Task.WhenAll(aliceTask, bobTask); Require(verdicts.Count(verdict => verdict.Action == GateAction.Allow) == 1, "exactly one actor must establish the concurrent first-use binding"); Require(verdicts.Count(verdict => verdict.Reason == "session_identity_drift:operator_changed") == 1, "the losing conflicting actor must be refused as drift"); } private static async Task InspectAsync( SessionIdentityDriftGate gate, AgentSession session) { using var scope = AgentRunScope.Begin(session, "identity-sample", trace: null); return await gate.InspectAsync(string.Empty); } private static async Task NewSessionAsync() => await new ChatClientAgent( new ScriptedChatClient(), new ChatClientAgentOptions { Name = "identity-session-factory", ChatOptions = new ChatOptions { MaxOutputTokens = 64 }, }) .CreateSessionAsync(); private static void SetOperator(AgentSession session, string actor) => session.StateBag.SetValue( OperatorAuthGate.OperatorMetadataKey, actor, JsonSerializerOptions.Default); private static void SetSessionId(AgentSession session, string identity) => session.StateBag.SetValue( SessionIdKey, identity, JsonSerializerOptions.Default); private static void Require(bool condition, string message) { if (!condition) { throw new InvalidOperationException("Session-identity sample failed: " + message + "."); } } }