Agent Trust Kit - End-to-End PoC: Financial Services Agent
Document Information
| Field | Value |
|---|---|
| Version | 1.0.0 |
| Status | Draft Proposal |
| Created | 2026-03-01 |
| Related | PoC Use Cases |
| Goal | Runnable end-to-end PoC with minimum implementation |
[!NOTE] > AI Dependency Clarification: The core PoC defined in this document focuses strictly on the trust infrastructure (SD-JWT minting, verification, policy, receipts). It uses a deterministic programmatic agent and does NOT require an OpenAI key, Azure subscription, or any AI services to run.
An Optional MAF Variant is provided at the end of this document for teams that have AI API keys and want to test integration with the real Microsoft Agent Framework.
Real-World Scenario
Business Context
Contoso Financial Services operates a multi-agent system for insurance claim processing. The system consists of:
- Claims Orchestrator Agent - An MAF-based AI agent that coordinates the claim workflow
- Member Lookup Tool - An ASP.NET Core API that returns member profile and fee data
- Fee Calculator Tool - An ASP.NET Core API that computes applicable fees
- Claims Specialist Agent - A secondary agent that handles complex claim adjudication
Problem: Today these services use shared API keys. Any agent can call any tool with any scope. There is no per-action authorization, no audit trail, and a compromised key exposes everything.
Solution: Agent Trust Kit mints a scoped SD-JWT capability token for each call, verified at the tool, with audit receipts.
Architecture
System Diagram
flowchart TB
subgraph Agents["Agent Plane"]
ORCH["Claims Orchestrator Agent<br/>(MAF + AgentTrustMiddleware)"]
SPEC["Claims Specialist Agent<br/>(delegates from Orchestrator)"]
end
subgraph Trust["Trust Plane - Agent Trust Kit"]
ISS["CapabilityTokenIssuer<br/>(wraps SdIssuer)"]
VER["CapabilityTokenVerifier<br/>(wraps SdVerifier)"]
POL["DefaultPolicyEngine<br/>(rule-based)"]
NONCE["MemoryNonceStore<br/>(IMemoryCache)"]
REC["LoggingReceiptWriter"]
end
subgraph Tools["Tool Plane"]
MEMBER["Member Lookup API<br/>(ASP.NET Core + Verifier Middleware)"]
FEES["Fee Calculator API<br/>(ASP.NET Core + Verifier Middleware)"]
end
ORCH -->|"1. Policy check"| POL
ORCH -->|"2. Mint SD-JWT"| ISS
ORCH -->|"3. Call with token"| MEMBER
ORCH -->|"3. Call with token"| FEES
MEMBER -->|"4. Verify token"| VER
FEES -->|"4. Verify token"| VER
VER -->|"5. Replay check"| NONCE
VER -->|"6. Emit receipt"| REC
ORCH -->|"7. Delegate"| SPEC
SPEC -->|"8. Sub-capability"| ISS
Project Structure (Minimum PoC)
samples/AgentTrustKit.PoC/
AgentTrustKit.PoC.sln
src/
AgentTrust.Core/ # Minimum capability token engine
AgentTrust.Core.csproj
CapabilityClaim.cs # Capability claim model
CapabilityContext.cs # Context/correlation model
CapabilityTokenIssuer.cs # Wraps SdIssuer
CapabilityTokenVerifier.cs # Wraps SdVerifier
CapabilityTokenResult.cs # Issuance result
CapabilityVerificationResult.cs # Verification result
INonceStore.cs # Replay prevention interface
MemoryNonceStore.cs # In-memory implementation
AuditReceipt.cs # Receipt model
IReceiptWriter.cs # Receipt writer interface
LoggingReceiptWriter.cs # Logger-based implementation
AgentTrust.Policy/ # Minimum policy engine
AgentTrust.Policy.csproj
IPolicyEngine.cs # Policy interface
PolicyRule.cs # Rule model
PolicyDecision.cs # Decision result
DefaultPolicyEngine.cs # First-match rule engine
PolicyBuilder.cs # Fluent builder
AgentTrust.AspNetCore/ # Minimum ASP.NET Core middleware
AgentTrust.AspNetCore.csproj
AgentTrustMiddleware.cs # Verification middleware
RequireCapabilityAttribute.cs # Endpoint-level authorization
HttpContextExtensions.cs # GetVerifiedCapability()
ServiceCollectionExtensions.cs # DI registration
apps/
MemberLookupApi/ # Tool server (ASP.NET Core)
MemberLookupApi.csproj
Program.cs
FeeCalculatorApi/ # Tool server (ASP.NET Core)
FeeCalculatorApi.csproj
Program.cs
ClaimsOrchestratorAgent/ # Agent (console app, NO AI KEYS REQUIRED)
ClaimsOrchestratorAgent.csproj
Program.cs
ClaimsOrchestratorAgent.Maf/ # Optional: Real MAF integration (OpenAI KEY REQUIRED)
ClaimsOrchestratorAgent.Maf.csproj
Program.cs
tests/
AgentTrust.Core.Tests/
CapabilityTokenIssuerTests.cs
CapabilityTokenVerifierTests.cs
MemoryNonceStoreTests.cs
AgentTrust.E2E.Tests/
EndToEndFlowTests.cs
Detailed Workflow: Agent-to-Tool Call
Workflow Sequence (Step-by-Step)
sequenceDiagram
autonumber
participant Agent as Claims Orchestrator
participant Policy as PolicyEngine
participant Issuer as CapabilityTokenIssuer
participant SdIssuer as SdIssuer (core)
participant HTTP as HttpClient
participant MW as AgentTrust Middleware
participant Verifier as CapabilityTokenVerifier
participant SdVerifier as SdVerifier (core)
participant Nonce as MemoryNonceStore
participant API as MemberLookup Endpoint
participant Receipt as ReceiptWriter
Note over Agent: Step 1: Agent decides to call MemberLookup.GetProfile
Agent->>Policy: EvaluateAsync(agentId, tool=MemberLookup, action=GetProfile)
Policy->>Policy: Match rules (first-match, priority-ordered)
Policy-->>Agent: Permit (maxResults=50, lifetime=60s)
Note over Agent: Step 2: Mint scoped capability token
Agent->>Issuer: Mint(CapabilityTokenOptions)
Issuer->>Issuer: Build JwtPayload with cap, ctx, iss, aud, exp, jti
Issuer->>Issuer: Set DisclosureStructure (cap.tool, cap.action, cap.limits = disclosable)
Issuer->>SdIssuer: Issue(payload, options)
SdIssuer->>SdIssuer: Create disclosures for selectively disclosable claims
SdIssuer->>SdIssuer: Compute digests, embed in _sd array
SdIssuer->>SdIssuer: Sign SD-JWT with agent private key (ES256)
SdIssuer-->>Issuer: IssuerOutput (issuance string + disclosures)
Issuer->>Nonce: TryMarkAsUsedAsync(jti, expiry) - reserve nonce
Issuer-->>Agent: CapabilityTokenResult (token, tokenId, expiresAt)
Note over Agent: Step 3: Call tool with token
Agent->>HTTP: GET /api/members/M-12345 [Authorization: SdJwt <token>]
Note over MW: Step 4: Inbound verification at tool server
HTTP->>MW: Request arrives
MW->>MW: Extract token from Authorization header
MW->>MW: Check ExcludedPaths (/health, /ready)
MW->>Verifier: VerifyAsync(token, options)
Verifier->>Verifier: Parse SD-JWT compact serialization
Verifier->>Verifier: Extract issuer from unverified payload
Verifier->>Verifier: Lookup trusted issuer key
Verifier->>SdVerifier: VerifyAsync(presentation, issuerKeyProvider)
SdVerifier->>SdVerifier: Validate JWT signature (ES256)
SdVerifier->>SdVerifier: Match disclosure digests against _sd
SdVerifier->>SdVerifier: Rehydrate disclosed claims
SdVerifier-->>Verifier: VerificationResult (ClaimsPrincipal)
Verifier->>Verifier: Validate exp (not expired, with clock skew)
Verifier->>Verifier: Validate aud (matches tool audience)
Verifier->>Nonce: IsUsedAsync(jti) - replay check
Nonce-->>Verifier: false (not yet used at this tool)
Verifier->>Nonce: TryMarkAsUsedAsync(jti, expiry) - mark used
Verifier->>Verifier: Extract CapabilityClaim from verified payload
Verifier-->>MW: CapabilityVerificationResult (valid, capability, context)
Note over MW: Step 5: Enrich HttpContext and proceed
MW->>MW: Store capability in HttpContext.Items
MW->>API: Invoke endpoint handler
Note over API: Step 6: Endpoint uses verified capability
API->>API: ctx.GetVerifiedCapability() - reads from HttpContext
API->>API: Apply limits (maxResults=50)
API->>API: Query member data
API-->>MW: Response (200 OK)
Note over MW: Step 7: Emit audit receipt
MW->>Receipt: WriteAsync(AuditReceipt: allow, tool, action, jti, correlationId)
MW-->>HTTP: Response
HTTP-->>Agent: Member data
Minimum Implementation
1. CapabilityClaim.cs - Capability Model
using System.Text.Json.Serialization;
namespace AgentTrust.Core;
/// <summary>
/// The capability scope embedded in the "cap" claim of an SD-JWT capability token.
/// </summary>
public record CapabilityClaim
{
[JsonPropertyName("tool")]
public required string Tool { get; init; }
[JsonPropertyName("action")]
public required string Action { get; init; }
[JsonPropertyName("resource")]
public string? Resource { get; init; }
[JsonPropertyName("limits")]
public CapabilityLimits? Limits { get; init; }
[JsonPropertyName("purpose")]
public string? Purpose { get; init; }
}
public record CapabilityLimits
{
[JsonPropertyName("max_results")]
public int? MaxResults { get; init; }
[JsonPropertyName("max_invocations")]
public int? MaxInvocations { get; init; }
}
2. CapabilityContext.cs - Correlation Model
using System.Text.Json.Serialization;
namespace AgentTrust.Core;
/// <summary>
/// Execution context for workflow tracing. Embedded in the "ctx" claim.
/// </summary>
public record CapabilityContext
{
[JsonPropertyName("correlation_id")]
public required string CorrelationId { get; init; }
[JsonPropertyName("workflow_id")]
public string? WorkflowId { get; init; }
[JsonPropertyName("step_id")]
public string? StepId { get; init; }
}
3. INonceStore.cs + MemoryNonceStore.cs - Replay Prevention
namespace AgentTrust.Core;
/// <summary>
/// Tracks token IDs to prevent replay attacks.
/// </summary>
public interface INonceStore
{
Task<bool> TryMarkAsUsedAsync(string tokenId, DateTimeOffset expiry,
CancellationToken ct = default);
Task<bool> IsUsedAsync(string tokenId, CancellationToken ct = default);
}
using Microsoft.Extensions.Caching.Memory;
namespace AgentTrust.Core;
/// <summary>
/// In-memory nonce store. Entries auto-evict after token expiry.
/// </summary>
public class MemoryNonceStore : INonceStore
{
private readonly IMemoryCache _cache;
public MemoryNonceStore(IMemoryCache cache)
{
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
}
public Task<bool> TryMarkAsUsedAsync(string tokenId, DateTimeOffset expiry,
CancellationToken ct = default)
{
// TryGetValue + Set is atomic enough for PoC; production needs distributed lock
if (_cache.TryGetValue(tokenId, out _))
{
return Task.FromResult(false); // Already used
}
_cache.Set(tokenId, true, expiry);
return Task.FromResult(true);
}
public Task<bool> IsUsedAsync(string tokenId, CancellationToken ct = default)
{
return Task.FromResult(_cache.TryGetValue(tokenId, out _));
}
}
4. CapabilityTokenIssuer.cs - Wraps SdIssuer
This is the core of the PoC. It wraps the real SdIssuer from SdJwt.Net:
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IdentityModel.Tokens;
using SdJwt.Net.Issuer;
namespace AgentTrust.Core;
public record CapabilityTokenOptions
{
public required string Issuer { get; init; }
public required string Audience { get; init; }
public required CapabilityClaim Capability { get; init; }
public required CapabilityContext Context { get; init; }
public TimeSpan Lifetime { get; init; } = TimeSpan.FromSeconds(60);
}
public record CapabilityTokenResult
{
public required string Token { get; init; }
public required string TokenId { get; init; }
public required DateTimeOffset ExpiresAt { get; init; }
}
/// <summary>
/// Mints SD-JWT capability tokens by delegating to <see cref="SdIssuer"/>.
///
/// The token payload structure:
/// iss - agent identity (always visible)
/// aud - tool/agent audience (always visible)
/// iat - issued-at (always visible)
/// exp - expiry (always visible)
/// jti - unique token ID (always visible)
/// cap - capability object (selectively disclosable sub-claims)
/// ctx - context object (selectively disclosable sub-claims)
/// </summary>
public class CapabilityTokenIssuer
{
private readonly SdIssuer _sdIssuer;
private readonly INonceStore _nonceStore;
private readonly ILogger<CapabilityTokenIssuer> _logger;
public CapabilityTokenIssuer(
SecurityKey signingKey,
string signingAlgorithm,
INonceStore nonceStore,
ILogger<CapabilityTokenIssuer>? logger = null)
{
_sdIssuer = new SdIssuer(signingKey, signingAlgorithm);
_nonceStore = nonceStore ?? throw new ArgumentNullException(nameof(nonceStore));
_logger = logger ?? NullLogger<CapabilityTokenIssuer>.Instance;
}
public CapabilityTokenResult Mint(CapabilityTokenOptions options)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentException.ThrowIfNullOrWhiteSpace(options.Issuer);
ArgumentException.ThrowIfNullOrWhiteSpace(options.Audience);
var now = DateTimeOffset.UtcNow;
var expires = now.Add(options.Lifetime);
var jti = Guid.NewGuid().ToString("N");
// Build the JWT payload with capability and context as nested objects
var capJson = JsonSerializer.Serialize(options.Capability);
var ctxJson = JsonSerializer.Serialize(options.Context);
var claims = new JwtPayload
{
{ "iss", options.Issuer },
{ "aud", options.Audience },
{ "iat", now.ToUnixTimeSeconds() },
{ "exp", expires.ToUnixTimeSeconds() },
{ "jti", jti },
{ "cap", JsonSerializer.Deserialize<Dictionary<string, object>>(capJson) },
{ "ctx", JsonSerializer.Deserialize<Dictionary<string, object>>(ctxJson) }
};
// Make cap sub-claims selectively disclosable:
// tool and action are always disclosed (tool needs them),
// resource and limits are disclosable (tool may or may not need them),
// context sub-claims (workflow_id, step_id) are disclosable.
var disclosureStructure = new
{
cap = new
{
resource = options.Capability.Resource != null,
limits = options.Capability.Limits != null,
purpose = options.Capability.Purpose != null
},
ctx = new
{
workflow_id = options.Context.WorkflowId != null,
step_id = options.Context.StepId != null
}
};
var issuanceOptions = new SdIssuanceOptions
{
DisclosureStructure = disclosureStructure
};
// Delegate to the real SdIssuer from SdJwt.Net
var output = _sdIssuer.Issue(claims, issuanceOptions);
_logger.LogInformation(
"Minted capability token {Jti} for {Tool}.{Action} -> {Audience}, expires {Expiry}",
jti, options.Capability.Tool, options.Capability.Action,
options.Audience, expires);
return new CapabilityTokenResult
{
Token = output.Issuance,
TokenId = jti,
ExpiresAt = expires
};
}
}
5. CapabilityTokenVerifier.cs - Wraps SdVerifier
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.IdentityModel.Tokens;
using SdJwt.Net.Verifier;
namespace AgentTrust.Core;
public record CapabilityVerificationOptions
{
public required string ExpectedAudience { get; init; }
public required IReadOnlyDictionary<string, SecurityKey> TrustedIssuers { get; init; }
public bool EnforceReplayPrevention { get; init; } = true;
public TimeSpan ClockSkewTolerance { get; init; } = TimeSpan.FromSeconds(30);
}
public record CapabilityVerificationResult
{
public bool IsValid { get; init; }
public string? Error { get; init; }
public string? ErrorCode { get; init; }
public CapabilityClaim? Capability { get; init; }
public CapabilityContext? Context { get; init; }
public string? TokenId { get; init; }
public string? Issuer { get; init; }
public static CapabilityVerificationResult Success(
CapabilityClaim capability, CapabilityContext context,
string tokenId, string issuer) => new()
{
IsValid = true, Capability = capability, Context = context,
TokenId = tokenId, Issuer = issuer
};
public static CapabilityVerificationResult Failure(
string error, string errorCode) => new()
{
IsValid = false, Error = error, ErrorCode = errorCode
};
}
/// <summary>
/// Verifies SD-JWT capability tokens by delegating to <see cref="SdVerifier"/>.
/// Performs: signature verification -> claim extraction -> audience check ->
/// expiry check -> replay prevention -> capability extraction.
/// </summary>
public class CapabilityTokenVerifier
{
private readonly INonceStore _nonceStore;
private readonly ILogger<CapabilityTokenVerifier> _logger;
public CapabilityTokenVerifier(
INonceStore nonceStore,
ILogger<CapabilityTokenVerifier>? logger = null)
{
_nonceStore = nonceStore ?? throw new ArgumentNullException(nameof(nonceStore));
_logger = logger ?? NullLogger<CapabilityTokenVerifier>.Instance;
}
public async Task<CapabilityVerificationResult> VerifyAsync(
string token,
CapabilityVerificationOptions options,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(token);
ArgumentNullException.ThrowIfNull(options);
try
{
// Step 1: Parse the SD-JWT to extract the unverified issuer
// The SD-JWT format is: <jwt>~<disclosure1>~<disclosure2>~...~
var jwtPart = token.Split('~')[0];
var unverifiedJwt = new JwtSecurityToken(jwtPart);
var issuer = unverifiedJwt.Issuer;
if (string.IsNullOrEmpty(issuer) ||
!options.TrustedIssuers.TryGetValue(issuer, out var issuerKey))
{
return CapabilityVerificationResult.Failure(
$"Untrusted issuer: {issuer}", "untrusted_issuer");
}
// Step 2: Verify using SdVerifier with the trusted issuer key
var sdVerifier = new SdVerifier(
(_, _) => Task.FromResult<SecurityKey>(issuerKey));
var verificationResult = await sdVerifier.VerifyAsync(token);
var principal = verificationResult.ClaimsPrincipal;
// Step 3: Validate audience
var aud = principal.FindFirst("aud")?.Value;
if (aud != options.ExpectedAudience)
{
return CapabilityVerificationResult.Failure(
$"Audience mismatch: expected {options.ExpectedAudience}, got {aud}",
"audience_mismatch");
}
// Step 4: Validate expiry
var expClaim = principal.FindFirst("exp")?.Value;
if (expClaim != null)
{
var expiry = DateTimeOffset.FromUnixTimeSeconds(long.Parse(expClaim));
if (DateTimeOffset.UtcNow > expiry.Add(options.ClockSkewTolerance))
{
return CapabilityVerificationResult.Failure(
"Token has expired", "token_expired");
}
}
// Step 5: Replay prevention
var jti = principal.FindFirst("jti")?.Value;
if (string.IsNullOrEmpty(jti))
{
return CapabilityVerificationResult.Failure(
"Token missing jti claim", "missing_jti");
}
if (options.EnforceReplayPrevention)
{
var expiry = expClaim != null
? DateTimeOffset.FromUnixTimeSeconds(long.Parse(expClaim))
: DateTimeOffset.UtcNow.AddMinutes(5);
var isNew = await _nonceStore.TryMarkAsUsedAsync(jti, expiry, ct);
if (!isNew)
{
return CapabilityVerificationResult.Failure(
"Token has already been used (replay detected)", "token_replayed");
}
}
// Step 6: Extract capability and context claims
var capClaim = ExtractClaim<CapabilityClaim>(principal, "cap");
var ctxClaim = ExtractClaim<CapabilityContext>(principal, "ctx");
if (capClaim == null)
{
return CapabilityVerificationResult.Failure(
"Token missing capability (cap) claim", "missing_capability");
}
if (ctxClaim == null)
{
return CapabilityVerificationResult.Failure(
"Token missing context (ctx) claim", "missing_context");
}
_logger.LogInformation(
"Verified capability token {Jti}: {Tool}.{Action} from {Issuer}",
jti, capClaim.Tool, capClaim.Action, issuer);
return CapabilityVerificationResult.Success(capClaim, ctxClaim, jti, issuer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Capability token verification failed");
return CapabilityVerificationResult.Failure(
ex.Message, "verification_error");
}
}
private static T? ExtractClaim<T>(ClaimsPrincipal principal, string claimType)
where T : class
{
var claimValue = principal.FindFirst(claimType)?.Value;
if (string.IsNullOrEmpty(claimValue))
return null;
try
{
return JsonSerializer.Deserialize<T>(claimValue);
}
catch
{
return null;
}
}
}
6. AuditReceipt.cs - Audit Trail
using Microsoft.Extensions.Logging;
namespace AgentTrust.Core;
public enum ReceiptDecision { Allow, Deny }
public record AuditReceipt
{
public required string TokenId { get; init; }
public required DateTimeOffset Timestamp { get; init; }
public required ReceiptDecision Decision { get; init; }
public required string Tool { get; init; }
public required string Action { get; init; }
public required string CorrelationId { get; init; }
public string? DenyReason { get; init; }
public long? DurationMs { get; init; }
}
public interface IReceiptWriter
{
Task WriteAsync(AuditReceipt receipt, CancellationToken ct = default);
}
/// <summary>
/// Writes receipts as structured log entries. Production: swap for DB/event sink.
/// </summary>
public class LoggingReceiptWriter : IReceiptWriter
{
private readonly ILogger<LoggingReceiptWriter> _logger;
public LoggingReceiptWriter(ILogger<LoggingReceiptWriter> logger)
{
_logger = logger;
}
public Task WriteAsync(AuditReceipt receipt, CancellationToken ct = default)
{
_logger.LogInformation(
"AUDIT RECEIPT | Decision={Decision} | Tool={Tool} | Action={Action} | " +
"TokenId={TokenId} | CorrelationId={CorrelationId} | DenyReason={DenyReason} | " +
"Duration={DurationMs}ms | Timestamp={Timestamp:O}",
receipt.Decision, receipt.Tool, receipt.Action,
receipt.TokenId, receipt.CorrelationId, receipt.DenyReason,
receipt.DurationMs, receipt.Timestamp);
return Task.CompletedTask;
}
}
7. DefaultPolicyEngine.cs - Rule-Based Authorization
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace AgentTrust.Policy;
public interface IPolicyEngine
{
Task<PolicyDecision> EvaluateAsync(PolicyRequest request,
CancellationToken ct = default);
}
public record PolicyRequest
{
public required string AgentId { get; init; }
public required string Tool { get; init; }
public required string Action { get; init; }
public string? Resource { get; init; }
}
public record PolicyDecision
{
public required bool IsPermitted { get; init; }
public string? DenialReason { get; init; }
public string? DenialCode { get; init; }
public int? MaxResults { get; init; }
public TimeSpan? MaxTokenLifetime { get; init; }
public static PolicyDecision Permit(int? maxResults = null,
TimeSpan? maxLifetime = null) => new()
{
IsPermitted = true,
MaxResults = maxResults,
MaxTokenLifetime = maxLifetime
};
public static PolicyDecision Deny(string reason, string code) => new()
{
IsPermitted = false,
DenialReason = reason,
DenialCode = code
};
}
public enum PolicyEffect { Allow, Deny }
public record PolicyRule(
string Name,
string AgentPattern,
string ToolPattern,
string ActionPattern,
PolicyEffect Effect,
int? MaxResults = null,
TimeSpan? MaxTokenLifetime = null,
int Priority = 0);
/// <summary>
/// Evaluates rules in priority order, first match wins. Default: deny.
/// </summary>
public class DefaultPolicyEngine : IPolicyEngine
{
private readonly IReadOnlyList<PolicyRule> _rules;
private readonly ILogger<DefaultPolicyEngine> _logger;
public DefaultPolicyEngine(
IReadOnlyList<PolicyRule> rules,
ILogger<DefaultPolicyEngine>? logger = null)
{
_rules = rules.OrderByDescending(r => r.Priority).ToList();
_logger = logger ?? NullLogger<DefaultPolicyEngine>.Instance;
}
public Task<PolicyDecision> EvaluateAsync(PolicyRequest request,
CancellationToken ct = default)
{
foreach (var rule in _rules)
{
if (Matches(rule.AgentPattern, request.AgentId) &&
Matches(rule.ToolPattern, request.Tool) &&
Matches(rule.ActionPattern, request.Action))
{
_logger.LogInformation(
"Policy rule '{Rule}' matched: {Agent} -> {Tool}.{Action} = {Effect}",
rule.Name, request.AgentId, request.Tool, request.Action, rule.Effect);
return Task.FromResult(rule.Effect == PolicyEffect.Allow
? PolicyDecision.Permit(rule.MaxResults, rule.MaxTokenLifetime)
: PolicyDecision.Deny($"Denied by rule: {rule.Name}", "policy_denied"));
}
}
_logger.LogWarning(
"No policy rule matched for {Agent} -> {Tool}.{Action}. Default: DENY.",
request.AgentId, request.Tool, request.Action);
return Task.FromResult(
PolicyDecision.Deny("No matching policy rule (default deny)", "no_rule_match"));
}
private static bool Matches(string pattern, string value)
{
if (pattern == "*") return true;
if (pattern.EndsWith("*"))
return value.StartsWith(pattern[..^1], StringComparison.OrdinalIgnoreCase);
return string.Equals(pattern, value, StringComparison.OrdinalIgnoreCase);
}
}
8. AgentTrustMiddleware.cs - ASP.NET Core Inbound Verification
using AgentTrust.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace AgentTrust.AspNetCore;
public record AgentTrustOptions
{
public required string Audience { get; init; }
public required CapabilityVerificationOptions VerificationOptions { get; init; }
public HashSet<string> ExcludedPaths { get; init; } = new() { "/health", "/ready" };
public string HeaderName { get; init; } = "Authorization";
public string HeaderPrefix { get; init; } = "SdJwt";
public bool EmitReceipts { get; init; } = true;
}
public class AgentTrustMiddleware
{
private readonly RequestDelegate _next;
private readonly CapabilityTokenVerifier _verifier;
private readonly IReceiptWriter _receiptWriter;
private readonly AgentTrustOptions _options;
private readonly ILogger<AgentTrustMiddleware> _logger;
private const string CapabilityKey = "AgentTrust.Capability";
private const string ContextKey = "AgentTrust.Context";
private const string IssuerKey = "AgentTrust.Issuer";
public AgentTrustMiddleware(
RequestDelegate next,
CapabilityTokenVerifier verifier,
IReceiptWriter receiptWriter,
AgentTrustOptions options,
ILogger<AgentTrustMiddleware> logger)
{
_next = next;
_verifier = verifier;
_receiptWriter = receiptWriter;
_options = options;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var path = context.Request.Path.Value ?? "";
// Skip excluded paths
if (_options.ExcludedPaths.Any(p =>
path.Equals(p, StringComparison.OrdinalIgnoreCase)))
{
await _next(context);
return;
}
// Extract token
var authHeader = context.Request.Headers[_options.HeaderName].FirstOrDefault();
if (string.IsNullOrEmpty(authHeader) ||
!authHeader.StartsWith($"{_options.HeaderPrefix} "))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsJsonAsync(new
{
error = "missing_capability_token",
message = "Authorization header with SdJwt token required"
});
return;
}
var token = authHeader[(_options.HeaderPrefix.Length + 1)..];
var sw = System.Diagnostics.Stopwatch.StartNew();
// Verify
var result = await _verifier.VerifyAsync(token, _options.VerificationOptions);
sw.Stop();
if (!result.IsValid)
{
_logger.LogWarning("Capability token verification failed: {Error}", result.Error);
if (_options.EmitReceipts && result.TokenId != null)
{
await _receiptWriter.WriteAsync(new AuditReceipt
{
TokenId = result.TokenId ?? "unknown",
Timestamp = DateTimeOffset.UtcNow,
Decision = ReceiptDecision.Deny,
Tool = "unknown",
Action = "unknown",
CorrelationId = "unknown",
DenyReason = result.ErrorCode,
DurationMs = sw.ElapsedMilliseconds
});
}
context.Response.StatusCode = 403;
await context.Response.WriteAsJsonAsync(new
{
error = result.ErrorCode,
message = result.Error
});
return;
}
// Enrich HttpContext
context.Items[CapabilityKey] = result.Capability;
context.Items[ContextKey] = result.Context;
context.Items[IssuerKey] = result.Issuer;
// Proceed to endpoint
await _next(context);
// Emit success receipt
if (_options.EmitReceipts)
{
await _receiptWriter.WriteAsync(new AuditReceipt
{
TokenId = result.TokenId!,
Timestamp = DateTimeOffset.UtcNow,
Decision = ReceiptDecision.Allow,
Tool = result.Capability!.Tool,
Action = result.Capability.Action,
CorrelationId = result.Context!.CorrelationId,
DurationMs = sw.ElapsedMilliseconds
});
}
}
}
/// <summary>
/// HttpContext extension methods for accessing verified capability data.
/// </summary>
public static class HttpContextExtensions
{
public static CapabilityClaim? GetVerifiedCapability(this HttpContext ctx)
=> ctx.Items["AgentTrust.Capability"] as CapabilityClaim;
public static CapabilityContext? GetCapabilityContext(this HttpContext ctx)
=> ctx.Items["AgentTrust.Context"] as CapabilityContext;
public static string? GetAgentIssuer(this HttpContext ctx)
=> ctx.Items["AgentTrust.Issuer"] as string;
}
9. MemberLookupApi - Protected Tool Server
// apps/MemberLookupApi/Program.cs
using System.Security.Cryptography;
using AgentTrust.AspNetCore;
using AgentTrust.Core;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// Load agent public keys (in production, from JWKS endpoint or config)
var agentPublicKeyJson = builder.Configuration["AgentTrust:OrchestratorPublicKey"]
?? throw new InvalidOperationException("Agent public key not configured");
var agentPublicKey = JsonWebKey.Create(agentPublicKeyJson);
// Register services
var nonceStore = new MemoryNonceStore(new MemoryCache(new MemoryCacheOptions()));
var verifier = new CapabilityTokenVerifier(nonceStore);
var receiptWriter = new LoggingReceiptWriter(
builder.Services.BuildServiceProvider().GetRequiredService<ILogger<LoggingReceiptWriter>>());
var trustOptions = new AgentTrustOptions
{
Audience = "tool://member-lookup",
VerificationOptions = new CapabilityVerificationOptions
{
ExpectedAudience = "tool://member-lookup",
TrustedIssuers = new Dictionary<string, SecurityKey>
{
["agent://claims-orchestrator"] = agentPublicKey
}
}
};
var app = builder.Build();
// Add Agent Trust verification middleware
app.UseMiddleware<AgentTrustMiddleware>(verifier, receiptWriter, trustOptions,
app.Services.GetRequiredService<ILogger<AgentTrustMiddleware>>());
// Health check (excluded from verification)
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));
// Protected endpoint - requires MemberLookup.GetProfile capability
app.MapGet("/api/members/{memberId}", (string memberId, HttpContext ctx) =>
{
var capability = ctx.GetVerifiedCapability();
var context = ctx.GetCapabilityContext();
var agentIssuer = ctx.GetAgentIssuer();
// Enforce tool+action match
if (capability?.Tool != "MemberLookup" || capability.Action != "GetProfile")
{
return Results.Json(new { error = "insufficient_capability",
message = $"Required: MemberLookup.GetProfile, got: {capability?.Tool}.{capability?.Action}" },
statusCode: 403);
}
// Enforce limits
var maxResults = capability.Limits?.MaxResults ?? 100;
// Simulate member lookup
var member = new
{
MemberId = memberId,
Name = "Jane Doe",
Plan = "Gold",
JoinDate = "2023-01-15",
Fees = new[]
{
new { Type = "Annual", Amount = 250.00m },
new { Type = "Processing", Amount = 15.00m }
},
AgentInfo = new
{
RequestedBy = agentIssuer,
CorrelationId = context?.CorrelationId,
LimitApplied = maxResults
}
};
return Results.Ok(member);
});
// Protected endpoint - requires MemberLookup.GetFees capability
app.MapGet("/api/members/{memberId}/fees", (string memberId, HttpContext ctx) =>
{
var capability = ctx.GetVerifiedCapability();
if (capability?.Tool != "MemberLookup" || capability.Action != "GetFees")
{
return Results.Json(new { error = "insufficient_capability" }, statusCode: 403);
}
var fees = new[]
{
new { Type = "Annual", Amount = 250.00m, DueDate = "2026-03-01" },
new { Type = "Processing", Amount = 15.00m, DueDate = "2026-03-01" },
new { Type = "Late Fee", Amount = 25.00m, DueDate = "2026-02-15" }
};
var maxResults = capability.Limits?.MaxResults ?? fees.Length;
return Results.Ok(fees.Take(maxResults));
});
app.Run();
10. ClaimsOrchestratorAgent - Agent Console App
// apps/ClaimsOrchestratorAgent/Program.cs
using System.Security.Cryptography;
using System.Text.Json;
using AgentTrust.Core;
using AgentTrust.Policy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
// =====================================================
// BOOTSTRAP: Agent identity + key setup
// =====================================================
using var loggerFactory = LoggerFactory.Create(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("=== Claims Orchestrator Agent starting ===");
// Generate agent signing key pair (in production, use HSM/Key Vault)
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var agentPrivateKey = new ECDsaSecurityKey(ecdsa) { KeyId = "orchestrator-key-1" };
// Initialize infrastructure
var nonceStore = new MemoryNonceStore(new MemoryCache(new MemoryCacheOptions()));
var receiptWriter = new LoggingReceiptWriter(loggerFactory.CreateLogger<LoggingReceiptWriter>());
var issuer = new CapabilityTokenIssuer(
agentPrivateKey, SecurityAlgorithms.EcdsaSha256, nonceStore,
loggerFactory.CreateLogger<CapabilityTokenIssuer>());
// =====================================================
// POLICY: Define what this agent is allowed to do
// =====================================================
var policyRules = new List<PolicyRule>
{
new("Allow MemberLookup",
AgentPattern: "agent://claims-orchestrator",
ToolPattern: "MemberLookup",
ActionPattern: "*",
Effect: PolicyEffect.Allow,
MaxResults: 50,
MaxTokenLifetime: TimeSpan.FromSeconds(60)),
new("Allow FeeCalculator reads",
AgentPattern: "agent://claims-orchestrator",
ToolPattern: "FeeCalculator",
ActionPattern: "Calculate",
Effect: PolicyEffect.Allow,
MaxTokenLifetime: TimeSpan.FromSeconds(30)),
new("Block admin tools",
AgentPattern: "*",
ToolPattern: "AdminConsole",
ActionPattern: "*",
Effect: PolicyEffect.Deny,
Priority: 100)
};
var policyEngine = new DefaultPolicyEngine(policyRules,
loggerFactory.CreateLogger<DefaultPolicyEngine>());
var correlationId = Guid.NewGuid().ToString("N")[..12];
// =====================================================
// SCENARIO 1: Successful member lookup
// =====================================================
logger.LogInformation("--- Scenario 1: Agent calls MemberLookup.GetProfile ---");
// Step 1: Check policy
var decision = await policyEngine.EvaluateAsync(new PolicyRequest
{
AgentId = "agent://claims-orchestrator",
Tool = "MemberLookup",
Action = "GetProfile"
});
if (decision.IsPermitted)
{
logger.LogInformation("Policy: PERMIT (maxResults={Max}, lifetime={Lifetime}s)",
decision.MaxResults, decision.MaxTokenLifetime?.TotalSeconds);
// Step 2: Mint capability token
var tokenResult = issuer.Mint(new CapabilityTokenOptions
{
Issuer = "agent://claims-orchestrator",
Audience = "tool://member-lookup",
Capability = new CapabilityClaim
{
Tool = "MemberLookup",
Action = "GetProfile",
Resource = "member:M-12345",
Limits = new CapabilityLimits { MaxResults = decision.MaxResults }
},
Context = new CapabilityContext
{
CorrelationId = correlationId,
WorkflowId = "claim-processing-001",
StepId = "step-1-member-lookup"
},
Lifetime = decision.MaxTokenLifetime ?? TimeSpan.FromSeconds(60)
});
logger.LogInformation("Minted token: id={Id}, expires={Exp}",
tokenResult.TokenId, tokenResult.ExpiresAt);
// Step 3: Call tool with token
// In a real deployment, this would be an HTTP call:
// httpClient.DefaultRequestHeaders.Add("Authorization", $"SdJwt {tokenResult.Token}");
// var response = await httpClient.GetAsync("http://member-lookup/api/members/M-12345");
logger.LogInformation("Token attached to request: Authorization: SdJwt <{Len} chars>",
tokenResult.Token.Length);
// Step 4: Verify the token (simulating tool-side verification)
var verifier = new CapabilityTokenVerifier(nonceStore,
loggerFactory.CreateLogger<CapabilityTokenVerifier>());
var verifyResult = await verifier.VerifyAsync(tokenResult.Token,
new CapabilityVerificationOptions
{
ExpectedAudience = "tool://member-lookup",
TrustedIssuers = new Dictionary<string, SecurityKey>
{
["agent://claims-orchestrator"] = agentPrivateKey
}
});
if (verifyResult.IsValid)
{
logger.LogInformation("VERIFIED: tool={Tool}, action={Action}, limits.maxResults={Max}",
verifyResult.Capability!.Tool, verifyResult.Capability.Action,
verifyResult.Capability.Limits?.MaxResults);
// Step 5: Emit receipt
await receiptWriter.WriteAsync(new AuditReceipt
{
TokenId = verifyResult.TokenId!,
Timestamp = DateTimeOffset.UtcNow,
Decision = ReceiptDecision.Allow,
Tool = verifyResult.Capability.Tool,
Action = verifyResult.Capability.Action,
CorrelationId = verifyResult.Context!.CorrelationId
});
}
else
{
logger.LogError("Verification FAILED: {Error}", verifyResult.Error);
}
}
// =====================================================
// SCENARIO 2: Replay prevention
// =====================================================
logger.LogInformation("--- Scenario 2: Replay prevention ---");
var replayToken = issuer.Mint(new CapabilityTokenOptions
{
Issuer = "agent://claims-orchestrator",
Audience = "tool://member-lookup",
Capability = new CapabilityClaim { Tool = "MemberLookup", Action = "GetProfile" },
Context = new CapabilityContext { CorrelationId = correlationId },
Lifetime = TimeSpan.FromSeconds(60)
});
var verifier2 = new CapabilityTokenVerifier(nonceStore,
loggerFactory.CreateLogger<CapabilityTokenVerifier>());
var opts = new CapabilityVerificationOptions
{
ExpectedAudience = "tool://member-lookup",
TrustedIssuers = new Dictionary<string, SecurityKey>
{
["agent://claims-orchestrator"] = agentPrivateKey
}
};
var firstUse = await verifier2.VerifyAsync(replayToken.Token, opts);
logger.LogInformation("First use: valid={Valid}", firstUse.IsValid);
var replayAttempt = await verifier2.VerifyAsync(replayToken.Token, opts);
logger.LogInformation("Replay attempt: valid={Valid}, error={Error}",
replayAttempt.IsValid, replayAttempt.ErrorCode);
// =====================================================
// SCENARIO 3: Policy denial
// =====================================================
logger.LogInformation("--- Scenario 3: Policy denies AdminConsole access ---");
var adminDecision = await policyEngine.EvaluateAsync(new PolicyRequest
{
AgentId = "agent://claims-orchestrator",
Tool = "AdminConsole",
Action = "DeleteAll"
});
logger.LogInformation("AdminConsole decision: permitted={P}, reason={R}",
adminDecision.IsPermitted, adminDecision.DenialReason);
// =====================================================
// SCENARIO 4: Expired token
// =====================================================
logger.LogInformation("--- Scenario 4: Token expiry containment ---");
var shortToken = issuer.Mint(new CapabilityTokenOptions
{
Issuer = "agent://claims-orchestrator",
Audience = "tool://member-lookup",
Capability = new CapabilityClaim { Tool = "MemberLookup", Action = "GetProfile" },
Context = new CapabilityContext { CorrelationId = correlationId },
Lifetime = TimeSpan.FromSeconds(1) // Expires in 1 second
});
logger.LogInformation("Waiting 2 seconds for token to expire...");
await Task.Delay(TimeSpan.FromSeconds(2));
var expiredResult = await verifier2.VerifyAsync(shortToken.Token,
new CapabilityVerificationOptions
{
ExpectedAudience = "tool://member-lookup",
TrustedIssuers = new Dictionary<string, SecurityKey>
{
["agent://claims-orchestrator"] = agentPrivateKey
},
ClockSkewTolerance = TimeSpan.Zero // Strict for demo
});
logger.LogInformation("Expired token: valid={Valid}, error={Error}",
expiredResult.IsValid, expiredResult.ErrorCode);
logger.LogInformation("=== All scenarios complete ===");
Expected Console Output
info: === Claims Orchestrator Agent starting ===
info: --- Scenario 1: Agent calls MemberLookup.GetProfile ---
info: Policy rule 'Allow MemberLookup' matched: agent://claims-orchestrator -> MemberLookup.GetProfile = Allow
info: Policy: PERMIT (maxResults=50, lifetime=60s)
info: Minted capability token abc123 for MemberLookup.GetProfile -> tool://member-lookup, expires 2026-03-01T14:01:00Z
info: Minted token: id=abc123, expires=2026-03-01T14:01:00+00:00
info: Token attached to request: Authorization: SdJwt <842 chars>
info: Verified capability token abc123: MemberLookup.GetProfile from agent://claims-orchestrator
info: VERIFIED: tool=MemberLookup, action=GetProfile, limits.maxResults=50
info: AUDIT RECEIPT | Decision=Allow | Tool=MemberLookup | Action=GetProfile | TokenId=abc123 | CorrelationId=a1b2c3d4e5f6
info: --- Scenario 2: Replay prevention ---
info: First use: valid=True
info: Replay attempt: valid=False, error=token_replayed
info: --- Scenario 3: Policy denies AdminConsole access ---
info: Policy rule 'Block admin tools' matched: agent://claims-orchestrator -> AdminConsole.DeleteAll = Deny
info: AdminConsole decision: permitted=False, reason=Denied by rule: Block admin tools
info: --- Scenario 4: Token expiry containment ---
info: Waiting 2 seconds for token to expire...
info: Expired token: valid=False, error=token_expired
info: === All scenarios complete ===
What the PoC Proves
| Capability | Scenario | How It's Proven |
|---|---|---|
| SD-JWT capability token minting | 1 | CapabilityTokenIssuer.Mint() delegates to real SdIssuer.Issue() |
| Selective disclosure | 1 | resource, limits, workflow_id, step_id are disclosable; tool, action always visible |
| Cryptographic verification | 1 | CapabilityTokenVerifier delegates to real SdVerifier.VerifyAsync() |
| Audience enforcement | 1 | Token rejected if aud does not match tool identity |
| Per-action authorization | 1 | Tool checks capability.Tool + capability.Action match |
| Limits enforcement | 1 | Tool reads MaxResults from verified capability and applies to query |
| Policy-driven authorization | 1, 3 | DefaultPolicyEngine allows MemberLookup, denies AdminConsole |
| Replay prevention | 2 | MemoryNonceStore blocks duplicate jti on second use |
| Expiry-based containment | 4 | Token with 1s lifetime rejected after 2s delay |
| Audit trail with correlation | 1 | LoggingReceiptWriter emits structured receipt with CorrelationId |
| Zero changes to existing packages | All | Implementation uses SdIssuer/SdVerifier from existing SdJwt.Net as-is |
| Default-deny security | 3 | No matching rule results in automatic denial |
Running the PoC
Prerequisites
dotnet --version # Requires .NET 8.0+
Steps
# 1. Clone and navigate
cd sd-jwt-dotnet
# 2. Build core library (already exists)
dotnet build src/SdJwt.Net/SdJwt.Net.csproj
# 3. Build and run the agent console app
cd samples/AgentTrustKit.PoC/apps/ClaimsOrchestratorAgent
dotnet run
# 4. (Optional) Start the tool server in a separate terminal
cd samples/AgentTrustKit.PoC/apps/MemberLookupApi
dotnet run --urls "http://localhost:5100"
# 5. (Optional) Run end-to-end tests
cd samples/AgentTrustKit.PoC/tests/AgentTrust.E2E.Tests
dotnet test
Estimated Effort
| Component | Files | Est. Lines | Est. Time |
|---|---|---|---|
| AgentTrust.Core | 8 | ~450 | 3 days |
| AgentTrust.Policy | 3 | ~120 | 1 day |
| AgentTrust.AspNetCore | 3 | ~180 | 1.5 days |
| MemberLookupApi | 1 | ~80 | 0.5 days |
| ClaimsOrchestratorAgent | 1 | ~200 | 1 day |
| ClaimsOrchestrator.Maf | 1 | ~100 | 0.5 days |
| Tests | 3 | ~300 | 2 days |
| Total | 20 | ~1430 | 9.5 days |
Appendix: Optional MAF Variant (Requires AI Key)
For teams that want to test the Agent Trust Kit with the actual Microsoft Agent Framework (MAF) and a real LLM, an optional project ClaimsOrchestratorAgent.Maf can be added.
Requirements to run this variant:
Microsoft.Extensions.AI(RC)Microsoft.Extensions.AI.OpenAI- An OpenAI API Key (or Azure OpenAI setup)
Minimum Implementation: ClaimsOrchestratorAgent.Maf/Program.cs
This variant demonstrates how AgentTrustMiddleware intercepts real LLM tool calls.
using System.Security.Cryptography;
using AgentTrust.Core;
using AgentTrust.Maf; // MAF Integration Package
using AgentTrust.Policy;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using OpenAI;
// 1. Setup Keys and OpenAI (Requires real API key)
var openAiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
?? throw new InvalidOperationException("OPENAI_API_KEY required for MAF variant");
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var agentPrivateKey = new ECDsaSecurityKey(ecdsa) { KeyId = "orchestrator-key-1" };
var builder = Host.CreateApplicationBuilder(args);
// 2. Register Agent Trust Services
builder.Services.AddAgentTrust(options =>
{
options.KeyCustodyProviderType = typeof(InMemoryKeyCustodyProvider);
options.NonceStoreType = typeof(MemoryNonceStore);
options.ReceiptWriterType = typeof(LoggingReceiptWriter);
options.PolicyEngineType = typeof(DefaultPolicyEngine);
});
// Configure local policy (same as base PoC)
builder.Services.AddSingleton<IReadOnlyList<PolicyRule>>(new[]
{
new PolicyRule("Allow MemberLookup", "agent://claims-orchestrator", "MemberLookup", "*", PolicyEffect.Allow)
});
// 3. Build MAF Agent Pipeline
Console.WriteLine("Building MAF Pipeline...");
// Create the inner LLM client
IChatClient innerClient = new OpenAIClient(openAiKey).AsChatClient(modelId: "gpt-4o-mini");
// Add tools that the LLM can call
var memberLookupTool = AIFunctionFactory.Create(
async (string memberId) => {
// In reality, this makes an HTTP call to the MemberLookupApi with the attached SD-JWT
Console.WriteLine($"[TOOL] MemberLookup called for {memberId}");
return "{ \"Name\": \"Jane Doe\", \"Plan\": \"Gold\" }";
}, "MemberLookup.GetProfile", "Looks up a member profile by ID");
var chatOptions = new ChatOptions { Tools = new[] { memberLookupTool } };
// Wrap the LLM client with Agent Trust Middleware
// This middleware intercepts the LLM's request to call `MemberLookupTool`,
// evaluates policy, and mints the SD-JWT capability token BEFORE the tool executes.
var trustClient = new AgentTrustChatClientBuilder(innerClient)
.UseAgentTrust(options =>
{
options.AgentId = "agent://claims-orchestrator";
options.ToolAudienceMapping = new Dictionary<string, string>
{
["MemberLookup.GetProfile"] = "tool://member-lookup"
};
options.EmitReceipts = true;
})
.Build();
// 4. Run the Agent
Console.WriteLine("Agent ready. Sending prompt...");
var prompt = "Can you look up the profile for member M-12345?";
Console.WriteLine($"\nUser: {prompt}");
var response = await trustClient.CompleteAsync(prompt, chatOptions);
Console.WriteLine($"\nAgent: {response.Message.Text}");
What the MAF Variant Proves
- Seamless Integration: The LLM decides to call the tool, but the trust middleware automatically handles the security (minting SD-JWTs based on policy) transparently.
- AI-Driven Invocation: The input is natural language, proving the trust kit works identically when driven by an autonomous LLM vs deterministic code.