The policy door
In this section
A subagent does not go rogue because it is malicious. It goes rogue because it lost the context it was handed: the rules were prose in a prompt, the window flooded, the prompt was compacted, and what is left is a capable model with no restrictions and a tool belt. Everything downstream of that — the deleted directory, the publish nobody approved, the public repo — is a consequence of one design mistake: the rules lived in the agent.
The policy door moves them into the store. A rule is a ROW: durable, generation-pinned, bound to an actor, a scope and a list of doors, carrying the authority that let it exist and the one sentence a refused caller is shown. Every door evaluates the rows before it decides anything. An agent that never read a rule still meets it. An agent whose window flooded loses nothing that binds it.
The shape of a rule
{
"id": "only-the-source-registry",
"subject": "*",
"scope": { "pathPrefix": "/projects/brainy" },
"doors": ["Bash", "add", "github_*"],
"effect": "refuse",
"reason": "Packages publish only to The Source, never npmjs.",
"authority": "record:PLATFORM-RENAME-TRAIN-FLEET",
"locked": true,
"since": "2026-09-15",
"until": null,
"match": [
{ "field": "command", "matches": "npm publish" },
{ "field": "command", "notMatches": "--registry[= ]https://source\\.soulcraft\\.com" }
]
}JSONfield | what it is | why it is a field and not a convention |
|---|---|---|
| a slug | it is quoted verbatim in the refusal, so it must read as a reason |
| an actor slug, | an actor is whoever the door AUTHENTICATED — never a name the request supplied about itself |
|
| absent axes are unscoped; a present axis must match or the rule stands aside |
| door names, tool names, globs ( | a rule may govern tools brainy does not serve — see below |
|
|
|
| one sentence | a rule with no reason is a wall; a rule with a reason is an instruction |
|
| no agent grants itself a rule — a rule with no authority cannot be declared |
| whether it moves under a lesser authority | a ratified rule is locked by default |
| calendar days |
|
| clauses over the call's own arguments ( | it is what lets a rule say "not |
There is no lookaround in a pattern: the engine's regular expressions are linear-time by construction, which is a property worth keeping in a gate that runs on every write. "This, but not that" is therefore two clauses — matches and notMatches — which is also the form a person reading the rule can check. A pattern carrying (?!…) is refused by name, saying so.
Rules are immutable rows. A rule that must change is retired (a retirement row that names it) and declared again — two audited writes — so the question "what bound whom on the day of the incident" stays answerable.
The verdict, and the precedence law
policy.check({ actor, door, args, scope }) answers { verdict: 'allow' | 'refuse' | 'ask', rule?, reason, skipped[], digest }. It is pure, deterministic, and implemented ONCE — in Rust, in native/src/policy/evaluate.rs. The in-process API, brainy serve, the MCP door and the harness hook all reach that one function through the crossing profileWriteDoor already is, which is why they cannot drift.
The most restrictive matching rule wins —
refusebeatsaskbeatsallow. Not "the most specific wins": specificity decides which rule is NAMED, never whether the call proceeds. A narrowallowmust not punch a hole in a widerefuse, because the actor most motivated to declare that narrow rule is the one the wide rule binds.No matching rule is
allow— stated loudly, because it is a decision. This engine is not a whitelist: a store with no rules behaves exactly as it did before this profile existed. The refusal a fresh agent meets comes from a rule that binds*.A rule that needs a name the door does not have STANDS ASIDE, counted. That is the
isActorclause — "the author is not the caller" — and the subject forms that name an actor. Learned from the Self's own 0.6.366 incident (2026-09-14, rolled back in seven minutes): a rule whose subject names a specific actor cannot be evaluated at a door that knows only an account. It is skipped, NAMED inskipped[], and never silently dropped. The failure mode being avoided is not "too permissive" — it is "every write refused, for every party, because one substitution was unknowable at that door".A refusal on a READ door is a refusal, never an empty answer. An empty answer reads as "this brain knows nothing", which is a different fact.
Where it is enforced: inside the door
policy.check is called INSIDE add, relate, update, remove, the memory writes, the accords writes and vfs.writeFile — before the plan is decided, so a caller cannot skip it by not calling it. The actor is whoever the door already knows: the minted key's subject on serve/MCP, the caller the in-process API carries (the same field memory.turn() refuses without).
Every refuse and every ask writes an audit row — actor, door, rule, generation, and the call's DIGEST rather than its arguments, because an audit trail read by whoever is investigating an agent must not become a second copy of what that agent was writing. policy.audit({ since, actor }) reads them. Audit rows, rule rows, retirement rows and ratification rows all carry visibility: internal and are fenced from ordinary recall exactly as board rows are.
Rules over tools brainy does not serve
A rule's doors may name ANY tool the agent might call — Bash, web_search, github_*, an MCP tool from another server. Brainy enforces its own doors inside the engine. For foreign tools there are two adapters, and the evaluation is the same function in both.
(a) The harness hook — available now
brainy policy check --actor <slug> --tool <name> --args-json - # stdinBASHExit 0 = allowed. Exit 2 = refused, with refused by rule <id>: <reason> on stderr. A coding-agent harness with a pre-tool hook blocks the tool call on exit 2 and shows the agent stderr — which is the whole mechanism: the agent is told the rule at the moment it matters, by something it cannot forget to consult.
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "brainy policy check --brain $BRAINY_STORE --actor $AGENT_SLUG --tool \"$TOOL_NAME\" --args-json -"
}
]
}
]
}
}JSONOther harnesses have the same shape: a pre-tool command, stdin carrying the arguments, a non-zero exit that blocks — name the actor/tool environment variables however your own harness's hook convention supplies them.
(b) The gateway — designed here, built later
brainy serve fronts the client's OTHER MCP servers: the client connects to brainy, brainy holds the upstream connections, every tools/call is checked against the same rules, an allowed call is forwarded verbatim and a refused one comes back as a named refusal (never a protocol error). The hook governs one machine's harness; the gateway governs every client of one store, including harnesses with no hook mechanism at all. The design constraint that decides it: the gateway must forward BYTE-IDENTICAL arguments and must never see a credential it does not need — the upstream server's own auth stays between the client and that server wherever the transport allows it.
(c) The mandate slice — always
memory.context() returns a mandate slice FIRST, never budget-trimmed: the rules that bind this caller, by actor and scope. The door enforces them anyway. The slice exists so the agent is TOLD, every turn, what it is bound by — an agent that knows the rule does not spend a turn discovering it by refusal.
Who may make a rule: two tiers
TIER 1 — tighten-only, no approval. Any agent may declare rules that only RESTRICT (refuse or ask) and only bind ITSELF and the subagents it filed an action for. Effective immediately, audited, retireable by the declarer or the owner. A tier-1 rule can never loosen, override or lock anything — declare refuses each of those by name, pointing at propose.
TIER 2 — everything else needs the owner. An allow rule, a rule on another agent, a lock, an unlock, retiring a rule the agent did not declare, a rule outside its own scope: policy.propose writes an INERT pending row and files a decision on the owner's board (adopt · adopt with edits · refuse — a picker with exactly one recommendation, rung by the doorbell). The rule activates only when policy.ratify is called under the owner's own credential — a key whose scope includes owner, minted only by a human sign-in. Never by author slug: every session today shares one tenant key, and a slug can be typed by anyone.
Rules the owner ratifies are locked by default. Unlock is a further owner ruling that names the rule (policy.unlock, a new row, audited). until expires a rule with no ruling at all. And there is one kill switch: policy.freeze — an owner ruling that suspends every tier-1 rule at once, its audit row naming each rule it suspended.
The doors
door | what it does | who |
|---|---|---|
| evaluate one call; audit a refusal or an ask | anyone |
| declare a tier-1 rule — tighten-only, binds the declarer and its own subagents | any agent |
| write an inert rule and file the owner's decision | any agent |
| activate a pending rule under the owner's ruling | owner credential |
| retire a rule (a retirement row) | the declarer, or the owner |
| lift a lock, naming the ruling | owner credential |
| suspend every tier-1 rule, naming each | owner credential |
| read the refusals and asks | anyone |
| read the rules that bind one actor, matched on subject exactly as | anyone |
| read every rule in force, optionally narrowed to a scope | anyone |
What this is not
It is not a sandbox and it does not contain a process. An agent with a shell and no hook can run anything the operating system lets it run; what the policy door guarantees is that every door brainy serves, and every tool routed through an adapter, is checked against rules the agent cannot forget, edit or outrun — and that every refusal leaves a row saying what was stopped, for whom, and under whose authority.