How it works
Three mutually unusable credential types, a four-role hierarchy, sessions that go stale in one request, and tenancy that never leaks existence.
Three credential types, mutually unusable
Three kinds of bearer credential exist, and each is rejected everywhere except its own surface. The separation is structural, not conventional: agent and user tokens are both JWTs, but a user token carries typ=user and the agent-token verifier explicitly refuses it — and vice versa.
| Credential | What it can do | What it can never do |
|---|---|---|
snops_key_… org API key | Call the whole management API as a machine principal with the admin role — the SDK / CI integration surface, unchanged since layer 1 | Act as an agent identity at the gateway; decide an approval without naming an explicit approver |
| Agent JWT (no typ claim) | Identify an agent to the gateway for tool calls | Call the management API at all — it fails session verification (typ ≠ user) and is a 401. An agent token can never approve its own call. |
User session JWT (typ=user) | Everything the account's role permits on the management API and dashboard | Be presented as an agent identity — the gateway's verifier rejects any token with typ=user |
The role hierarchy
Roles form a hierarchy, not a lattice — each includes everything below it. Every endpoint declares its minimum role; below is what each step up actually unlocks, taken from the gates in the route code.
| Role | Unlocks (cumulative) |
|---|---|
viewer | Every read: agent registry and audit trails, live calls, approvals list, MCP servers, CSV exports, governance report, policy check and policy source. Change nothing. |
developer | + POST /agents (register), PATCH /agents/{id}, POST …/activate, POST …/rotate (agent token rotation) |
security | + lifecycle decisions — …/approve, …/suspend, …/reactivate, …/review, DELETE /agents/{id} — plus approval decisions, POST /mcp-servers, and org policy editing (PUT/DELETE /policy/source) |
admin | + user management: GET/POST /users, PATCH /users/{id} |
403 requires the <role> role (you are <role>) — never a silent filter. Layer 2's segregation of duties still applies on top: even with the security role, you cannot approve an agent you created yourself.Session mechanics
Login returns a 12-hour JWT (typ=user, HS256). But the token is only half the story: on every request, the API re-reads the account row and checks status and role fresh from the database. Disable someone or demote them and it takes effect on their very next call — there is no "wait for the token to expire" window. The token proves who you are; the row decides what you currently may do.
Tenancy
The org is the tenant boundary, and every query on every governed table filters by the authenticated principal's org_id. Reaching for another tenant's resource returns the same 404 as nonexistence — a foreign agent ID, a foreign user ID, even a malformed UUID all look identical, so existence never leaks across tenants.
The last-admin guard
Demoting or disabling the only active admin in an org is refused with 409 cannot remove the last admin. The check counts active admins inside the same transaction as the update, so two concurrent demotions cannot race past it. An org can never lock itself out of its own user management.
Login timing defense
When a login email is unknown, the API still runs a full bcrypt verification against a pre-built dummy hash before answering. Unknown email, wrong password, and disabled account all return the same 401 invalid credentials in the same amount of time — login cannot be used to probe which addresses have accounts.