Appearance
Per-Hop Agent Identity
Gatez implements per-hop agent identity via an RFC 8693 Security Token Service (STS) that mints short-lived, single-hop JWTs carrying a cryptographically verified chain of actors. This enables authorization on both the originating human AND the acting agent simultaneously, turning audit trails from descriptive into enforceable.
Why Per-Hop Agent Identity Matters
The Identity Gap
Traditional gateway architectures (including Gatez pre-ADR-008) answer "which tenant" but not "which agent is acting" or "on whose behalf" in a way that downstream hops can verify and authorize on live. Agent delegation lineage is reconstructed after the fact from audit logs, which means:
- Tool servers cannot verify the calling agent's identity
- Human approvals (HITL) cannot show the full verified chain
- Cross-agent delegation cannot be enforced cryptographically
- Blast radius controls cannot distinguish between direct calls and delegated calls
Regulated Workflows Demand Cryptographic Lineage
In healthcare (HIPAA), finance (SOC2), and government (FedRAMP) environments:
- Every agent action must trace back to an authenticated human
- Delegation chains must be tamper-proof, not log-stitched guesses
- Tool execution requires proof of both "who authorized this" (human) and "who is acting now" (agent)
- Cross-tenant agent calls must be cryptographically impossible
The Uber Architecture Precedent
Uber's "Solving the Agent Identity Crisis" (2026) describes an architecture that converges 1:1 with Gatez:
- AI Gateway (≈ Gatez L2) — LLM routing, PII redaction, token budgets
- MCP Gateway (≈ Gatez L3) — tool enforcement, HITL gates
- AI Agent Mesh (≈ Gatez A2A) — agent-to-agent delegation topology
- STS (the missing piece in Gatez) — per-hop token exchange carrying verified actor chains
Gatez now ships all four, adapted to multi-tenant on-prem environments.
Architecture Overview
Components
┌─────────────────────────────────────────────────────────────┐
│ Human User (Zitadel JWT) │
│ tenant_id: clinical │
│ sub: nurse@clinical │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Agent 1: Investigation Agent │
│ Calls: POST /sts/token │
│ subject_token: <Zitadel human JWT> │
│ actor_token: <agent's credential> │
│ audience: agent:oncall-agent │
│ ← Returns: STS hop token │
│ sub: agent:investigation-agent │
│ aud: agent:oncall-agent (single hop only) │
│ act_chain: [user:nurse@clinical, agent:investigation] │
└──────────────────────┬──────────────────────────────────────┘
│ A2A delegation + hop token
▼
┌─────────────────────────────────────────────────────────────┐
│ Agent 2: Oncall Agent │
│ Receives: A2A message + STS token from Agent 1 │
│ L3 verifies: aud == "agent:oncall-agent" ✓ │
│ L3 authorizes on BOTH: │
│ - originating_human: user:nurse@clinical │
│ - acting_agent: agent:investigation-agent │
│ Calls MCP tool: get_patient │
│ L3 checks: agent:investigation allowed? ✓ │
│ Calls: POST /sts/token (next hop) │
│ subject_token: <prior STS token> │
│ actor_token: <oncall-agent credential> │
│ audience: agent:orders-agent │
│ ← Returns: new hop token │
│ act_chain: [user:nurse@clinical, agent:investigation, │
│ agent:oncall-agent] │
└─────────────────────────────────────────────────────────────┘Data Flow
Hop 1 (Human → Agent 1): Agent calls STS with the human's Zitadel JWT. STS verifies via existing JWKS cache, seeds
act_chainwithuser:nurse@clinical, appendsagent:investigation-agent, mints hop token bound toaud: agent:oncall-agent.A2A Delegation: Agent 1 sends A2A message + hop token to Agent 2. L3 verifies the token's
audmatches the receiving agent, extracts the dual principal.Tool Execution: Agent 2 calls MCP tools. L3 authorizes based on BOTH the acting agent AND the originating human. HITL cards show the full verified chain.
Hop N (Agent → Agent): Agent 2 calls STS with the prior hop token as
subject_token. STS verifies its own signature, appends Agent 2 toact_chain, mints next hop token.
Key Properties
- Single-hop binding: Each token is valid for exactly one
aud(audience). Replay at a different hop fails signature verification. - Append-only chain: Every hop appends to
act_chain; STS rejects loops (ifaudalready appears in the chain). - Tenant isolation: STS refuses any exchange where
subject,caller, andaudiencedon't share the sametenant_id. - Short TTL: Tokens expire in 120 seconds, minimizing replay windows.
- Replay defense:
jti(JWT ID) is tracked in Redis for the token TTL; reuse is rejected.
The act_chain Model: Dual-Principal Authorization
Traditional systems authorize on a single identity: the caller. Gatez's agent identity system authorizes on two principals simultaneously:
- Originating Human (
act_chain[0]) — who started the chain, verified via Zitadel - Acting Agent (
claims.sub) — who is executing now, verified via Agent Registry
Why Dual Principals?
Scenario: A patient triage agent (Agent A) delegates to a scheduling agent (Agent B) to book an appointment.
- Traditional single-principal: The scheduling tool server sees only Agent B. It cannot verify that a human authorized this chain, or that Agent A (not Agent B) initiated the delegation.
- Gatez dual-principal: The scheduling tool server authorizes on:
originating_human: user:nurse@clinical— proves a real nurse started thisacting_agent: agent:investigation-agent— proves the investigation agent delegated (not the scheduling agent acting alone)
This blocks:
- Unauthorized agent self-delegation loops
- Tool calls by agents without human authorization
- Cross-tenant agent impersonation (STS guards
tenant_idequality)
Example act_chain Evolution
Hop 1 (Human → Investigation Agent):
json
{
"sub": "agent:investigation-agent",
"aud": "agent:oncall-agent",
"act_chain": [
{"id": "user:nurse@clinical", "type": "human", "ts": 1750000000},
{"id": "agent:investigation-agent", "type": "agent", "ts": 1750000005}
]
}Hop 2 (Investigation Agent → Oncall Agent):
json
{
"sub": "agent:oncall-agent",
"aud": "agent:orders-agent",
"act_chain": [
{"id": "user:nurse@clinical", "type": "human", "ts": 1750000000},
{"id": "agent:investigation-agent", "type": "agent", "ts": 1750000005},
{"id": "agent:oncall-agent", "type": "agent", "ts": 1750000010}
]
}The chain is append-only: each hop adds one entry. L3 extracts:
originating_human=act_chain[0].idacting_agent=claims.sub
The Agent Registry: Principals, Policy, and Projection
The Control Plane API maintains an Agent Registry (/api/agents) — the missing principal layer between the human (Zitadel) and the tool (MCP server).
AgentRegistration Schema
json
{
"agent_id": "agent:investigation-agent",
"tenant_id": "clinical",
"allowed_audiences": ["agent:oncall-agent", "agent:orders-agent"],
"allowed_tools": ["get_patient", "get_lab_results"],
"credential_ref": "service-user-investigation-client-id",
"description": "Patient investigation agent"
}| Field | Type | Description |
|---|---|---|
agent_id | string | Globally unique, prefixed form: agent:<name> |
tenant_id | string | Tenant scope (multi-tenancy invariant) |
allowed_audiences | array | Agent IDs this agent may delegate to (A2A policy) |
allowed_tools | array | MCP tools this agent may invoke |
credential_ref | string | P0: Zitadel service-user client_id; P1: mTLS cert ref; P2: SPIFFE SVID |
description | string | Human-readable description |
Delegation Policy Projection
Problem: Delegation policy exists in multiple places:
- L3 A2A runtime checks (
a2a.rs::check_delegation) - STS issue-time checks (when minting hop tokens)
- Agent Registry (
allowed_audiences)
Solution: The Agent Registry is the single source of truth. On every registration or update, the Control Plane API projects the delegation policy to Redis:
gatez:deleg:{agent_id} → SET of allowed audiencesBoth the STS (issue-time) and L3 (call-time) read this projection. This eliminates policy drift, which here is a security bug: allow-at-mint but deny-at-call (or vice versa).
Example Policy Projection
Registration:
bash
POST /api/agents
{
"agent_id": "agent:investigation-agent",
"allowed_audiences": ["agent:oncall-agent", "agent:orders-agent"]
}Redis State After Projection:
gatez:agent-registry → Hash
agent:investigation-agent → {"agent_id": "agent:investigation-agent", ...}
gatez:deleg:agent:investigation-agent → SET
agent:oncall-agent
agent:orders-agentSTS Mint-Time Check:
rust
// When minting a hop token for aud=agent:oncall-agent
SISMEMBER gatez:deleg:agent:investigation-agent agent:oncall-agent
→ 1 (allowed)L3 Call-Time Check (same projection):
rust
// When Agent 1 sends A2A message to Agent 2
SISMEMBER gatez:deleg:agent:investigation-agent agent:oncall-agent
→ 1 (allowed)Token Exchange Flow (RFC 8693)
Endpoint
POST /sts/token (Control Plane API, port 4001)
Request
json
{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "<Zitadel JWT (hop 1) or prior STS token (hop N)>",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"audience": "agent:oncall-agent",
"actor_token": "<calling agent's credential JWT>",
"scope": "get_patient"
}| Field | Required | Description |
|---|---|---|
grant_type | Yes | Must be urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | Yes | Hop 1: Zitadel human JWT; Hop N: prior STS token |
subject_token_type | No | Token type hint (optional) |
audience | Yes | The next hop agent ID (single hop only) |
actor_token | Yes | Proves the calling agent's identity (P0: Zitadel service-user JWT) |
scope | No | Optional intent → becomes purpose claim |
Response
json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"issued_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_type": "N_A",
"expires_in": 120
}| Field | Description |
|---|---|
access_token | The STS hop token (single-use, single-hop) |
issued_token_type | Always urn:ietf:params:oauth:token-type:jwt |
expires_in | TTL in seconds (120s) |
STS Processing Steps
Verify
subject_token:- Hop 1: Verify via Zitadel JWKS (reusing the existing dual-issuer JWKS cache)
- Hop N: Verify via STS's own signing key
- Extract
tenant_idandact_chain(or seed it on hop 1)
Resolve the calling agent from
actor_token:- P0: Decode the JWT, extract
agent_id/azp/sub - Lookup in Agent Registry (
gatez:agent-registryRedis hash) - Fail if not registered
- P0: Decode the JWT, extract
Resolve the audience agent:
- Lookup
audiencein Agent Registry - Fail if not registered
- Lookup
Tenant guard:
- Verify
subject.tenant_id == caller.tenant_id == audience.tenant_id - Reject cross-tenant exchange (multi-tenancy invariant)
- Verify
Delegation policy check:
- Check
SISMEMBER gatez:deleg:{caller.agent_id} {audience} - Fail if not allowed
- Check
Loop detection:
- Reject if
audiencealready appears inact_chain - Prevents circular delegation
- Reject if
Mint the hop token:
- Append caller to
act_chain - Set
sub = caller.agent_id,aud = audience - Set
jti = uuid,exp = now + 120s - Sign with STS signing key (P0: HS256 symmetric; P1: EdDSA asymmetric)
- Audit to ClickHouse (
gateway.sts_token_issuance)
- Append caller to
Verification at L3
When an A2A message or MCP tool call arrives at L3 with an sts_token, the Agent Gateway verifies and authorizes:
Verification Steps
- Decode and verify signature (P0: HS256; P1: EdDSA via STS JWKS endpoint)
- Check expiry (
expclaim) - Check issuer (
iss == "https://sts.gatez.internal") - Check audience (for A2A hops:
audmust match the receiving agent) - Replay guard:
SET NXonsts:jti:{jti}with TTL = remaining token lifetime - Extract dual principal:
originating_human = act_chain[0].idacting_agent = claims.sub
Authorization
L3 can now authorize on both principals:
- Acting agent: Check
acting_agentagainst tool allowlist - Originating human: Future ABAC policies (e.g., "only nurses may book appointments")
- Session budget: Deduct from the session's token budget
- Blast radius: Enforce max tool calls, max session duration per human+agent pair
Example Code Path (Rust)
rust
// L3 MCP tool call handler
let hop = sts_verify::verify_token(&req.sts_token, None, &signing_key)?;
sts_verify::check_replay(&mut redis, &hop.jti, 120).await?;
// Dual-principal authz
if !sts_verify::authorize_hop(&hop, &req.tool_name, &allowed_tools) {
return Err((StatusCode::FORBIDDEN, "agent not allowed to call this tool"));
}
// Audit with full chain
audit_tool_call(&hop.tenant_id, &hop.acting_agent, &hop.originating_human, &hop.act_chain, &req.tool_name).await;Loop Detection and Blast Radius Controls
Loop Detection
Attack: A malicious agent tries to delegate back into its own chain to create an infinite loop.
Defense: STS rejects any audience that already appears in act_chain.
Example:
act_chain: [user:nurse@clinical, agent:investigation, agent:oncall]
audience: agent:investigation ← REJECTED (loop detected)Blast Radius Controls
With verified identity chains, L3 can enforce limits per human+agent pair:
- Max delegation depth: Reject chains longer than 5 hops (configurable per tenant)
- Max tool calls per session: Prevent runaway agent loops
- Max token budget per human: Even if an agent has a large budget, the human's delegation is capped
- Time-based limits: Expire sessions after N minutes, even if tokens remain
These are policy enforcements, not just audit trails. The verified act_chain is the authorization input.
Phased Rollout (P0 → P1 → P2)
P0: Symmetric Signing + Zitadel Service Users (Shipped)
Signing:
- HS256 with
STS_SIGNING_KEY(shared secret) - Internal network only (STS endpoint not exposed externally)
- L3 uses the same key to verify
Agent Credentials:
- Each agent has a Zitadel service-user (machine-to-machine OAuth2 client)
credential_refin Agent Registry points to the client ID- Agent proves identity via Zitadel service-user JWT (
actor_token)
Feature Flag:
- Server:
GATEZ_AGENT_IDENTITY=ongates STS + registry endpoints - Frontend:
VITE_AGENT_IDENTITY=ongates operator UI
Limitations:
- Symmetric key rotation requires downtime
- No JWKS endpoint (L3 must know the same secret)
- Service-user credentials are not workload-attested
P1: EdDSA Asymmetric + JWKS Endpoint (In Progress)
Signing:
- EdDSA (Ed25519) asymmetric keypair
- STS signs with private key
- L3 verifies via public key fetched from STS JWKS endpoint
- Key rotation supported via
kid(key ID) in JWT header
JWKS Endpoint:
GET /sts/.well-known/jwks.json- Returns public keys for signature verification
- L3 caches JWKS and refreshes periodically
Agent Credentials:
- Still Zitadel service-user (P0 credential path), but mTLS support added
- Agent can present mTLS client cert as
actor_tokenproof
Benefits:
- Zero-downtime key rotation
- L3 doesn't need the signing secret
- JWKS-based verification is industry standard (OIDC, OAuth2)
P2: SPIFFE/SPIRE Workload Identity (Future)
Agent Credentials:
- Each agent workload gets a SPIFFE JWT-SVID from SPIRE
- SVIDs are cryptographically attested (workload must prove it's running the expected binary)
actor_tokenis the SPIFFE JWT-SVID
Benefits:
- Cryptographic proof the agent is the claimed workload
- No static credentials (SVIDs are short-lived, auto-rotated)
- Industry standard for zero-trust service mesh (Uber uses this)
Deployment:
- Requires SPIRE server + agent deployment
- Integrates with Kubernetes admission controllers for workload attestation
Observability and Auditing
ClickHouse: sts_token_issuance Table
Every token issuance (success or denial) is logged:
sql
CREATE TABLE IF NOT EXISTS gateway.sts_token_issuance (
timestamp DateTime64(3) DEFAULT now64(3),
tenant_id String,
jti String,
subject String,
audience String,
human String,
chain_depth UInt8,
act_chain String,
purpose String,
decision String,
deny_reason String,
ttl_secs UInt32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (tenant_id, timestamp);| Column | Description |
|---|---|
tenant_id | Tenant scope |
jti | JWT ID (unique token identifier) |
subject | Acting agent (claims.sub) |
audience | Next hop agent (claims.aud) |
human | Originating human (act_chain[0].id) |
chain_depth | Length of act_chain |
act_chain | Full chain as JSON |
purpose | Optional intent (scope from request) |
decision | ISSUED or DENIED |
deny_reason | Reason for denial (empty if ISSUED) |
ttl_secs | Token TTL (120s) |
Query Examples:
sql
-- All issuances for a tenant in the last hour
SELECT * FROM gateway.sts_token_issuance
WHERE tenant_id = 'clinical'
AND timestamp >= now() - INTERVAL 1 HOUR
ORDER BY timestamp DESC;
-- Denied exchanges (security events)
SELECT timestamp, tenant_id, subject, audience, deny_reason
FROM gateway.sts_token_issuance
WHERE decision = 'DENIED'
ORDER BY timestamp DESC
LIMIT 100;
-- Deepest delegation chains (detect complex agent workflows)
SELECT tenant_id, human, subject, audience, chain_depth
FROM gateway.sts_token_issuance
WHERE chain_depth > 3
ORDER BY chain_depth DESC;Redis: Replay Denylist
Every jti is tracked in Redis for the token TTL:
sts:jti:{jti} → "1" (TTL = 120s)L3 checks SET NX before accepting a token. Replay attempts fail.
Traces
Every STS issuance and L3 verification emits OpenTelemetry spans:
sts.token_exchange(Control Plane API)agent_gateway.verify_sts_token(L3)agent_gateway.check_replay(L3)
Jaeger traces link the hop token issuance to the downstream tool call, forming a complete cross-layer trace.
Security Considerations
Threat Model
Threat: Stolen hop token replayed at a different hop.
Mitigation: Single-hop binding (aud claim). Token valid only for the specified audience. Verification fails if aud doesn't match.
Threat: Stolen hop token replayed before expiry.
Mitigation: jti replay denylist in Redis. L3 checks SET NX; reuse is rejected.
Threat: Agent delegates to an unauthorized audience.
Mitigation: Delegation policy projection to Redis. STS (issue-time) and L3 (call-time) both check gatez:deleg:{agent_id}. Policy drift is impossible — they read the same source.
Threat: Cross-tenant agent impersonation.
Mitigation: STS tenant guard. Rejects any exchange where subject.tenant_id != caller.tenant_id != audience.tenant_id.
Threat: Delegation loop (circular agent chain).
Mitigation: Loop detection. STS rejects if audience already appears in act_chain.
Threat: Symmetric key compromise (P0).
Mitigation: P1 upgrades to EdDSA asymmetric. L3 verifies via JWKS endpoint, never sees the private key.
Threat: Actor token forgery.
Mitigation: P0: Zitadel service-user JWT (verified via Zitadel JWKS). P1: mTLS client cert. P2: SPIFFE JWT-SVID (cryptographically attested).
Comparison: Gatez vs Competitors
| Feature | Gatez | Uber (internal) | AWS AgentCore | Kong Konnect |
|---|---|---|---|---|
| STS Token Exchange | ✅ RFC 8693 | ✅ (internal) | ❌ | ❌ |
| Dual-Principal Authz | ✅ human + agent | ✅ | ❌ | ❌ |
| Multi-Tenancy | ✅ (enforced at STS) | ❌ (single org) | ✅ (accounts) | ✅ (orgs) |
| On-Prem Deployment | ✅ | ✅ | ❌ (cloud only) | ❌ (SaaS primary) |
| Verified act_chain | ✅ | ✅ | ❌ | ❌ |
| Delegation Policy | ✅ (projected to Redis) | ✅ | ❌ | ❌ |
| Loop Detection | ✅ | ✅ | ❌ | ❌ |
| Replay Defense | ✅ (jti denylist) | ✅ | ❌ | ❌ |
| JWKS Endpoint | 🚧 P1 (in progress) | ✅ | ❌ | ❌ |
| SPIFFE/SPIRE | 🚧 P2 (planned) | ✅ | ❌ | ❌ |
Gatez is the only open-source, on-prem, multi-tenant gateway with per-hop agent identity.
Use Cases
Healthcare (HIPAA Compliance)
Scenario: A clinical triage agent delegates to a scheduling agent to book a patient appointment.
Without STS:
- Scheduling tool server sees only "some agent" calling it
- Cannot verify a nurse authorized this chain
- Audit trail is log-stitched, not cryptographically verified
With STS:
- Scheduling tool receives
sts_tokenwithact_chain: [user:nurse@clinical, agent:triage, agent:scheduling] - Tool server verifies the token signature (proves Gatez STS issued it)
- HITL approval card shows "Nurse Sarah (verified) via Triage Agent"
- Audit log records
originating_human: user:sarah@clinical(tamper-proof)
Government (FedRAMP)
Scenario: A classified document-processing agent must never delegate to agents outside its approval chain.
Without STS:
- Agent self-delegation is logged but not enforced
- Circular delegations can loop indefinitely
With STS:
- Agent Registry defines
allowed_audiences: ["agent:redaction", "agent:archive"] - STS rejects delegation to any other agent at mint-time
- L3 enforces the same policy at call-time (policy projection guarantees consistency)
- Loop detection prevents circular chains
Finance (SOC2)
Scenario: A trading agent delegates to a risk-assessment agent before executing a trade.
Without STS:
- Cannot prove the trade was authorized by a human trader
- Blast radius: a runaway agent can delegate indefinitely
With STS:
act_chaincryptographically provesuser:trader@hedge-fundauthorized the chain- Max delegation depth enforced (e.g., 3 hops max)
- Token budget deducted per-human, not just per-agent
- Audit shows full verified chain:
trader → trading-agent → risk-agent → execution-agent
Configuration Reference
Environment Variables
| Variable | Default | Description |
|---|---|---|
GATEZ_AGENT_IDENTITY | off | Feature flag: on enables STS + registry endpoints; off returns 404 |
STS_ED25519_PRIVATE_DER_B64 | (auto-gen in local) | P1 (current): EdDSA signing keypair, PKCS#8 v2 DER, base64. Public key published at GET /sts/jwks. Auto-generated ephemerally when GATEZ_ENV=local |
STS_JWKS_URL | (unset) | On the agent gateway: STS JWKS endpoint to verify hop tokens (e.g. http://control-plane-api:4001/sts/jwks). When set, L3 uses EdDSA; when unset, falls back to the HS256 shared secret |
STS_SIGNING_KEY | (legacy) | P0 (legacy fallback): HS256 symmetric secret, shared by CP-API and L3. Used only when STS_JWKS_URL is unset |
STS_ISSUER | https://sts.gatez.internal | Issuer claim in minted tokens |
STS_TOKEN_TTL_SECS | 120 | Hop token TTL in seconds |
Redis Keys
| Key Pattern | Type | Description |
|---|---|---|
gatez:agent-registry | Hash | Agent registrations (field = agent_id, value = JSON) |
gatez:deleg:{agent_id} | Set | Delegation allowlist (projected from Agent Registry) |
sts:jti:{jti} | String | Replay denylist (TTL = token TTL) |
ClickHouse Table
See gateway.sts_token_issuance schema in the ClickHouse Schema Reference.
Next Steps
Enable the feature:
bashexport GATEZ_AGENT_IDENTITY=on # EdDSA + JWKS (recommended): L3 verifies against the published public key export STS_JWKS_URL=http://control-plane-api:4001/sts/jwks # on the agent gateway # CP-API auto-generates an ephemeral Ed25519 key when GATEZ_ENV=local, or set # STS_ED25519_PRIVATE_DER_B64 to a fixed PKCS#8 v2 DER keypair (base64). # # Legacy HS256 fallback (leave STS_JWKS_URL unset): # export STS_SIGNING_KEY=your-secure-key-change-meRegister an agent:
bashcurl -X POST http://localhost:4001/api/agents \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "agent:investigation-agent", "tenant_id": "clinical", "allowed_audiences": ["agent:oncall-agent"], "allowed_tools": ["get_patient"], "credential_ref": "service-user-investigation-client-id" }'Mint a hop token (agent workflow):
bashcurl -X POST http://localhost:4001/sts/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "$HUMAN_JWT", "actor_token": "$AGENT_JWT", "audience": "agent:oncall-agent", "scope": "get_patient" }'Verify in L3: Attach the
sts_tokento A2A messages or MCP tool calls. L3 verifies and authorizes on both the human and the agent.Audit: Query
gateway.sts_token_issuancein ClickHouse to see all token issuance events.
References
- ADR-008: Per-Hop Agent Identity via STS Token Exchange
- RFC 8693: OAuth 2.0 Token Exchange
- IETF WIMSE: Web and Internet Messaging, Sync, and Extensions (working group)
- Uber Blog: "Solving the Agent Identity Crisis" (2026)
- SPIFFE: Secure Production Identity Framework For Everyone