Skip to content

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

  1. Hop 1 (Human → Agent 1): Agent calls STS with the human's Zitadel JWT. STS verifies via existing JWKS cache, seeds act_chain with user:nurse@clinical, appends agent:investigation-agent, mints hop token bound to aud: agent:oncall-agent.

  2. A2A Delegation: Agent 1 sends A2A message + hop token to Agent 2. L3 verifies the token's aud matches the receiving agent, extracts the dual principal.

  3. 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.

  4. Hop N (Agent → Agent): Agent 2 calls STS with the prior hop token as subject_token. STS verifies its own signature, appends Agent 2 to act_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 (if aud already appears in the chain).
  • Tenant isolation: STS refuses any exchange where subject, caller, and audience don't share the same tenant_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:

  1. Originating Human (act_chain[0]) — who started the chain, verified via Zitadel
  2. 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 this
    • acting_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_id equality)

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].id
  • acting_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"
}
FieldTypeDescription
agent_idstringGlobally unique, prefixed form: agent:<name>
tenant_idstringTenant scope (multi-tenancy invariant)
allowed_audiencesarrayAgent IDs this agent may delegate to (A2A policy)
allowed_toolsarrayMCP tools this agent may invoke
credential_refstringP0: Zitadel service-user client_id; P1: mTLS cert ref; P2: SPIFFE SVID
descriptionstringHuman-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 audiences

Both 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-agent

STS 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"
}
FieldRequiredDescription
grant_typeYesMust be urn:ietf:params:oauth:grant-type:token-exchange
subject_tokenYesHop 1: Zitadel human JWT; Hop N: prior STS token
subject_token_typeNoToken type hint (optional)
audienceYesThe next hop agent ID (single hop only)
actor_tokenYesProves the calling agent's identity (P0: Zitadel service-user JWT)
scopeNoOptional 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
}
FieldDescription
access_tokenThe STS hop token (single-use, single-hop)
issued_token_typeAlways urn:ietf:params:oauth:token-type:jwt
expires_inTTL in seconds (120s)

STS Processing Steps

  1. 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_id and act_chain (or seed it on hop 1)
  2. Resolve the calling agent from actor_token:

    • P0: Decode the JWT, extract agent_id / azp / sub
    • Lookup in Agent Registry (gatez:agent-registry Redis hash)
    • Fail if not registered
  3. Resolve the audience agent:

    • Lookup audience in Agent Registry
    • Fail if not registered
  4. Tenant guard:

    • Verify subject.tenant_id == caller.tenant_id == audience.tenant_id
    • Reject cross-tenant exchange (multi-tenancy invariant)
  5. Delegation policy check:

    • Check SISMEMBER gatez:deleg:{caller.agent_id} {audience}
    • Fail if not allowed
  6. Loop detection:

    • Reject if audience already appears in act_chain
    • Prevents circular delegation
  7. 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)

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

  1. Decode and verify signature (P0: HS256; P1: EdDSA via STS JWKS endpoint)
  2. Check expiry (exp claim)
  3. Check issuer (iss == "https://sts.gatez.internal")
  4. Check audience (for A2A hops: aud must match the receiving agent)
  5. Replay guard: SET NX on sts:jti:{jti} with TTL = remaining token lifetime
  6. Extract dual principal:
    • originating_human = act_chain[0].id
    • acting_agent = claims.sub

Authorization

L3 can now authorize on both principals:

  • Acting agent: Check acting_agent against 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_ref in Agent Registry points to the client ID
  • Agent proves identity via Zitadel service-user JWT (actor_token)

Feature Flag:

  • Server: GATEZ_AGENT_IDENTITY=on gates STS + registry endpoints
  • Frontend: VITE_AGENT_IDENTITY=on gates 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_token proof

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_token is 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);
ColumnDescription
tenant_idTenant scope
jtiJWT ID (unique token identifier)
subjectActing agent (claims.sub)
audienceNext hop agent (claims.aud)
humanOriginating human (act_chain[0].id)
chain_depthLength of act_chain
act_chainFull chain as JSON
purposeOptional intent (scope from request)
decisionISSUED or DENIED
deny_reasonReason for denial (empty if ISSUED)
ttl_secsToken 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

FeatureGatezUber (internal)AWS AgentCoreKong 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_token with act_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_chain cryptographically proves user:trader@hedge-fund authorized 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

VariableDefaultDescription
GATEZ_AGENT_IDENTITYoffFeature 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_ISSUERhttps://sts.gatez.internalIssuer claim in minted tokens
STS_TOKEN_TTL_SECS120Hop token TTL in seconds

Redis Keys

Key PatternTypeDescription
gatez:agent-registryHashAgent registrations (field = agent_id, value = JSON)
gatez:deleg:{agent_id}SetDelegation allowlist (projected from Agent Registry)
sts:jti:{jti}StringReplay denylist (TTL = token TTL)

ClickHouse Table

See gateway.sts_token_issuance schema in the ClickHouse Schema Reference.


Next Steps

  1. Enable the feature:

    bash
    export 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-me
  2. Register an agent:

    bash
    curl -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"
      }'
  3. Mint a hop token (agent workflow):

    bash
    curl -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"
      }'
  4. Verify in L3: Attach the sts_token to A2A messages or MCP tool calls. L3 verifies and authorizes on both the human and the agent.

  5. Audit: Query gateway.sts_token_issuance in 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

Enterprise API + AI + Agent Gateway