grants
A grant decides what a principal can do. Every permission check asks one question: can principal P do action A on scope S? For example, can google:114alice send a message in eng/oncall? Two tables hold the answer. acl, the access control list, holds the permission rows. acl_membership records that one id belongs to another, such as a role. One auth.Authorize() call reads both tables.
Three functions read the same rows
The figure shows the caller on the left and the rows on the right. Three functions read the same rows for three questions. Can this call run? Which tools does this turn see? Can this person pass a grant on?
┌─────────────────────────────────┐ ┌───────────────────────────────────┐
│ caller │ │ acl row │
│ operator REST: the JWT sub │ │ principal, action, scope, │
│ agent socket: the turn's sender │ │ params, predicate, effect, │
│ and folder:<turn folder> │ │ grant_option │
└────────────────┬────────────────┘ └─────────────────┬─────────────────┘
│ expanded through acl_membership, │
│ transitive: role:member, │
│ role:operator, roles, the account a │
│ paired JID acts as │
▼ ▼
┌───────────────────────────────────────────────────────────────────────────┐
│ the rows that reach the expanded principal set, read on every call │
└──────┬─────────────────────────┬─────────────────────────────┬────────────┘
▼ ▼ ▼
┌──────────────────────┐ ┌──────────────────────┐ ┌───────────────────────┐
│ Authorize │ │ EffectiveActions │ │ Delegate │
│ the per-call gate: │ │ tools/list: the │ │ add_acl: each new row │
│ scope glob, params, │ │ action held at any │ │ a subset of the rows │
│ predicate; deny │ │ scope; a deny row │ │ held WITH GRANT │
│ wins, no row = deny │ │ hides no tool │ │ OPTION; never a deny, │
└──────────────────────┘ └──────────────────────┘ │ never a wildcard │
│ principal │
└───────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│ AuthorizeTurn, the agent socket's two axes. Route: folder:<turn folder> │
│ and its roles confer in full. Identity: the sender and its roles add an │
│ allow only inside the turn folder's subtree, on the exact action, with no │
│ lattice. A deny counts on both axes, everywhere. │
└───────────────────────────────────────────────────────────────────────────┘
Four words name every part of the question
- Principal
- Who asks: a person such as
google:114alice, the agent of a folder such asfolder:atlas/eng, an id from a channel, or a named role. - Action
- What the principal wants:
interact,admin,mcp:<tool>or*. - Scope
- Where: a folder path such as
atlas/eng, or a glob such asatlas/**. - Effect
allow, the default, ordeny. A deny wins.
A broader action includes the narrower ones
Actions include each other. * includes admin, and admin includes interact and every mcp:<tool>. So one admin row on a folder covers every tool call inside it, and you do not list the tools one by one. One matching deny stops the check, even when other rows allow it. reference / grants has the tables of principal shapes, the action lattice and the two seeded roles. The rest of the page uses the four words and real rows.
Three rows cover three common cases
Each row below is a row you would write. The CLI form is arizuko group -i <instance> grant <principal> <scope> [action], and the action defaults to admin.
Alice can talk in her own folder
principal: google:114alice action: interact scope: alice
The eng admin manages the whole subtree
The glob eng/** matches eng, eng/sre, eng/sre/oncall and every deeper folder.
principal: google:114alice action: admin scope: eng/**
One user is banned everywhere
A deny row at scope ** wins over every allow.
principal: discord:user/badguy action: * scope: ** effect: deny
The same shape grants a whole channel at once. Make the room the principal, for example discord:837412095138652180/1504288810112233445 with interact on main/lab. Everyone in the room then gets the grant. reference / grants has the full syntax.
A role gives one permission set to many members
When several people need the same permissions, create a role, grant the role and add members: one acl row for the role and one acl_membership edge for each member. Create both through the acl and acl_membership resources.
acl: principal: role:editor action: admin scope: docs/**
acl_membership: child: google:114alice parent: role:editor
A second editor is one more edge. A new scope for editors is one more acl row. A role can contain roles: role:senior-editor as a child of role:editor works in the same way. The store refuses a write that would make a principal its own ancestor, so the walk always ends.
A pairing edge links a channel id to a person
Alice posts in Discord as discord:user/811270095431860224 and has a Google login. She confirms a pairing link in a browser where she is logged in with Google. Onboarding follows the flow. The confirmation writes one membership edge:
acl_membership: child: discord:user/811270095431860224 parent: google:114alice added_by: pairing
Her next Discord message expands to include the canonical sub, so every grant on google:114alice applies. No acl row changes.
The sender does not lend the agent their rights
On the agent socket, a turn has two principals: the folder it runs in and the person who sent the message. The grants of the folder count everywhere they reach. The grants of the sender count only inside the subtree of that folder, and only on the exact action. Without this limit, an operator who chats in a team room would give the agent of that room the reach of the operator. Anyone who could make the operator say something there could then use that reach. A deny counts on both principals, so a deny on the person still refuses the turn that the person started.
Agents and operators pass the same check
The REST and MCP faces share the handler, the folder-containment rule and the check. A REST call from an operator and an MCP call from an agent both go to auth.Authorize, over the same acl rows. REST uses the JWT subject. MCP supplies both the attested sender and the turn folder. reference / grants has the full call signature.
A subagent holds exactly what its parent holds
When an agent starts a Claude Code subagent inside its turn, the subagent uses the same unix socket. So the gate sees the same principal and gives the same answer. A narrower job gets no narrower set. A subagent that summarises one file can still call every tool of the parent, secrets and outbound requests included. Limit the parent folder to what you let a subagent do, not only to what the agent itself needs.
Five patterns cause problems
- Do not add deny rows to take back access
- Remove the allow row or the membership edge. A deny is for a real exception, such as one banned user in an open channel, not for undoing a grant.
- Do not grant
adminwith a params predicate adminmeans full write in the scope. For a narrow capability, usemcp:<tool>with params.- Do not put routing into the ACL
- The
routestable answers where a message goes.aclanswers whether a principal can act there. The two tables share the names of room ids but never merge. - Do not skip roles for operators
arizuko group -i <instance> grant <sub> '**'makes someone an operator: it adds the sub torole:operator. Another operator is another membership edge, not a new wildcard grant.- Do not put a predicate on a
folder:principal - The agent socket carries no login claims, so such a row never matches. Put the predicate on a user principal.
auth/README.md has every rule
[auth/README.md] has every rule behind the figure. reference / grants has the rule syntax. auth explains where principals come from.