How agent identity works
The agent record, the token lifecycle from mint to revocation, and why an agent token, a user session, and an org API key can never stand in for each other.
The agent record
Registering an agent creates one row, scoped to your organisation. Every field below is returned by GET /agents/{agent_id}:
| Field | Meaning |
|---|---|
id | UUID, generated at registration. The token's sub claim and the CIMD URL both use it. |
name | Human-readable agent name. |
owner | The person or team responsible — who gets the call when this agent misbehaves. |
environment | One of production / staging / dev. Decides the initial status: dev and staging start active; production starts as a draft that needs a second person's approval (layer 2). |
risk_level | low / medium / high — drives the layer-2 review cadence. |
allowed_mcp / denied_mcp | The declared MCP-server scope. Baked into the token as a claim; also read live by the policy engine. |
status | draft → approved → active → suspended → decommissioned. Changed only via the lifecycle endpoints, never via PATCH. |
created_by / approved_by | Creator and approver on record. The API refuses self-approval — they are always different people. |
org_id | The owning organisation. Every read and write is filtered by it; other tenants get a 404, not a 403. |
created_at / updated_at | Set by the database; updated_at advances on every change. |
expires_at | Optional hard end date for the agent (NULL = none). |
last_review / next_review | Review bookkeeping; next_review advances by the risk cadence when a review is recorded. |
suspended_at / suspend_reason | Set on suspension; the reason is mandatory and lands in the audit trail. |
Token lifecycle
The agent token is a self-contained HS256 JWT — decoding it yields identity and scope with no callback. It is minted inside the registration transaction and returned once, in the token field of the response. The server keeps only two derivatives: a bcrypt hash (proof of what was issued) and a SHA-256 index (a searchable fingerprint). The plaintext is never stored and can never be shown again — losing it means rotating it.
register ──▶ mint JWT (sub, scope, jti) ──▶ shown ONCE in the response
│
└─▶ stored: bcrypt hash + SHA-256 index (never plaintext)
every proxy call: verify signature + issuer ──▶ look up SHA-256 index
│
rotate / decommission ─▶ revoked_at = NOW() ──────┘ revoked? → 401
(cached ≤ ~5 s)Verification. On every /proxy/… call the proxy verifies the JWT signature and issuer, then checks revocation: the exact token is looked up by its SHA-256 in agent_tokens, and it is rejected with 401 revoked agent token once every row bearing that fingerprint is revoked. The result is cached for proxy_identity_ttl_seconds (default 5), so a just-revoked token survives at most that window.
Revocation. Rotation and decommission both set revoked_at on every live token row for the agent. Rotation then mints a replacement; decommission does not. Each issuance carries a jti nonce — without it, two mints in the same second would be byte-identical, and rotating would revoke the very token it hands back.
Pre-006 tokens. Tokens issued before the revocation index existed have no stored SHA-256. For those the proxy falls back to “does this agent still have any live token row?” — so they keep working until fully revoked, and become strictly per-token at their next rotation.
Three credentials, never interchangeable
| Credential | Who presents it | Hard boundary |
|---|---|---|
| Agent token | AI agents, at the proxy | A JWT with typ: "user" is rejected as an agent identity — a human session can never impersonate an agent. |
| User session JWT | Humans, in the dashboard | Carries typ: "user" and a 12-hour expiry; only valid for the management API, never the proxy. |
snops_key_… | Scripts, CI, the SDKs | Org-scoped, stored as a SHA-256 index, compared in constant time. Resolves to the admin role — treat it like a root credential. |
proxy_identity_ttl_seconds (default 5 s). And identity is org-scoped end to end: agents, tokens, and audit events all carry org_id, so no query crosses a tenant boundary.What an active identity is allowed to do — approvals, suspension, policy — is the next layer: Lifecycle & policy.