Skip to content

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.

Scope names follow the pattern <resource>:<verb>[:own]:

  • <resource> is what the route acts on — workspace, audit, members, etc.
  • <verb> is read or write. write covers create / update / delete.
  • :own is 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.

ScopeGates
caps:writeEvery config-mutation route — providers, agent roles, content packs (including upload / clone / file edits). Owners and Operators hold it; Members can’t change config.
ScopeGates
workspace:readList/read every workspace in the org, plus per-workspace reads (sandbox files, snapshot files, session state, budgets, activity).
workspace:read:ownSame access, narrowed to workspaces the caller created.
workspace:writeWorkspace 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:ownSame per-workspace lifecycle ops, narrowed to caller-owned workspaces. Bulk ops don’t accept :own.
ScopeGates
tasks:writeSubmit 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:ownSubmit tasks against caller-owned workspaces. Also gates resume (re-animates the session). Every default role carries this.
ScopeGates
audit:readQuery every event in the org’s audit log (GET /audit, GET /audit/stats).
audit:read:ownSame 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.

ScopeGates
auth:writeManage 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:ownManage 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:writeCreate / 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 without auth: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).
ScopeGates
members:readList every membership in the org.
members:read:ownList only the caller’s own membership row. Reserved for custom roles that need stricter narrowing than the defaults.
members:writeInvite, 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.
ScopeGates
apikeys:writeList / 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:writeManage billing. Owner-only.

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).
  • GET reads 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 are scoped via three closed-enum templates rather than arbitrary scope sets — see API keys for the templates and what they exclude.