arizuko

arizukoconcepts › grants

grants

Auth minted the principal; a grant decides what that principal may do. Every permission check asks the same question: may principal P do action A on scope S? — for example, may google:114alice send a message in eng/oncall? Two tables hold the answer: acl — ACL, Access Control List, the permission rows — for the rules, and acl_membership for “this id belongs to that group.” One auth.Authorize() call walks both. Get those two tables right and there is nothing else to learn.

the four words

Four words name everything in that question. Get these right and the rest is rows.

the action lattice

Actions imply each other. If you have *, you have admin; if you have admin, you have interact and every mcp:<tool>. So a single admin row on a folder covers every tool call inside it — you do not list them one by one. One matching deny stops the check, even when other rows would allow it. The full principal shapes, the action lattice, and the two seeded roles are tabulated in reference / grants; here we stay with the four words and some real rows.

three rows, three real patterns

Each row below is something you would actually insert.

1. Alice can talk in her own folder.

INSERT INTO acl (principal, action, scope, granted_at) VALUES
  ('google:114alice', 'interact', 'alice', now());

2. The eng folder admin manages the whole subtree. The glob eng/** matches eng, eng/sre, eng/sre/oncall, and so on.

INSERT INTO acl (principal, action, scope, granted_at) VALUES
  ('google:114alice', 'admin', 'eng/**', now());

3. Ban one user globally. A deny row at scope ** beats every allow.

INSERT INTO acl (principal, action, scope, effect, granted_at) VALUES
  ('discord:user/badguy', '*', '**', 'deny', now());

The same shape grants a whole channel at once: make the room the principal (discord:837…/channel/1504… with interact on main/lab) and everyone who joins the room picks up the grant for free. Each row has a one-line arizuko grants add … CLI equivalent — full syntax in reference / grants.

roles — one indirection, many members

When the same permission set should apply to several people, you do not copy the row. You make a role, grant the role, and add members. Two tables, two operations.

-- Define the role: editors may admin the docs subtree.
INSERT INTO acl (principal, action, scope, granted_at) VALUES
  ('role:editor', 'admin', 'docs/**', now());

-- Bind alice to the role.
INSERT INTO acl_membership (child, parent, added_at) VALUES
  ('google:114alice', 'role:editor', now());

Adding a second editor — one more acl_membership row. Granting editors a new scope — one more acl row. Roles can contain roles: acl_membership(role:senior-editor, role:editor) works the same way.

claiming a channel identity

Alice posts in Discord as discord:user/811…. Later she logs in via Google. The OAuth callback writes one membership edge:

INSERT INTO acl_membership (child, parent, added_at) VALUES
  ('discord:user/811...', 'google:114alice', now());

That edge is the claim. The next Discord message from her expands to include the canonical sub; any grants on google:114alice apply automatically. No acl row gets rewritten, no migration runs — the channel-side id keeps working as before.

where the check runs

Every gate asks the same question — may this principal do this action on this scope? A message wanting into a folder hits a gate once; a tool call leaving that folder hits one again. The two faces share the handler, the folder-containment rule, and the check itself. An operator’s REST call and an agent’s MCP call both land in auth.Authorize, over the same acl rows; what differs is only where the principal came from — a JWT subject on one side, the socket’s folder on the other. There is no second mechanism for agents and no default derived from where a folder sits. The full call signature lives in reference / grants.

A subagent holds exactly what its parent holds. When an agent spawns a Claude Code subagent inside its turn, the subagent talks over the same unix socket, so the gate sees the same principal and answers the same way. There is no narrower set for a narrower job: a subagent spawned to summarise one file can still call every tool the parent can, secrets and outbound requests included. That is deliberate — the socket is the boundary, and a second socket would be a second answer to the same question. Scope the parent folder to what you are willing to let a subagent do, not just what the agent itself needs. Decision and reasoning: specs/5/5 R4.

don't do this

where to go next

Canonical spec with the full schema and evaluation algorithm: specs/5/32-acl-unified.md. Rule-syntax reference: reference / grants. For where principals come from in the first place, see auth.