What an owned key is
Every owned key carries an owner — the account that minted it, recorded on the key’s identity record as an owner claim. That single claim is what makes the key owned rather than free-standing, and it drives three properties:- Minted by an owner. A non-admin mints only keys owned by itself; only an admin may mint an ownerless machine key (see accounts). Ownership is exactly one level deep — an owned key can never itself mint keys.
- Capped by the owner. The key’s scopes are a subset of the owner’s, enforced
at mint time and re-checked on every request. A mint that asks for a scope the
owner does not hold is rejected with a
400at mint time, never a silently narrowed key. - Dies with the owner. Because the cap is re-applied per request, disabling or narrowing the owner attenuates the key on its very next call — no revocation of the key itself required.
Attenuation over time
An owned key’s effective authority is its own grant intersected with its owner’s current policy, computed fresh on every request at the gate. The key does not carry a frozen snapshot of what its owner could do when it was minted:- Move the owner from an editor role to a viewer role and every key that owner holds is re-evaluated against the narrower policy on its next request.
- Disable the owner, or empty the owner’s scopes, and every owned key is denied — the key is powerless while the owner is down, and recovers if the owner is restored.
The capability projection
An owned-key holder needs to know what it may actually do — otherwise a scoped key meets a wall of401/403s with no way to discover its own limits. GET /api/auth/me
answers that: the caller’s derived capability projection.
user_id, its owner (if any), whether it
is an admin, its effective scopes, and the concrete routes, route patterns,
sub-MCP mounts, tools, and agents it can reach right now — plus a mintable
flag. Two properties make it trustworthy:
- Derived, never stored. The projection is computed from the same
access_control_routesstore and policy the gate reads, filtered by the caller’s effective scopes. There is no second access-control list that could drift from the gate. It is cached per policy version and identity, so a policy edit yields a fresh projection on the next read. - jq-exact. For a policy that carries a jq condition, the projection evaluates
the same condition per candidate
(path, method)through the real enforcer, so it never advertises a door the gate would then deny. The projection is a subset of what the gate admits, by construction.
GET /api/auth/me
is authenticated-implies-allowed: an authenticated caller reaches it regardless
of the route table (an unauthenticated caller still gets a 401). It stays under
the reserved /api/auth prefix — it is never public.
Data isolation
An owned key is restricted: any identity whose claims carry an owner is confined to its own slice of the data-bearing surfaces, while unrestricted identities — admins, editor/viewer role-holders, ownerless machine keys — keep the full operator view (their limits come from scopes and jq fences, not isolation). This single mechanical test — does the identity carry an owner claim? — is the whole isolation boundary; there is no new stored type field. A restricted caller sees only its own slice of three surfaces — and that slice is the key’s own identity, never its owner’s. Same-owner keys do not share a slice and cannot see each other’s activity; each key is its own island.- Tool runs, keyed on the run’s owning identity — the caller’s own key
identity (
get_current_user_id()), not its owner. A restricted caller lists and reads only the runs its own key submitted, and a fetch of any other identity’s run — including one belonging to a sibling key under the same owner — is a403. - Interactions, keyed on
audience(below). - Notifications, also keyed on
audience.
audience — one concept across interactions and notifications
audience is the identity a message is addressed to — the same field, with the
same meaning, on both ask_user (interactions) and notify_user (notifications).
It drives the same own-slice-only read on the interactions inbox and the
notifications inbox: a restricted caller sees only messages whose audience is its
own key identity; a message with no audience is an operator/broadcast, shown to
unrestricted callers but not to restricted ones. The write side mirrors this. When a
restricted caller sends with no audience, the field is auto-scoped to its own key
identity — so the message lands in (and reads back from) its own slice rather than
broadcasting — and an attempt to address another identity is refused with a 403,
the write-side dual of the read isolation.
Do not confuse audience with a notification’s channel recipient. They are
two orthogonal axes, and both may be set at once:
Setting
audience is honored even when a channel recipient is also set — you get
both the external push and the in-app inbox entry. Two honest notes for tool
authors:
audienceis a routing hint, not an authorization input for reads. Reads authorize by the caller’s identity, and unrestricted operators always see everything, so addressing a message can neither hide it from operators nor expose another identity’s data. For an unrestricted caller,audienceonly chooses whose scoped inbox surfaces it.- A restricted caller addresses only its own slice — it cannot broadcast. The
slice is keyed on the caller’s own key identity, so leaving
audienceunset auto-scopes the send to that key itself: an unaddressednotify_userorask_useris recorded to — and read back from — the caller’s own scoped inbox, never dropped and never a broadcast. Any otheraudience— a foreign identity, including the key’s own owner — is a cross-identity attempt rejected with a403, and is never escalated to operators. An owned key therefore notifies itself simply by leavingaudienceunset; it never sets an id to reach its own inbox.
To run a restricted machine key, an admin mints it as an owned key with the
owner set to the admin. A
* owner caps nothing, so attenuation is a no-op — but
the isolation rule still applies, so the machine key sees only its own slice:
the runs and addressed messages of that key itself, never its owner’s and never a
sibling key’s. Ownership is thus how you get an isolated automation identity, not
only a human-delegated one.Access channels
The ways an owned key reaches a caller are a defined, closed list — only industry best practice, with the rejected shapes recorded so they are never “conveniently” added later.1
Programmatic API
The key travels in an
Authorization: Bearer <key> or X-Api-Key header.
Header-only is law — a credential never appears in a query parameter, a URL
path, or a body at rest.2
MCP client
The same key in an MCP client’s header config, against the main
/mcp endpoint
or a scope-gated sub-MCP mount.3
Browser UI (the scoped Studio)
The key pasted at the Studio sign-in page and held in the
browser. The Studio renders from the projection, so a scoped key sees only the
features it can reach — one UI for everyone, no separate portal.
4
QR / deep-link claim
A one-time, short-TTL claim link —
<origin>/login#claim=<token>. The token
rides the URL fragment, so it never reaches a server log; the browser (or
tai auth claim) exchanges it once at the always-public POST /api/login/claim,
receives the raw key exactly once (the token then burns), and proceeds as the
browser channel. See the owned-keys guide.Scope doctrine
Isolation makes some read scopes safe to grant to owned keys, and leaves others strictly operator-only. Grant an owned key only the scopes in the safe column; the others expose account-wide data or let an owned key mutate a shared namespace it does not own.
Per-resource ownership for the operator surfaces is not offered — the boundary is
deliberately “own slice on the isolated surfaces, operator-only everywhere else.”
See also
- Access control — the request gate, scopes, and the jq policy model owned keys are capped by.
- Accounts — owners, roles, and who may mint what.
- Use owned keys — mint a capped key, inspect it, and share it by QR claim link.
- CLI reference —
tai auth whoami,tai auth claim, andtai keys claim-link. - HTTP API reference —
/api/auth/me,/api/auth/claim-links, and/api/login/claim.

