Grant rules control which MCP tools an agent can use. They are plain text files in the group folder, evaluated at container spawn time. The result is a filtered MCP manifest — the agent receives only the tools it is permitted to call. There is no second enforcement layer; the manifest is the only check.
* # allow everything
send_reply # allow a specific tool
!send_document # deny a specific tool
send_message(jid=telegram:*) # allow with argument constraint
!delegate_to_child # prevent delegation to children
Rules are evaluated top to bottom. An explicit deny (!) beats an earlier allow. The wildcard * is only meaningful as the first rule — it grants everything, subject to subsequent denies.
| Tier | Default rules | Typical use |
|---|---|---|
0 (groups/main/) | * | Operator-controlled groups, full access |
1 (groups/main/team/) | send_messagesend_replyspawn_group | Trusted subgroups with delegation |
2 (groups/main/team/pub/) | send_reply | Public-facing groups, reply only |
Tier is determined by folder depth from groups/. No permission table. Depth is the default.
Create a .grants file in the group folder:
# groups/main/research/.grants
send_reply
read_diary
!delegate_to_child
!spawn_group
Or have the agent set its own grants via the set_grants MCP tool — subject to the narrowing invariant:
set_grants(["send_reply", "read_diary", "!send_document"])
When a group spawns a child group, the child inherits the parent's grants and can only narrow them further. grants.NarrowRules(parent, child) merges the two rule sets:
// parent grants: ["send_message", "send_reply", "spawn_group"]
// child requests: ["send_message", "send_reply", "spawn_group", "read_db"]
// result: ["send_message", "send_reply", "spawn_group"]
// — read_db dropped because parent does not have it
The merged rules go into start.json before the container starts. The buildMCPServer function filters the tool list against these rules before sending the manifest to the agent. An agent that cannot see a tool cannot call it.
Rules can constrain argument values, not just tool names:
send_message(jid=telegram:*) # can only send to telegram: JIDs
send_message(jid=telegram:-100*) # can only send to telegram groups (negative IDs)
This is enforced at the MCP handler level: the gateway checks the rule constraint before executing the tool call. An agent cannot bypass it by calling the tool with a different JID.
A group that handles public Telegram messages should be able to reply but not send unsolicited messages, not spawn subgroups, and not delegate:
# groups/main/public/.grants
send_reply
!send_message
!spawn_group
!delegate_to_child
!schedule_task
The agent in this group receives a manifest with only send_reply (plus read-only tools like get_facts). It literally cannot call send_message — the tool is not in its manifest.
grants.NarrowRules is a structural invariant. A tier-1 group cannot grant tier-0 permissions to its children, because NarrowRules removes any rule that the parent does not have. This is enforced in code, not by policy documentation. The hierarchy is enforced by the merger function.
Because the manifest is the policy, there is no second check — no middleware, no authorization layer in the tool handler (except for argument constraints). The filter runs once at container spawn. The agent's capabilities are fixed for the lifetime of that container invocation. This is why the manifest is described as the policy: it is not just a description of what is allowed; it is the mechanism that makes other tools unreachable.