Authorization — roles, grants, guardrails¶
This page is the precise, lookup-oriented companion to Authorization, which explains the model in plain language. Here you get the exact action vocabulary, the default role bundles, the grant and ceiling shapes, and the order the server composes them in.
Authorization is decided by two independent axes that evaluate in opposite directions:
- Grants — the additive axis. What a principal can do:
effective = union(role bundles, individual grants). Allow-only, and default-deny — an action not in the union is refused. - Ceilings — the narrowing axis. What a principal is capped to: a
conjunctive policy chain (
org ∧ user ∧ app ∧ run). Every present layer can only ever subtract; no role or grant overrides it.
The final verdict is the intersection:
allowed = (action ∈ union(roles, grants)) AND (every present ceiling allows it)
This mirrors AWS's identity-policies ∩ permission-boundaries split.
There is no explicit Deny and no allow/deny precedence — "a
Contributor may not clone" is expressed by omitting memory.clone from
the Contributor bundle, never by a deny rule.
The rules are enforced server-side. Every other surface — the portal, the CLI, an AI agent, a third-party client — only predicts the server's decision to shape its UI (grey out a button, pre-empt a tool call). An upstream "allow" is a floor, never the final word; the server re-decides on every request.
Action vocabulary¶
Authorization is expressed over a fixed set of action strings. The
tool.<name> namespace is reserved for capability-tool actions.
| Action | Governs |
|---|---|
memory.read |
Read a memory's nodes. |
memory.write |
Create, edit, and delete nodes within a memory. |
memory.create |
Create a new organization-shared memory. |
memory.clone |
Copy an entire memory, including into another organization. |
memory.delete |
Delete an organization-shared memory. |
run.execute |
Execute a headless multi-node run. |
llm.invoke |
Invoke an LLM. |
comm.outbound |
Send an outbound communication (e.g. a chat or email message). |
tool.<name> |
Reserved namespace for capability-tool actions. |
Matcher grammar. Everywhere an action list appears — a role bundle, a grant, or a ceiling's allow-list — entries match an action three ways:
| Entry | Matches |
|---|---|
memory.write |
Exactly that action. |
memory.* |
Any action with that prefix (memory.read, memory.write, memory.create, …). |
* |
Every action. |
Roles as bundles¶
A role is a named bundle of actions — the everyday shorthand, so
permissions aren't handed out one at a time. OrgMember.role is one of a
four-rung ladder: OWNER > ADMIN > CONTRIBUTOR > READER.
The default bundles (DEFAULT_ROLE_BUNDLES) are:
| Role | Bundled actions |
|---|---|
READER |
memory.read |
CONTRIBUTOR |
memory.read, memory.write |
ADMIN |
memory.read, memory.write, memory.create, memory.clone, memory.delete |
OWNER |
memory.read, memory.write, memory.create, memory.clone, memory.delete |
Notes:
memory.create/memory.clone/memory.deleteare Admin/Owner only. A Contributor can read and write existing memories but cannot create, clone, or delete organization-shared ones.- The run-time verbs (
run.execute,llm.invoke,comm.outbound) are not in any role bundle — they are governed by the ceiling chain and the run gates, not the role ladder. - Personal and private memory creation is the owner path (any org member, for their own memory), outside the role ladder entirely — see strict-owner classes below.
- The bundle map is a deliberate, versioned constant. A planned change
moves
memory.createintoCONTRIBUTORwhile keepingmemory.cloneout; when it lands it will be an explicit edit to this map, never a silent shift.
Individual grants¶
A grant hands one principal extra actions without changing their
role — the additive exception. It is backed by a PrincipalGrant row:
| Field | Meaning |
|---|---|
principalType |
USER (v1); reserved for future app/agent principals. |
principalId |
The granted user. |
organizationId |
The org the grant applies within. |
actions[] |
Allowed actions, in the matcher grammar above (e.g. ['memory.clone']). |
expiresAt |
Optional expiry; NULL = perpetual until revoked. |
deletedAt |
Soft-delete column; a revoked grant is one with deletedAt set. |
Grants are purely additive — they only ever add actions to the union, never remove them. Two properties are load-bearing:
- A grant is an exception on top of membership, never a substitute for
it. The gate reads the caller's live
OrgMemberrow and their live, in-scope grants together. With no live membership, even a live grant authorizes nothing — revoking someone's membership instantly cuts every capability, grants included. - Only live, in-scope grants count. The server filters on
deletedAt IS NULL, an unexpiredexpiresAt(NULL OR > now), and the matchingorganizationIdbefore the grant enters the union.
To let one Contributor clone memories, attach a memory.clone grant —
you never promote them to Admin, and you never touch anyone else's
permissions. (There is no custom-role mechanism today: OrgMember.role
holds exactly one of the four ladder roles, and the grant is the
supported way to add a single action to one member.)
Ceilings — the policy chain¶
A ceiling is the opposite of a grant: it sets a cap that trims what's allowed and cannot be overridden. Ceilings compose as a strict conjunction over four ordered layers:
- Absent layer (no policy row) — contributes no restriction; it is simply not part of the conjunction.
- Present layer — default-deny over its
allowlist: the action must match an entry (exact,prefix.*, or*) or that layer denies. - Overall — allowed only if every present layer allows. Order does not change the result (conjunction is commutative); it only keeps failure reporting stable.
A ceiling can only ever subtract, so no role or grant can buy past it.
What the ceiling chain governs today. The policy chain was built for
headless runs — it gates the run-time action vocabulary
(run.execute, llm.invoke, comm.outbound, tool.*) as a run
executes. It is not currently consulted for interactive
memory-management mutations (memory.create / clone / delete): those
existing Organization.policy rows were authored to constrain runs, and
silently re-scoping them to interactive actions would change their
meaning. Extending ceilings to interactive management actions is a
planned, deliberate step, not today's behavior.
Composition order¶
The server composes the axes in one canonical order. The two pure axes above (grants, ceilings) sit in the middle; strict ownership and the platform bypass wrap around them:
- Strict-owner classes first. A
personal- orprivate-class memory is owner-only. No role, grant, ceiling, or platform bypass reaches it — the owner (or, for agent-mediated access, the owner's own installed agent under an active subscription) is the only principal who can. See Memory access for the per-class gates. - Platform
ADMIN/OWNERbypass. For non-strict classes, a platform-level admin or owner is allowed without consulting the axes. This bypass sits outside the evaluator. - The additive union.
action ∈ union(role bundles, live grants), default-deny. This is where an ordinary member's role and any individual grants decide a management action. - The ceiling chain. Every present
org ∧ user ∧ app ∧ runlayer must also allow the action (see the scope note above for which verbs this currently applies to).
An action is permitted only if it survives every applicable step.
Worked evaluations¶
A Contributor clones a memory — refused. memory.clone is not in the
CONTRIBUTOR bundle and the member holds no grant, so the union does not
contain it → refused at step 3. Promoting to Admin or attaching a
memory.clone grant fixes it; both are additive.
A Contributor with a memory.clone grant — allowed. The union is
{memory.read, memory.write} (bundle) ∪ {memory.clone} (grant) =
{memory.read, memory.write, memory.clone}, which contains the action →
step 3 passes. No management ceiling applies today → allowed.
A revoked member with a still-live grant — refused. The grant row is
live, but the OrgMember row is soft-deleted, so step 3 finds no
membership and refuses before the union is even consulted. A grant never
outlives the membership it hangs on.
A run tries comm.outbound under an org policy of ['run.execute'] —
refused. The additive axis may allow it, but the org ceiling layer is
present and its allow-list doesn't match comm.outbound, so the
conjunction fails at step 4.
Related¶
- Authorization — the same model in plain language, with the badges/keys/house-rules analogy.
- Memory access — how the per-class gates (system, app, knowledge, personal, group, private) resolve read and write.
- Edge conditions — the deterministic conditions a headless run evaluates as it walks a flow.
- Multi-node run flows — where the run-time action vocabulary and the policy chain are exercised.