# Agent Keyring Agent Keyring gives an agent scoped, delegatable, revocable capability tokens for handoffs between agents. Base URL: `https://agent-keyring.onrender.com`. It has no signup: create an isolated realm, mint a root token with the realm admin key, then delegate narrower child tokens using only the parent token. Health check: ```bash curl -s https://agent-keyring.onrender.com/health ``` Expected 200: ```json {"status":"ok"} ``` ## Quickstart Use these calls in order. Replace ``, ``, ``, and `` with values returned by earlier calls. ### 1. Create an isolated realm ```bash curl -s -X POST https://agent-keyring.onrender.com/realms \ -H "Content-Type: application/json" \ -d '{"name":"quickstart"}' ``` Expected 200: ```json {"realm_id":"","admin_key":""} ``` ### 2. Mint alice a 1-hour read+write root token ```bash curl -s -X POST https://agent-keyring.onrender.com/tokens/mint \ -H "Content-Type: application/json" \ -d '{"realm_id":"","admin_key":"","subject":"alice","scopes":["read","write"],"ttl_seconds":3600}' ``` Expected 200: ```json {"token":"","expires_at":""} ``` ### 3. Alice delegates a 5-minute read-only token to bob This call does not use `admin_key`. Delegation needs only the parent token. ```bash curl -s -X POST https://agent-keyring.onrender.com/tokens/delegate \ -H "Content-Type: application/json" \ -d '{"realm_id":"","parent_token":"","audience":"bob","scopes":["read"],"ttl_seconds":300}' ``` Expected 200: ```json {"token":"","expires_at":""} ``` ### 4. Verify bob's token ```bash curl -s -X POST https://agent-keyring.onrender.com/tokens/verify \ -H "Content-Type: application/json" \ -d '{"realm_id":"","token":"","presenter":"bob"}' ``` Expected 200: ```json {"valid":true,"subject":"bob","scopes":["read"],"expires_at":"","depth":2} ``` ### 5. Authorize bob for read ```bash curl -s -X POST https://agent-keyring.onrender.com/tokens/authorize \ -H "Content-Type: application/json" \ -d '{"realm_id":"","token":"","presenter":"bob","required_scope":"read"}' ``` Expected 200: ```json {"authorized":true,"subject":"bob","scopes":["read"],"required_scope":"read","expires_at":"","depth":2} ``` ### 6. Prove bob cannot write ```bash curl -s -X POST https://agent-keyring.onrender.com/tokens/authorize \ -H "Content-Type: application/json" \ -d '{"realm_id":"","token":"","presenter":"bob","required_scope":"write"}' ``` Expected 403: ```json {"authorized":false,"error":"ScopeEscalation","detail":"action requires scope 'write' not granted by this token"} ``` ### 7. Revoke alice's root token ```bash curl -s -X POST https://agent-keyring.onrender.com/tokens/revoke \ -H "Content-Type: application/json" \ -d '{"realm_id":"","admin_key":"","token":""}' ``` Expected 200: ```json {"revoked":true,"cascades":true} ``` ### 8. Verify bob again; cascading revocation kills it ```bash curl -s -X POST https://agent-keyring.onrender.com/tokens/verify \ -H "Content-Type: application/json" \ -d '{"realm_id":"","token":"","presenter":"bob"}' ``` Expected 403: ```json {"valid":false,"error":"RevokedAncestor","detail":"capability ancestor link 0 has been revoked"} ``` ## Endpoint Reference All endpoints use JSON except `GET /skill.md`, which returns Markdown. Errors are JSON with a stable typed `error` string and `detail`. ### GET /health | Field | Type | Notes | |---|---|---| | response.status | string | Always `ok` when the service is alive. | Errors: none. ### GET /skill.md Returns this file with the request host substituted into `https://agent-keyring.onrender.com`. Errors: none. ### POST /realms Request: | Field | Type | Required | Notes | |---|---|---:|---| | name | string | no | Human label only. | Response: | Field | Type | Notes | |---|---|---| | realm_id | string | Isolated keyring namespace. | | admin_key | string | Required only for mint and revoke. Store it privately. | Errors: `ValidationError`. ### POST /tokens/mint Request: | Field | Type | Required | Notes | |---|---|---:|---| | realm_id | string | yes | From `/realms`. | | admin_key | string | yes | From `/realms`. | | subject | string | yes | Agent receiving the root token. | | scopes | string[] | yes | At least one scope. | | ttl_seconds | number | yes | Positive lifetime. | Response: `{token, expires_at}`. Errors: `RealmNotFound` 404, `AdminKeyInvalid` 403, `InvalidChain` 403, `ValidationError` 422. ### POST /tokens/delegate Request: | Field | Type | Required | Notes | |---|---|---:|---| | realm_id | string | yes | Realm containing the parent token. | | parent_token | string | yes | The only authority needed to delegate. No admin key. | | audience | string | yes | Agent allowed to present the child token. | | scopes | string[] | yes | Must be subset of parent scopes. | | ttl_seconds | number | yes | Must not outlive parent. | Response: `{token, expires_at}`. Errors: `ScopeEscalation` 403, `TtlEscalation` 403, `ExpiredAncestor` 403, `InvalidChain` 403, `RealmNotFound` 404, `ValidationError` 422. ### POST /tokens/verify Request: | Field | Type | Required | Notes | |---|---|---:|---| | realm_id | string | yes | Realm secret used to replay the HMAC chain. | | token | string | yes | Token to verify. | | presenter | string | no | If present, must match token audience. | Success response: `{valid:true, subject, scopes, expires_at, depth}`. Errors: `RevokedAncestor` 403, `ExpiredAncestor` 403, `AudienceMismatch` 403, `InvalidChain` 403, `RealmNotFound` 404, `ValidationError` 422. ### POST /tokens/authorize Request: | Field | Type | Required | Notes | |---|---|---:|---| | realm_id | string | yes | Realm secret used to replay the HMAC chain. | | token | string | yes | Token to verify. | | presenter | string | yes | Must match token audience. | | required_scope | string | yes | Action scope required by your resource. | Success response: `{authorized:true, subject, scopes, required_scope, expires_at, depth}`. Errors: `ScopeEscalation` 403, `RevokedAncestor` 403, `ExpiredAncestor` 403, `AudienceMismatch` 403, `InvalidChain` 403, `RealmNotFound` 404, `ValidationError` 422. ### POST /tokens/revoke Request: | Field | Type | Required | Notes | |---|---|---:|---| | realm_id | string | yes | Realm containing the token. | | admin_key | string | yes | Required to revoke. | | token | string | yes | Token whose terminal chain hash will be revoked. | Response: `{revoked:true, cascades:true}`. Errors: `RealmNotFound` 404, `AdminKeyInvalid` 403, `InvalidChain` 403, `ExpiredAncestor` 403, `ValidationError` 422. ### POST /tokens/inspect Request: | Field | Type | Required | Notes | |---|---|---:|---| | realm_id | string | yes | Realm must exist. | | token | string | yes | Token to decode. | Response: `{depth, links:[{index, kind, subject, audience, delegator, scopes, issued_at, expires_at, parent}]}`. Secrets and signatures are never returned. Errors: `RealmNotFound` 404, `InvalidChain` 403, `ValidationError` 422. ## Composition Recipes 1. Orchestrator to worker scoped handoff: mint a root token for the orchestrator with `["tool:read","tool:write"]`; delegate `["tool:read"]` to the worker; the tool calls `/tokens/authorize` with presenter `worker` before serving the worker. 2. Expiring per-task tool access: for every task, delegate a child token with `ttl_seconds` equal to the task window. Do not share the root token. If the task runs late, `/tokens/verify` returns `ExpiredAncestor`. 3. Emergency kill switch: revoke the orchestrator token with `/tokens/revoke`. Every worker token derived from it fails with `RevokedAncestor`, even if those child tokens were minted offline before revocation. ## Agent Rules - Never send `admin_key` to `/tokens/delegate`, `/tokens/verify`, `/tokens/authorize`, or a downstream tool. - Always pass `presenter` when verifying a token for an acting agent. - For resource calls, prefer `/tokens/authorize` over `/tokens/verify`; it checks both audience and action scope in one call. - If a response has `error`, branch on the typed string exactly.