Skip to main content
Every request reaching a server is judged as one of a small, fixed set of identity types. This page names those types, shows what each may do across the feature surface, and explains the composed layers that produce the verdict. It builds on access control (the request gate), accounts (the humans behind it), and owned keys (capped sub-identities).

The model at a glance

Before the full matrix, here is the one-line model: an editor does everything across the feature surface except access-control administration and a small set of admin-only deployment and infrastructure actions; a viewer sees that same surface read-only; and both always keep self-service over their own credentials.
Admin-only means deployment-level. A handful of genuinely dangerous, infrastructure actions are reserved for an admin and are never grantable to a non-admin role. Everything else — the day-to-day feature-configuration surface — an editor reaches through its role grants (a per-tag read / write level), because an editor is trusted to build and run the features it is granted.
The two columns below are independent lists — the left is the admin-only set, the right is what an editor drives through its grants: A viewer reaches that same editor surface at read only. Both an editor and a viewer always keep self-service — their own API keys, their own identity, and their own password.

Identity types

There are seven actors. Each is distinguished by how it authenticates and whether it is an admin — the admin verdict is a condition-free * effective policy, computed server-side and stamped on the request, never taken from a provider’s word.

The three composed layers

A role composes three parts, and enforcement is their intersection, fail-closed:
  1. A base-tier ceiling (non-editable) — owner-scoping for a minted key, the reserved /api/auth control-plane gate, and the viewer read-only ceiling. This is the security posture a role inherits from its tier (admin, editor, or viewer); it is never authored through the grant map.
  2. A route action-class (declared at registration) — every authed route declares one of read, write, fenced, or secret. read/write are the grantable classes; fenced/secret are the admin-only fence. A route that declares no class fails boot — there is no allow-by-omission.
  3. A per-tag access-level map (editable) — the role’s level on each feature group: none, read, or write. This is the part an admin edits.
The action a request needs is derived from its HTTP method centrally — GET/HEAD/OPTIONS → read, POST/PUT/PATCH/DELETE → write — so the same route classes read and write uniformly.

Permissions matrix

Rows are feature areas; columns are the identity types. Every cell reads against the current seeded roles. The cell words mean: Listing a server’s advertised tools is a read: it answers from the listing the answering worker bound the server from, and never starts the server or connects to it, so a viewer reads the full list. Setting the tool selection is a write. Both routes carry the server’s title in the path (GET /api/mcp-status/{title}/advertised-tools, PUT /api/mcp-config/entries/{title}/tool-filter), so, as with the reload, the request path a policy and the route table judge names the one server. by role — an owned key reaches a feature exactly where its owner’s live role allows, intersected with the owner’s scopes and re-evaluated on every request. There is no per-key grant map: a key follows its owner’s role up and down as an admin edits it. Isolation to the caller’s own runs, interactions, and notifications is the only hard resource confinement.

The grantable per-tag surface

The resource is a feature group (a route’s tag); the action is derived from the HTTP method — the same shape as Kubernetes RBAC, AWS IAM, or GitHub fine-grained permissions. A role’s level on a tag decides its reach:
  • write admits the tag’s reads and its non-fenced writes;
  • read admits the tag’s reads only (GET / HEAD / OPTIONS);
  • none (the default for any tag not in the map) admits nothing.
A route declared self-service (own keys, the tokens payload, claim links, the capability and identity projections, the read-only scopes listing, logout, a provider’s own-credential routes) is not level-governed: every non-admin role keeps it, whatever its map says. A /api/auth control-plane route the base-tier ceiling reserves is not level-governed either. The grant map therefore lists only the feature groups a level opens; a tag whose routes are all admin-only, self-service or public is never in it, and a grant on it is refused. A role granted a tag reaches every grantable route under it — including a route added later under that same tag. Shipping a brand-new feature group is a deliberate release step: its tag is level none for every non-admin role until an admin grants it. The seeded editor and viewer roles derive their maps from the live route registry, so a new grantable feature area joins their default reach automatically; a role an operator authored does not, and an admin opens it explicitly.

