grants

arizukoreference › grants

Since v0.38.0 every authorization question — "may principal P perform action A on scope S?" — is answered by one auth.Authorize call against two tables: acl (permission rows) and acl_membership (role + identity indirection). This page is the syntax + semantics reference for both the rule grammar (still used for MCP params) and the tier defaults derived in code at spawn time.

See concepts › authorization for the mental model, GRANTS.md for the maintainer walkthrough, and specs/6/9-acl-unified.md for the migration story. Code: auth/authorize.go, auth/policy.go (structural / hierarchy checks), grants/grants.go (rule engine, tier defaults).

1. Tier model

Tier is derived from folder depth, not stored. From auth/identity.go:16:

Tier  = min(strings.Count(folder, "/"), 3)
World = first path segment
tierfolder shapeexamplerole
00 slashesroot, maininstance root — unrestricted, CLI-only surface
11 slashatlas/support"world" group — basic send + per-platform actions for every platform routed under this world + management tools + read-write share mount
22 slashesatlas/support/oncalldepth-2 group — basic send + per-platform actions narrowed to routes under this folder's subtree + read-only share mount
33+ slashesatlas/support/oncall/launch-q3deep group — reply-only (reply, send_file, like, edit)

2. The two tables

Adjacent tables still in play but orthogonal: groups (folder hierarchy — what exists), routes (which JIDs land in which folder — what reaches the agent).

3. Principals, actions, scopes

principal kindexamplenotes
OAuth subgoogle:114019…Canonical human, minted by auth/oauth.go.
Folder agentfolder:atlas/engThe container spawned at this folder.
Platform identitytelegram:user/123456Channel-side identity, not yet OAuth-claimed.
Room JIDdiscord:837…/1504…Channel / room itself — carries route grants.
Rolerole:operatorIndirection. Members via acl_membership.
Wildcards**, google:*, folder:**Globs anchor on : and /.

Actions: interact, admin, mcp:<tool>, or *. The structural / hierarchy gate on outbound verbs, route management, task ownership, etc. lives separately in auth.AuthorizeStructural (formerly auth.Authorize — renamed in v0.38.0).

Scope is the folder path globbed segment-wise — * doesn't cross /, ** matches zero or more segments. Same matcher as before, now exposed as auth.MatchGroups (pure function, no DB query).

4. Rule grammar (MCP params)

The grant-rule grammar still parses params predicates on mcp:<tool> entries. Strings parsed by grants.ParseRule:

rule    := ["!"] action ["(" params ")"]
params  := param ("," param)*
param   := ["!"] name ["=" glob]

Examples

*                                       # tier-0 instance root
send                                    # any send call
send(jid=telegram:group/*)              # send only to telegram groups
!post                                   # forbid posting
send_file(jid=*)                        # file send to any allowed target
share_mount(readonly=false)             # writable workspace mount
share_mount(readonly=true)              # read-only workspace mount

5. Tier defaults

From grants.DeriveRules. Computed at every spawn, used as the fall-back for mcp:* actions with no matching acl row.

tierdefault rules
0["*"] — unrestricted
1basicSendActions + platformRules(RouteSourceJIDsInWorld(world)) + tier1FixedActions + share_mount(readonly=false)
2basicSendActions + platformRules(RouteSourceJIDsInWorld(folder)) + share_mount(readonly=true) — narrower platform scope: only JIDs routed to this folder or its descendants
3+["reply", "send_file", "like", "edit"]

basicSendActions

send, send_file, reply

platformActions (used by platformRules)

For each platform appearing in the route table's scope, each verb is registered as <verb>(jid=<platform>:*).

send, send_file, reply,
forward,
post, quote, repost, like, dislike, delete, edit

tier1FixedActions

schedule_task, register_group, escalate_group, delegate_group,
get_routes, set_routes, add_route, delete_route,
list_tasks, pause_task, resume_task, cancel_task

6. Structural gate (AuthorizeStructural)

A second authorization layer in auth/policy.go — renamed from auth.Authorize to auth.AuthorizeStructural in v0.38.0 — enforces hierarchy and tier invariants the rule list can't express. Even if the agent's rules permit the action, this gate can reject.

tooltier ruleextra constraint
list_tasksanynone
send, send_file, reply, post, like, dislike, delete, edit, forward, quote, repostanytarget JID's owning folder must equal id.Folder or be a descendant
reset_sessionanytarget folder must equal own or descendant
inject_messagetier ≤ 1
register_grouptier < 2tier 0 ⇒ target must contain "/" (worlds are CLI-only); tier 1 ⇒ child must be direct child of own folder
escalate_grouptier ≥ 2
delegate_grouptier ≠ 3target must be strictly inside id.Folder
get_routes, set_routes, add_route, delete_routetier < 2tier 1 ⇒ route target inside own folder
schedule_task, pause_task, resume_task, cancel_tasktier ≠ 3tier 2 ⇒ task owner == own folder; tier 1 ⇒ task owner in same world
list_acltier ≤ 2tier 2 ⇒ own subtree only; tier > 2 ⇒ unauthorized
invite_createtier < 2tier 1 ⇒ target in same world

7. The operator role

Pre-v0.38.0 the operator was a literal user_groups(sub, '**') row. Now it's a role: acl_membership(sub, role:operator), plus one wildcard acl row granting role:operator the action of choice on scope **. arizuko grant <sub> ** writes the membership edge; the role's permissions are seeded once by arizuko create.

# Make alice an operator.
arizuko grant google:114alice@example.com '**'

# Equivalent SQL:
INSERT INTO acl_membership (child, parent, added_at) VALUES
  ('google:114alice@example.com', 'role:operator', now());

Adding more operators is one membership row each — the permissions row is shared.

8. Spawn-time composition

Canonical site: Gateway.runAgentWithOpts in gateway/gateway.go.

1. Identity  = auth.Resolve(F)
               → {Folder: F, Tier: min(depth, 3), World: firstSeg}

2. Rules     = grants.DeriveRules(store, F, id.Tier, id.World)
               → tier-default list including routed-platform rules

3. Env       = base ∪ FolderSecrets(F)      (deepest-wins hierarchy)
                    ∪ UserSecrets(UserSubByJID(J))   (single-user chats only)

Per-folder MCP overrides live in acl as (principal=folder:<F>, action=mcp:<tool>, scope=<F>, params, effect) rows. The in-container MCP server consults them at registration (grants.MatchingRules — tools with no match are dropped from tools/list) and at invocation (grants.CheckAction — synthesized calls still get refused).

9. Key invariants

10. Go deeper