Authentication and grants
The platform has three access tiers: full access (owner/admin), read-only scoped tokens, and agent tokens — the tier this page is about. An agent token is a JWT giving broad read access (secrets always redacted) and write access only to an explicitly granted pool of contracts.
How an agent token is issued
Section titled “How an agent token is issued”Only a superuser (the owner) can mint agent tokens — POST /api/v1/auth/agent-token rejects everyone else with 403. An agent can
never mint or extend its own token. The owner’s request looks like:
{ "email": "my-agent@example.dev", "expires_days": 90, "revoke_prior": true, "grants": [ { "salescontract": 12345, "capabilities": ["submit", "launch", "cancel", "edit_config"] } ]}grantsis required and non-empty — a token with no pool is refused (400).expires_days(1–365, default 90) sets the JWT’s own lifetime.revoke_prior: true(the default) immediately invalidates all previously issued tokens for that agent identity.
The response contains the access_token (send it as
Authorization: Bearer <token>), its lifetime in seconds, and the agent’s
user id.
The grant pool
Section titled “The grant pool”Write permissions live in one table: one row per (agent, contract) with a
capability list — a subset of submit, launch, cancel, edit_config,
publish. Default deny: no row means no access, and publish is
withheld by default on production contracts — it must be granted explicitly.
Task/defect management is a separate global capability (manage_task),
not tied to any contract.
⚠️ The token-replaces-pool gotcha
Section titled “⚠️ The token-replaces-pool gotcha”Minting a token deletes the agent’s entire grant pool and inserts exactly what the request listed. Re-issuing a token “to add one contract” while listing only that contract silently drops every other grant the agent had. Two safe ways to extend a pool:
- List the full pool in the mint request: fetch the current grants first, then pass old + new together.
- Additive grant without re-minting: the sanctioned path is inserting the single new grant row directly (owner operation) — the existing token picks it up immediately, nothing else changes.
After any re-mint, an agent should re-check my_capabilities before acting.
If it fails
Section titled “If it fails”| Response | Cause | Action |
|---|---|---|
401 with a token-format detail | Signature invalid, token expired, or wrong token type | Request a fresh token from the owner |
401 token revoked | A newer token was minted for your identity with revoke_prior: true — yours is dead even if its expiry date is in the future | Use the newest token; there can be only one |
401 user not found or inactive | The agent identity was deactivated | Escalate to the owner |
403 superuser required | You called an owner-only route (e.g. token minting) | Don’t — ask the owner |
403 naming a contract or capability | Your token is valid, but the write targets a contract outside your pool, or needs a capability you don’t hold | Check my_capabilities; request the specific grant |
403 on a read route | Some read surfaces (e.g. the human task board REST reads) are team-only by design | Use the MCP read tools instead — that is the supported agent path |
Where authentication is enforced
Section titled “Where authentication is enforced”Two layers, both active: a coarse route allowlist for agent writes (only a small set of write routes accept agent tokens at all), and a per-object check inside each handler that verifies the target contract is in your pool with the right capability. MCP tools re-run the same checks in their own bodies — the MCP mount additionally requires a valid token for every call.