{"protocol":"clank-doc/1","frameworkVersion":"0.13.0","slug":"agent-access","title":"Agent access inbox and scoped grants","description":"Every authenticated Clank backend includes an application local agent access inbox:","group":{"id":"agents","title":"Agents and generation"},"url":"https://docs.clank.run/docs/agent-access","source":"docs/agent-access.md","headings":["What a grant represents","Human workflow","Agent readable grant API","Discovery","Configure capacity","Storage and isolation"],"tableOfContents":[{"id":"what-a-grant-represents","title":"What a grant represents","level":2},{"id":"human-workflow","title":"Human workflow","level":2},{"id":"agent-readable-grant-api","title":"Agent-readable grant API","level":2},{"id":"discovery","title":"Discovery","level":2},{"id":"configure-capacity","title":"Configure capacity","level":2},{"id":"storage-and-isolation","title":"Storage and isolation","level":2}],"markdown":"# Agent access inbox and scoped grants\n\nEvery authenticated Clank backend includes an application-local agent access inbox:\n\n```text\nhttps://your-app.example/__clank/oauth/access\n```\n\nIt shows the MCP clients currently authorized to act as the signed-in application user. A user can\nreduce a read/write grant to read-only or revoke it completely. The page is server rendered, works\nwithout client JavaScript, and is part of the application—not the Clank deployment control plane.\n\n## What a grant represents\n\nAn agent grant is one OAuth refresh-token family created after a user approves an MCP client. It is\nbound to all of the following:\n\n- this application and its exact MCP resource URL;\n- one application user;\n- one dynamically registered OAuth client;\n- `agent:read` or `agent:read agent:write` scopes; and\n- the configured access and refresh expiration windows.\n\nThe inbox never stores or displays raw access tokens, refresh tokens, authorization codes, PKCE\nverifiers, session cookies, redirect URIs, or consent proofs. Grant identifiers and client IDs are\nopaque management identifiers, not bearer credentials.\n\nAn access token is still short lived, and the rotating refresh family is bounded by the existing\nOAuth lifetime. By default one user can retain at most 100 active grants in one app. The limit is\nadmission control: when it is reached, a new authorization code remains unused and can be retried\nafter the user revokes an old grant.\n\n## Human workflow\n\nThe normal OAuth consent screen links to **Review existing agent access**. A signed-in user can also\nopen the inbox directly. Each grant shows:\n\n- the registered client name and public client ID;\n- whether it can read only or read and write;\n- when it was created and last used; and\n- when the active token family expires.\n\n**Make read-only** removes `agent:write` from every live token in the family. **Revoke** consumes\nthe complete family. Both operations require the user's browser session and its CSRF proof.\n\nChanges apply on the next MCP HTTP request because Clank authenticates the bearer and reads its\ncurrent scopes for every request, including requests carrying an existing MCP session ID. Reducing\na grant therefore immediately hides mutation tools and rejects mutation calls. Revocation rejects\nexisting access tokens and prevents another refresh.\n\nA grant cannot be expanded from the inbox. To restore write access, the MCP client must start a new\nOAuth flow and the user must approve `agent:write` on a fresh consent screen.\n\n## Agent-readable grant API\n\nSame-origin application UI and account tooling can read the no-store JSON contract:\n\n```http\nGET /__clank/oauth/grants\nCookie: __Host-clank-id=...\n```\n\n```json\n{\n  \"protocol\": \"clank-agent-grants/1\",\n  \"grants\": [\n    {\n      \"id\": \"clank_grant_AbCdEfGhIjKlMnOpQrStUvWx\",\n      \"clientId\": \"clank_client_ZyXwVuTsRqPoNmLkJiHgFeDc\",\n      \"clientName\": \"Codex\",\n      \"scopes\": [\"agent:read\", \"agent:write\"],\n      \"createdAt\": 1785513600000,\n      \"lastUsedAt\": 1785517200000,\n      \"expiresAt\": 1788105600000\n    }\n  ],\n  \"hasMore\": false,\n  \"managementPath\": \"/__clank/oauth/access\",\n  \"grantsPath\": \"/__clank/oauth/grants\"\n}\n```\n\nThe response contains at most the 100 most recent active grants. `hasMore` explicitly reports a\ntruncated view. It is partitioned by the current application user; a valid grant ID owned by\nanother user is indistinguishable from a missing ID.\n\nReduce a grant by sending the only permitted scope set:\n\n```http\nPATCH /__clank/oauth/grants/<grant-id>\nContent-Type: application/json\nX-Clank-CSRF: <current browser CSRF token>\n\n{\"scopes\":[\"agent:read\"]}\n```\n\nRevoke it with:\n\n```http\nDELETE /__clank/oauth/grants/<grant-id>\nX-Clank-CSRF: <current browser CSRF token>\n```\n\nMutation responses return the updated `clank-agent-grants/1` list plus an `updated` record. The API\ndoes not enable CORS and does not accept an MCP bearer as browser authority. Missing sessions,\ninvalid CSRF proofs, malformed JSON, scope expansion, cross-user IDs, and unsupported methods fail\nclosed with bounded structured errors.\n\n## Discovery\n\nClank publishes the inbox through its own discovery extensions:\n\n- `/.well-known/clank` → `mcp.accessManagement`;\n- OAuth protected-resource metadata → `clank_agent_access_url`; and\n- OAuth authorization-server metadata → `clank_agent_access_url` and\n  `clank_agent_grants_endpoint`.\n\nStandard OAuth and MCP fields are unchanged. Clients that do not understand these Clank extension\nfields can ignore them.\n\n## Configure capacity\n\nThe secure default needs no application code. To set a smaller per-user ceiling:\n\n```ts\nconst runtime = await openBackend(backend, {\n  path: \"app.sqlite\",\n  agent: {\n    title: \"Customer workspace\",\n    maxUserGrants: 20,\n  },\n});\n```\n\n`maxUserGrants` must be an integer from 1 through 1,000. Lowering the configured ceiling never\nsilently revokes existing grants; it prevents additional token families until usage falls below\nthe ceiling. An authorization code rejected at capacity can be retried after revocation only while\nthat short-lived code remains valid.\n\n## Storage and isolation\n\nGrant state lives in the same private SQLite database as that app's users and OAuth tables. Every\ndeployed app therefore has a separate inbox and grant namespace. Deleting a user cascades their\ncodes and token families; disabling a user invalidates authentication; deleting the app removes\nthe database under the normal deployment retention and deletion contract.\n\nThe grant API changes authorization state but not application documents, so it does not increment\nthe app's live data revision. Connected MCP requests still observe the changed scope immediately\nbecause token authentication does not use the application query cache.\n\nContinue with [Agent protocol](agent-protocol.md) for OAuth transport details and [The MCP server\nbuilt into every app](per-app-mcp.md) for UI-to-tool parity and contract freshness.\n"}