---
title: Authentication and grants
description: How agent tokens are minted, what the grant pool is, the token-replaces-pool gotcha and the two safe ways around it, and every 401/403 cause with its fix.
locale: en
version: 0.2.0
canonical: https://enterium-developer-docs.pages.dev/developers/guides/authentication/
---

> Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json.

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

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:

```json
{
  "email": "my-agent@example.dev",
  "expires_days": 90,
  "revoke_prior": true,
  "grants": [
    { "salescontract": 12345, "capabilities": ["submit", "launch", "cancel", "edit_config"] }
  ]
}
```

- `grants` is **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

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

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:

1. **List the full pool** in the mint request: fetch the current grants
   first, then pass old + new together.
2. **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

| 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

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.