The admin-only fence

Two route action-classes are never grantable — no per-tag level opens them for any non-admin role. They cover the infrastructure mutations, the role and policy control plane, and the deployment’s env/settings store — not the everyday feature-configuration surface an editor drives through each feature’s own routes:
  • fenced — the admin-only mutations. Running a tool directly (POST /api/run-tool); reloading or removing a tool; writing and reloading config (POST /api/config/env, /api/config/reload); replacing the manifest; reloading failed MCPs and deregistering an MCP; reloading the fleet; marketplace install / uninstall / update; backup import / export; and the role and policy control plane — creating, editing, deleting, and rolling back roles, and rolling back a user’s policy. Editing scopes, the route table, and the public-route pins is not fenced — those routes are ordinary read/write class, held admin-only by the base-tier /api/auth control-plane ceiling described below.
  • secret — the admin-only reads, over two admin-owned stores. The bulk role / policy reads: the roles listing (GET /api/auth/roles), the role version histories, and a user’s policy version history — their payload carries raw jq policy bodies, so a bulk read is admin-equivalent. And the deployment config store: the stored environment (GET /api/config/env) and the per-settings-class values (GET /api/config/settings-schema). Environment and settings are one store — the stored environment plus each settings class’s values, including deployment secrets such as the connector encryption key — so only an admin reads or writes it.
Because the class is declared on the route, the fence is not a hand-maintained list an author can forget to extend: a new mutating route that should be admin-only declares fenced, and a route that declares nothing fails boot rather than defaulting open.
An editor configures every feature; it does not own the deployment store. An editor keeps full feature functionality — it configures connectors, MCP and sub-MCP config, presets, hooks, schedules, and tool extensions through each feature’s own grantable routes and stores, never through the env/settings store. Running a tool through the async run surface (POST /api/tool-runs), scheduling a tool, setting the MCP config, reloading a single MCP server, mounting a sub-MCP, setting tool extensions, registering hooks, and reading the hooks listing are ordinary grantable read/write routes an editor reaches. What stays admin-only is the deployment config store — reading the env and settings (GET /api/config/env, GET /api/config/settings-schema) and the env write — alongside the pre-existing infrastructure mutations (a direct tool run, tool reload / remove, fleet reload, manifest replace, MCP reload-failed / deregister, marketplace, backup) and access-control administration.

The control-plane (/api/auth) gate

The editor and viewer base tiers default-deny the whole /api/auth area and carve back in only a fixed self-service list: managing one’s own API keys and claim links, the identity projection (me, capabilities, tokens-payload, the read-only GET /api/auth/scopes), changing one’s own password, and logout. Everything else under /api/auth — editing scopes, the route table, public-route pins, policy history and rollback, and role and account management — falls to the default-deny, so only an admin reaches it. This ceiling is separate from the action-class fence, and it holds even for a viewer: the self-service surface stays reachable regardless of the read-only ceiling.

How a request is judged

The verdict composes the layers with AND, evaluated fail-closed — a backend error at any layer raises and surfaces as a clean deny, never a silent default.
1

Authenticate and resolve the role

The credential resolves to an identity and its policy. The policy carries a live role-name pointer, not a frozen copy — enforcement reads the role’s current grant map at request time. For an owned key, the governing role is the owner’s, and the key’s scopes are intersected with the owner’s right here, so a change to the owner takes effect on the key’s next call.
2

The base-tier ceiling

The role’s base-tier condition applies: owner-scoping, the /api/auth control-plane gate, and — for a viewer — the read-only ceiling. An admin is condition-free and skips this pass.
3

The per-tag level + the fence

The request’s route resolves to its feature tags and action-class. A fenced or secret route denies every non-admin outright. A route declared self-service is admitted for every role here. Otherwise the governing role’s level on the route’s tag must satisfy the method’s derived action. An admin skips this pass entirely.
4

The route→scope table

