Permissions & scopes
Every authenticated caller — human session or
API key — carries a set of permission
scopes. Each route in the API reference
declares which scope(s) it requires; a request that doesn’t carry
any of them returns 403 permission_denied.
The console mirrors this — anything a caller can’t reach is hidden rather than shown-and-disabled, so the UI never offers an action it knows will fail.
The scope ladder
Section titled “The scope ladder”Scope names follow the pattern <resource>:<verb>[:own]:
<resource>is what the route acts on —workspace,audit,members, etc.<verb>isreadorwrite.writecovers create / update / delete.:ownis an optional suffix that narrows the scope to resources the caller created. The unscoped form grants org-wide access.
For resources where ownership is meaningful (workspaces, audit events, credentials) both variants exist; the caller carries one or the other. For resources where ownership isn’t meaningful (providers, agent roles, content packs, secrets) only an unscoped write scope exists.
When a route accepts both variants — (workspace:read OR workspace:read:own) — the handler does the narrowing: callers
with the unscoped scope see everything in the org; callers with
only the :own variant see only resources whose created_by_user_id
matches their own.
Scope reference
Section titled “Scope reference”Configuration
Section titled “Configuration”| Scope | Gates |
|---|---|
caps:write | Every config-mutation route — providers, agent roles, content packs (including upload / clone / file edits). Owners and Operators hold it; Members can’t change config. |
Workspaces
Section titled “Workspaces”| Scope | Gates |
|---|---|
workspace:read | List/read every workspace in the org, plus per-workspace reads (sandbox files, snapshot files, session state, budgets, activity). |
workspace:read:own | Same access, narrowed to workspaces the caller created. |
workspace:write | Workspace lifecycle: create, restart, archive / unarchive, stop. Bulk lifecycle (POST /workspaces/stop-all, POST /agent-roles/{id}/stop-workspaces) requires the unscoped variant since it crosses users. |
workspace:write:own | Same per-workspace lifecycle ops, narrowed to caller-owned workspaces. Bulk ops don’t accept :own. |
| Scope | Gates |
|---|---|
tasks:write | Submit a new task in any workspace in the org. Never granted to any default role — see member roles. Kept as a closed-enum value for custom roles. |
tasks:write:own | Submit tasks against caller-owned workspaces. Also gates resume (re-animates the session). Every default role carries this. |
| Scope | Gates |
|---|---|
audit:read | Query every event in the org’s audit log (GET /audit, GET /audit/stats). |
audit:read:own | Same endpoints, narrowed to events attributable to the caller via user_id. |
/workspaces/{id}/audit is gated by both an audit:read* scope
and a workspace:read* scope — workspace creators with :own
audit see every event for their workspace (not narrowed by
user_id), since they own the workspace’s full activity trail.
Credentials and secrets
Section titled “Credentials and secrets”| Scope | Gates |
|---|---|
auth:write | Manage org-scoped outbound credentials (the bot accounts everyone in the org uses by default). Also allows flipping allow_user_override on Authorization Code credentials so members can layer personal identities. Holders see every credential on reads, including other members’ personal overrides. |
auth:write:own | Manage personal credentials only — the per-user override rows a Member creates on themselves. Authorization Code kind only (the only path where a per-user durable credential is meaningful). |
secrets:write | Create / delete Vault secrets referenced from caps.yaml transforms. |
Credential and secret reads are open to any signed-in caller — no scope required:
GET /credentials,GET /credentials/{id}— handler narrows per-user: callers withoutauth:write(so Members) see only org-scoped credentials plus their own personal overrides.GET /secrets— lists secret names (values are never returned by any endpoint).
Memberships
Section titled “Memberships”| Scope | Gates |
|---|---|
members:read | List every membership in the org. |
members:read:own | List only the caller’s own membership row. Reserved for custom roles that need stricter narrowing than the defaults. |
members:write | Invite, change roles, and remove members. Owner-only. The handler additionally enforces Identity.Role == Owner inline on the role-change and remove paths — that’s hierarchy logic on top of the scope. |
API keys and billing
Section titled “API keys and billing”| Scope | Gates |
|---|---|
apikeys:write | List / create / revoke API keys. Owners and Operators hold it. Members can’t see the key inventory at all — the labels operators pick (e.g. Stripe deploy) telegraph the org’s integrations. |
billing:write | Manage billing. Owner-only. |
Routes with no scope check
Section titled “Routes with no scope check”A handful of routes need no scope beyond being authenticated:
GET /me/session— resolves the caller’s own identity.GET /config— server-side defaults the console / CLI need to render forms (task timeout defaults, OAuth callback URL).GETreads on org-shared config:/providers,/providers/{id},/agent-roles,/agent-roles/{id},/agent-roles/{id}/access-review,/agent-roles/k8s-stats/stream,/packs,/packs/{id},/packs/{id}/files,/packs/{id}/files/{path}.GET /credentials,GET /credentials/{id},GET /secrets— see above for the per-user narrowing on credentials.
API keys
Section titled “API keys”API keys are scoped via three closed-enum templates rather than arbitrary scope sets — see API keys for the templates and what they exclude.