Scoped API tokens for programmatic access from other devices #109
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lz/agent-nexus#109
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Want
Register long-lived API tokens in Nexus that other devices can use to call the HTTP API, each token limited to a chosen set of capabilities. Concrete first use case: a token that can do nothing but
GET /api/quota, so a phone widget / status bar / dashboard elsewhere can read fleet quota without holding credentials that could spawn a workspace or read the config volume.Designed. Spec:
docs/superpowers/specs/2026-09-04-api-tokens-design.md.Why the current auth model can't do this
Auth today is a single operator passphrase → an in-memory
SessionStoreentry holding the Argon2id-derived master key, addressed by an httpOnly cookie (nexus/src/lib/server/auth/session.ts,routes/api/auth/unlock/+server.ts). Consequences for a headless caller:hooks.server.tsgates onsessionexisting, full stop. There is no capability dimension.event.locals.masterKeyis the unlocked vault key. A token minted for reading quota must never imply the vault is unlocked.Shape
api_tokens:id,label,token_hash,scopes(JSON array),created_at,last_used_at,expires_atnullable (null = never). Revoke is a hard delete, per the repo's no-soft-delete convention.encrypted_*column, sorotateMasterPassphrasegains no pass andsealed-columns.test.tsneeds no change.Authorization: Bearer nxs_<32 bytes base64url>. The prefix makes tokens identifiable in logs and leak scanners. Plaintext is shown exactly once, at creation. Cookie auth is unchanged and remains the browser path.hooks.server.tsgains a fourth branch: no cookie session, but a valid bearer token → setevent.locals.tokenScopesand do not setevent.locals.masterKey. Every vault-touching route already guards onmasterKey, so that alone is defense in depth — but it is not the authorization model, because sensitive routes exist that need no vault key (/api/workers/[id]/logs,/api/settings/agent-instructions,artifacts/[artifactId]/raw).event.route.id, the SvelteKit route pattern (/api/workers/[id]/health), not the URL path. Exact-string matched, no regex, no traversal surface; an unmapped route is denied by default.quota:readandworkspaces:read, independently grantable./api/workersand/api/workers/[id]/sessionsare mapped for their GET handlers, and their POST handlers spawn a workspace and create a session. Without the method ruleworkspaces:readwould be a write scope./settings?tab=tokens— create (label + scopes + optional expiry), show the plaintext once, list with last-used, revoke.Two properties fall out rather than needing code: a token cannot manage tokens (
/api/tokensis not in the map), and a token cannot open a terminal (terminal/upgrade.tsdoes cookie auth only and never parsesAuthorization).Resolved
quota:readneeds no key at all./api/statestays inPUBLIC_API.