// SPDX-License-Identifier: MIT // Copyright (c) 2026 AgentEval Contributors using System.Net; using AgentEval.MAF.Gatekeeper; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; namespace AgentEval.Samples; /// Offline proof that contained work cannot starve the normal HTTP resource pool. public static class GatekeeperBulkheadIsolation { private static readonly ContainmentTarget Target = new ContainmentTarget.Session("sample-tenant", "contained-session"); public static async Task RunAsync() { GatekeeperSampleContractRenderer.Print("19"); Console.WriteLine("\n=== Gatekeeper — Bulkhead + Containment Isolation (offline) ===\n"); var normal = new BlockingHandler("normal"); var isolated = new BlockingHandler("isolated"); using var pool = new ContainmentHttpClientPool( new ContainmentHttpClientPoolOptions { NormalMaxConcurrency = 3, IsolatedMaxConcurrency = 1, NormalPrimaryHandlerFactory = () => normal, IsolatedPrimaryHandlerFactory = () => isolated, }); var store = new RoutingStore(Active(Target)); var function = HttpFunction() .WithContainmentHttpResourceIsolation(store, _ => Target, pool); Console.WriteLine("── Act 1 · a CONTAINED session floods the HTTP tool with 6 concurrent calls ──"); Task[] containedCalls; using (AgentRunScope.Begin(new SampleSession(), "bulkhead-sample", trace: null)) { containedCalls = StartCalls(function, 6); await isolated.WaitForStartedAsync(1); } Console.WriteLine($" 6 contained calls launched → {isolated.PeakCount} running in the isolated pool (1 permit); the rest queue behind it"); Console.WriteLine("\n── Act 2 · normal traffic arrives while the contained flood is still stuck ──"); store.Snapshot = ContainmentSnapshot.NotContained(Target); Task[] normalCalls; using (AgentRunScope.Begin(new SampleSession(), "bulkhead-sample", trace: null)) { normalCalls = StartCalls(function, 6); await normal.WaitForStartedAsync(3); } Console.WriteLine($" 6 normal calls launched → {normal.PeakCount} running at once on the normal pool (3 permits) — the flood took nothing from them"); Require(isolated.PeakCount == 1, "contained work must use the one-permit isolated pool"); Require(normal.PeakCount == 3, "normal work must retain all three normal permits"); Require(pool.IsolatedPeakRequests == 1 && pool.NormalPeakRequests == 3, "pool metrics must report measured peaks, not configured claims"); Console.WriteLine("\n── Act 3 · release and drain — routing is proved by the response bodies ──"); normal.Release(); var normalResults = await Task.WhenAll(normalCalls); Require(normalResults.All(result => string.Equals(result?.ToString(), "normal", StringComparison.Ordinal)), "normal work must route only through the normal client"); Console.WriteLine(" normal pool released → all 6 normal calls completed through the 'normal' client"); isolated.Release(); var containedResults = await Task.WhenAll(containedCalls); Require(containedResults.All(result => string.Equals(result?.ToString(), "isolated", StringComparison.Ordinal)), "contained work must route only through the isolated client"); Require(pool.NormalCurrentRequests == 0 && pool.IsolatedCurrentRequests == 0, "all permits must be released after completion"); Console.WriteLine(" bulkhead released → the contained flood drained serially through the 'isolated' client"); Console.WriteLine("\n Effect ledger Observed Expected"); Console.WriteLine($" normal concurrency peak {pool.NormalPeakRequests} 3 (all permits kept)"); Console.WriteLine($" contained concurrency peak {pool.IsolatedPeakRequests} 1 (bulkhead held)"); Console.WriteLine($" permits leaked at the end {pool.NormalCurrentRequests + pool.IsolatedCurrentRequests} 0"); Console.WriteLine(" ✅ contained saturation stayed inside its one-permit bulkhead while normal work used three independent permits."); } private static AIFunction HttpFunction() => AIFunctionFactory.Create( async (IServiceProvider services, CancellationToken cancellationToken) => { var client = services.GetRequiredService(); return await client.GetStringAsync("https://offline.invalid/", cancellationToken); }, "offline_http_fetch"); private static Task[] StartCalls(AIFunction function, int count) => Enumerable.Range(0, count).Select(_ => function.InvokeAsync().AsTask()).ToArray(); private static ContainmentSnapshot Active(ContainmentTarget target) => ContainmentSnapshot.FromRecord( new ContainmentRecord( target, ContainmentStatus.Active, new DateTimeOffset(2026, 8, 4, 10, 0, 0, TimeSpan.Zero), releasedAtUtc: null, reasonCode: "resource_exhaustion", evidenceReference: "sample-incident", issuer: "gatekeeper-sample", reviewer: null, version: 1, etag: "sample-etag")); private static void Require(bool condition, string message) { if (!condition) { throw new InvalidOperationException("Bulkhead sample failed: " + message + "."); } } private sealed class BlockingHandler(string response) : HttpMessageHandler { private readonly TaskCompletionSource _release = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource _started = new(TaskCreationOptions.RunContinuationsAsynchronously); private int _startedCount; private int _currentCount; private int _peakCount; public int StartedCount => Volatile.Read(ref _startedCount); public int PeakCount => Volatile.Read(ref _peakCount); public async Task WaitForStartedAsync(int count) { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10)); while (StartedCount < count) { await _started.Task.WaitAsync(timeout.Token); await Task.Yield(); } } public void Release() => _release.TrySetResult(); protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Interlocked.Increment(ref _startedCount); _started.TrySetResult(); var current = Interlocked.Increment(ref _currentCount); RecordPeak(current); try { await _release.Task.WaitAsync(cancellationToken); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response), }; } finally { Interlocked.Decrement(ref _currentCount); } } private void RecordPeak(int current) { var observed = Volatile.Read(ref _peakCount); while (current > observed) { var prior = Interlocked.CompareExchange(ref _peakCount, current, observed); if (prior == observed) { return; } observed = prior; } } } private sealed class RoutingStore(ContainmentSnapshot snapshot) : IContainmentStore { public ContainmentSnapshot Snapshot { get; set; } = snapshot; public ContainmentSnapshot GetCurrent(ContainmentTarget target) => Snapshot; public ValueTask ContainAsync( ContainmentRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public ValueTask ReleaseAsync( ContainmentReleaseAuthorization authorization, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public void Dispose() { } } private sealed class SampleSession : AgentSession; }