The request path maps to scope ids, and the guard admits the request only if the identity’s scopes cover every one. Because every role-holder carries *, this gate only meaningfully constrains scoped keys: a registered, authenticated route the operator mapped to no scope resolves to the universal scope *, so every role-holder reaches it out of the box while a scoped key needs an explicit row. A path the application does not serve (no registered route) resolves to nothing and fails closed for every ordinary identity — the sole exception is the stamped super-admin, admitted so a root can never lock itself out of a route it can map anyway.
An authentication failure returns a fixed 401; an authenticated-but-forbidden request returns a fixed 403. The full reason is logged server-side only.

Roles are live and editable

A role is not a fixed template. An admin can create, edit, delete, and roll back roles — through the Studio or the tai roles CLI — and every edit is live: it changes every current holder’s authorization on their next request, with no reassignment. Editing a role also re-attenuates every key its holders own, since an owned key follows its owner’s live role. The reserved admin role is permanent — undeletable, unrenamable, always full control — and multiple admins are allowed. See accounts for the editing surface and the assignment flow.

The last enabled admin stays

An admin is every enabled principal whose own policy is admin-shaped — an account on the admin role, the owner the setup door creates, a service principal on the admin role — whichever provider or door created it. The platform refuses any change that would leave none (409, nothing written), on every door that can change a principal’s standing:
  • a member action that changes a role, disables, or removes an account;
  • disabling or deleting a principal through the principals routes;
  • editing a principal’s own policy through the API-key routes — its scopes or its condition — or rolling that policy back to an earlier version.
A principal’s disabled state lives on its principal record and is mirrored into its policy data ("disabled": true) for enforcement. Only the principals routes, the member actions, and an access_control restore change it — a restore gives a principal the deployment does not hold yet its archived state, and leaves an existing principal as it stands. A policy edit or rollback never does. An edit whose policy data carries a disabled value other than the principal’s current state (true while disabled, false while enabled) is refused (400, nothing written); any other edit, and every rollback, keeps the principal’s current state. Minting an API key whose policy data carries disabled is refused the same way: an API key is switched off by revoking it. Deleting a scope or a route never reaches an admin’s grant: the universal scope * names no route, so mapping a route to it (POST /api/auth/scopes with "scope_id": "*") and deleting it (DELETE /api/auth/scopes/*) are both refused (400, nothing written). Mapping a URL into any scope is refused the same way when a route that declares itself public serves it, on any method: such a route answers everyone whatever it is mapped to, and the routes listing marks it declared_public: true. A policy that grants nothing is never written either. A policy with neither a scope nor a condition reads as no key at every door, so minting an API key with neither is refused (400, nothing written), and so is an edit that would leave a key’s or a principal’s own policy with neither — the refusal says to revoke the key, or to disable or delete the principal. For the last enabled admin principal the last-admin 409 answers first. Because every admin counts, demoting the last admin account is allowed while another admin principal — the owner, say — is still enabled. Promoting someone never needs the check, and an API key’s own policy row is not a principal and never counts. Nor can it become one: creating a principal under the user id of a minted key (POST /api/auth/principals) is refused (409, nothing kept).

An account is not an API key

Two things are easy to confuse because both hand someone a working identity. They are different in kind — who creates them, what sets their authority, and how far that authority reaches. An account is the human side: only an admin creates one, and its authority is the role the admin assigns by name. The account’s enforced policy carries a live pointer to that role, not a copy — editing the role changes the account’s access on its next request, and moving the account to a different role re-points it immediately. An API key is a credential any user mints to delegate a slice of its own access to a teammate, a device, or a script. A key minted by a non-admin is an owned key: attenuated to a subset of its creator’s authority, capped live (the cap is re-applied every request, so it follows the owner up and down), and isolated to its own slice of the runs, interactions, and notifications surfaces. A key can never exceed the user who created it, and an owned key can never itself mint further keys. Editing an account’s role therefore also re-attenuates every key that account owns, without touching the keys themselves.

See also