Agent authentication
A human always owns the account and the data. An agent — Claude Code, Cursor, Codex, Gemini CLI, a CI bot — never holds an account of its own. It holds an agent token: a delegated credential minted by a human, bound to that human and to exactly one repository, restricted to an explicit scope list, revocable at any time from the dashboard, and visibly distinct from the human in every audit trail. Reviews recorded through an agent token are attributed "<label> (agent, on behalf of <owner>)" — never silently as the owner.
Agent tokens look like dbat_… and are sent as a Bearer header on every /api/v1 endpoint (alongside the existing HTTP Basic scheme for CI push tokens, which is unchanged):
curl -H "Authorization: Bearer dbat_…" \
https://dungbeetle.dev/api/v1/repositoryGet a token with the CLI
The easiest path — dungbeetle login runs the device flow for you:
dungbeetle login \
--label "Claude Code on MacBook" \
--scopes runs:read baselines:read analytics:read reviews:writeFirst, copy your one-time code: XXXX-XXXX
Then approve this agent in your browser: https://dungbeetle.dev/ui/connect?code=XXXX-XXXX
Waiting for approval…
Connected as "Claude Code on MacBook" to repository my-app.
Scopes: runs:read baselines:read analytics:read reviews:write
Credential stored at ~/.config/dungbeetle/credentials.json.Open the printed URL signed in as yourself, check the label and requested scopes, pick the repository the agent may touch, and approve. The CLI stores the token at $XDG_CONFIG_HOME/dungbeetle/credentials.json (mode 0600, keyed by server origin), where push / push-baselines pick it up automatically. --label defaults to dungbeetle CLI on <hostname>; omitting --scopes requests the Triage preset. dungbeetle logout revokes the token on the server and deletes it locally. The CLI talks to the hosted cloud by default — self-hosting? Point it at your server with --server <url> or DUNGBEETLE_SERVER_URL.
The approval can happen from any browser on any machine — the code is what links them — so headless boxes and cloud sandboxes work too.
The device flow, raw
For a custom agent, the same flow is two public HTTP endpoints (RFC 8628 shaped; no auth header — the agent has no credential yet).
1. Start an authorization:
curl -X POST https://dungbeetle.dev/auth/device/code \
-H 'content-type: application/json' \
-d '{"label": "My custom agent", "scopes": ["runs:read", "reviews:write"]}'{
"device_code": "…high-entropy secret, keep private…",
"user_code": "XXXX-XXXX",
"verification_uri": "https://dungbeetle.dev/ui/connect",
"verification_uri_complete": "https://dungbeetle.dev/ui/connect?code=XXXX-XXXX",
"expires_in": 600,
"interval": 5
}Show the human the user_code and verification_uri_complete. scopes is optional and defaults to the Triage preset; an unknown scope is a 400.
2. Poll for the token every interval seconds while the human approves at /ui/connect:
curl -X POST https://dungbeetle.dev/auth/device/token \
-H 'content-type: application/json' \
-d '{"device_code": "…"}'Until approval, the poll returns an RFC 8628 error in the standard { "error": … } envelope:
| Error | Status | Meaning |
|---|---|---|
authorization_pending | 400 | Not approved yet — keep polling. |
slow_down | 429 | Polling too fast — add 5s to the interval (honor Retry-After). |
expired_token | 400 | The 10-minute window passed — start over. |
access_denied | 400 | The human denied the request. |
invalid_grant | 400 | Unknown or already-redeemed device_code. |
On approval, the next poll mints and returns the token — codes are single-use, so exactly one poll can ever redeem an authorization:
{
"token": "dbat_…",
"token_type": "bearer",
"label": "My custom agent",
"scopes": ["runs:read", "reviews:write"],
"repository": { "id": "repo_…", "name": "my-app" }
}The plaintext token is delivered exactly once — store it securely.
Scopes
Scopes are granted at approval time and enforced deny-by-default: a request outside the token's scopes gets 403 {"error": "This agent token lacks the '<scope>' scope."}, and endpoints not on this map at all (there is no agent-reachable account, team, billing, or token-minting surface) return 403 for any agent token.
| Scope | Grants |
|---|---|
runs:read | List runs, fetch a run + report, fetch run screenshots. |
runs:write | Ingest runs, probe/upload screenshot artifacts. |
baselines:read | List baselines, read version history and snapshots. |
baselines:write | Upload baseline versions. |
reviews:write | Approve/reject/reopen a run, including promoting candidates to baselines. |
analytics:read | Trend, flakiness, and usage-vs-plan-limits reads. |
GET /api/v1/repository (identity probe) and DELETE /api/v1/token (self-revoke) work with any valid agent token, regardless of scopes.
Presets offered at approval:
| Preset | Scopes | Use for |
|---|---|---|
| Triage (recommended) | runs:read baselines:read analytics:read reviews:write | Coding agents that triage and approve — they read diffs but can't push runs or rewrite baselines directly. |
| Read-only | runs:read baselines:read analytics:read | Reporting and dashboards. |
| Full | All six | Agents that also capture and push. |
Manage and revoke
- Dashboard —
/ui/agentslists your agent tokens (label, repository, scopes, created, last used, optional expiry) and pending device codes, with one-click Revoke. Revocation is immediate: the next request gets401. - Self-revoke — a Bearer token may always revoke itself:
curl -X DELETE -H "Authorization: Bearer dbat_…" …/api/v1/token.dungbeetle logoutdoes this and removes the stored credential. - Expiry — optional, set at approval; expired tokens
401and are swept by retention pruning.
Security notes
- Hashed at rest. Tokens are stored as a peppered HMAC-SHA-256 hash, never plaintext; verification is constant-time. The high-entropy
device_codeis hashed the same way; the shortuser_codeonly routes a signed-in human to the pending request and is useless without a session. - One repository per token. A token can never see or touch anything outside the repository the human picked at approval — and the server re-checks that the approver actually has access to it.
- Owned, not autonomous. Deleting the owning user (or the repository) cascades the token away; an agent credential cannot outlive its owner.
- Audited. Every mint, use, denial, and revocation is written to the structured audit log with the agent's label; reviews carry
"<label> (agent, on behalf of <owner>)"in the append-only trail. - Single-use redemption. Device codes expire in 10 minutes, are rate-limited per IP, and can be redeemed exactly once.
Next: wire the token into your agent — Claude Code, Cursor, Codex, Gemini CLI, or any MCP client via Setup.