Identity and delegation#
An agent should never hold a long-lived credential, and every call it makes should say which agent, acting for which person, with which permissions. MAQPNA gives each session a session identity issued by the identity broker, and uses it on every governed call.
| Term | Meaning |
|---|---|
| identity broker | maqpna-identity: issues session tokens, publishes its public keys (JWKS), exchanges and delegates tokens |
| session identity | The signed identity of one session: agent, tier, namespace, the person it acts for, scopes |
| session token | The bearer form of the session identity: a compact JWS signed with Ed25519 (alg: EdDSA) |
| access token | An admin-API token for a person (OIDC) or the static break-glass token; never given to agents |
| identity provider | Your OIDC single sign-on system (Keycloak, Entra ID, Okta) |
What a session token says#
{
"iss": "maqpna-identity",
"sub": "spiffe://acme.eu/ns/team-a/agent/coder/session/fix-4821",
"aud": "maqpna-gateway",
"iat": 1791024000, "nbf": 1791024000, "exp": 1791024900,
"jti": "5f0c2e9a7b1d4c3e8a6f2b0d9c1e7a54",
"act": { "sub": "user:alice@acme.eu" },
"scope": "tools:github models:team-a.coder",
"maqpna_ns": "team-a", "maqpna_agent": "coder", "maqpna_session": "fix-4821", "maqpna_tier": "tier-1-microvm",
"maqpna_groups": "eng oncall",
"maqpna_principal_verified": "requester"
}
subis a SPIFFE-style workload ID: trust domain, namespace, agent and session. The gateway checks that it agrees with themaqpna_*claims.act.subis the person the agent acts for (on-behalf-of, RFC 8693 style).maqpna_groupsholds their verified groups (at most 32), andmaqpna_principal_verifiedsays how the person was bound:requester(the creator of the session, checked by an admission policy),trustedCreator(a trusted portal created it) oridToken(an OIDC ID token was verified). Absent means unverified.scopeis space-separated:tools:<server>,models:<route>,agents:<name>. A session's scopes must be a subset of its agent's.- Lifetime. 15 minutes by default, never more than the broker's maximum (1 hour) or the session's remaining TTL. The operator re-mints the token at 80% of its lifetime; for warm-pool and attested sessions the pod refreshes it itself.
Least privilege, by construction#
- No static secrets in sandboxes. The token is mounted read-only, expires in minutes and is useless outside its scopes. Upstream credentials (API keys, OAuth client secrets, mTLS keys) live only in the gateway; the agent never sees them.
- Derived tokens only narrow. Token exchange (
POST /v1/exchange, RFC 8693) issues a shorter-lived token for an allow-listed audience, with scopes provably narrower than the original. An exchanged token cannot be exchanged again. - No token passthrough. The gateway strips the agent's
Authorizationheader before forwarding. The upstream receivesX-Maqpna-Namespace,X-Maqpna-Agent,X-Maqpna-Session,X-Maqpna-User,X-Maqpna-SubjectandX-Maqpna-Groupsfrom the verified claims, or a short-lived transaction token (Txn-Token, 60 seconds by default), and its own credential. - Per-user credentials stay in a vault. When an MCP server needs the user's own OAuth token (GitHub, Microsoft 365), the gateway injects it from its encrypted token vault after the user connected the account once; the agent never holds it.
- Optional workload identity (SPIFFE). With SPIRE, the sandbox also presents an X.509-SVID whose ID must equal the token subject (
tls.svidMode: optional | required).
Delegation between agents#
When agent A calls agent B over agent-to-agent (A2A), B may need to act for the same person. B exchanges the call's transaction token (or A's token) together with its own session token at the broker and receives a delegation token whose act claim nests the chain, outermost first:
"act": { "sub": "spiffe://acme.eu/ns/team-a/agent/planner/session/p-17",
"act": { "sub": "user:alice@acme.eu" } }
- The chain is capped at 5 hops (
-max-delegation-depth); loops back into an agent of the chain are refused (delegation_loop). - Scopes are narrowed to B's, expiry is at most A's and B's, and the original person's groups and verification carry through.
- Policies keep matching the human: the gateway walks the chain to the
user:entry. Revoking any agent of the chain, or the delegator's token ID, revokes the delegated token too.
sequenceDiagram
participant A as Agent A (planner)
participant GW as Gateway
participant B as Agent B (coder)
participant IB as Identity broker
A->>GW: A2A SendMessage to team-a/coder<br/>(A's session token, scope agents:coder)
GW->>GW: verify, revocation for A and its chain,<br/>allowedCallers, depth, policy, DLP
GW->>B: forward + Txn-Token (tctx.target team-a/coder)
B->>IB: POST /v1/exchange<br/>subject_token = Txn-Token, actor_token = B's token
IB-->>B: delegation token: sub = B, act = {A, act: {user:alice}}
B->>GW: tools/call with the delegation token
GW->>GW: policy principals match user alice,<br/>ledger ext.actChain
What you see#
maqpna token inspect decodes a session token and shows its claims; maqpna token verify checks it against the broker's keys; maqpna whoami shows the identity and roles the gateway sees for your admin access token. See maqpna token inspect, maqpna token verify and maqpna whoami.