Skip to content

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.delete are 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.create into CONTRIBUTOR while keeping memory.clone out; 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 OrgMember row 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 unexpired expiresAt (NULL OR > now), and the matching organizationId before 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:

org  ∧  user  ∧  app  ∧  run
  • Absent layer (no policy row) — contributes no restriction; it is simply not part of the conjunction.
  • Present layer — default-deny over its allow list: 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:

  1. Strict-owner classes first. A personal- or private-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.
  2. Platform ADMIN/OWNER bypass. For non-strict classes, a platform-level admin or owner is allowed without consulting the axes. This bypass sits outside the evaluator.
  3. 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.
  4. The ceiling chain. Every present org ∧ user ∧ app ∧ run layer 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.

  • 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.