The unified per-target gate¶
This is where "one gate model" becomes literal. Every action — a Spawn verb from
executor A or a curl from executor B — resolves to allowed / needs-my-approval
/ denied through one function, evaluate_gate, over one per-user rule store. A
per-(target, action) operator rule can tighten but never loosen the hard floors
the platform already enforces — and that "never loosen" is not a policy promise but a
structural property of the composition math.
It unifies; it does not add another engine¶
There are already several gate systems in the platform. The unified gate does not add another one — it defines a single composition so they all compose by most-restrictive-wins, and adds the one genuinely-missing piece: operator-customizable per-(target, action) rules, stored per user ("N boxes, N rule sets").
| Layer | System | What it decides |
|---|---|---|
| L0 | Estate hard floor | prod/irreversible classifier + code-enforced, forge-resistant operator markers on the box where a command runs |
| L1 | Capability / grant model | can this principal do this verb at this tier on this box — deny-by-default |
| L2 | Per-action harness policy | the safe-by-default per-tool-call disposition + argument-schema validation |
| L3 | Per-target operator rules | the operator's gate_rules disposition for this (owner, target, action) — the one net-new store |
Above these sits the escalation engine (the route a needs-approval takes) and Ward (the out-of-band notary that attests the whole thing is intact).
One evaluation model¶
Every action, from either executor, is evaluated by one entry point:
allowed→ dispatch the action.needs_approval→ route through the escalation engine (enqueue → notify → decide → execute-on-approve → resume; fail-closed on TTL; never-self-approve). One route for both executors.denied→ refuse: a 403 for a verb; a model-readable denial for a workflow (which degrades, never bypasses).
It is called at both execution planes over the same store: executor A's control surface calls it before actuating a Commander verb, and executor B's runtime calls it per tool-use before dispatch. The two planes agree because they run the same function over the same rules. Independently, the box-local code-enforced hooks (L0) remain in force where the command actually runs — defense in depth: even if the Hub-side evaluator were bypassed, the box re-checks the floor.
The action-identity vocabulary¶
The rule store and the evaluator meet on one action vocabulary, so a Spawn verb and a
curl are configured the same way:
action := { kind: spawn_verb , verb: deploy|drive|capture|status|stop|reset|… }
| { kind: mcp_tool , name: <tool name> }
| { kind: integration , verb: <connector>.<verb> }
| { kind: http , host: <pattern>, method: <pattern> }
| { kind: tier , tier: Read|Steer|Spawn|Kill|OperatorOk }
A tier rule lets an operator write one rule covering "all Kill-tier actions on box
B" without enumerating verbs — it matches any action whose resolved tier is that tier.
Every concrete action resolves to a (kind, tier) pair through the one authoritative
verb→tier map, so the rule store, the grant model, and the harness policy share one
tier axis and one ordering.
Composition: min() makes "tighten-only" structural¶
Order the three dispositions by restrictiveness:
The composition is arithmetic:
floor = min(L0, L1) # hard floor: capability + irreversible/prod
final = min(floor, L2, L3) # harness policy + operator rule can only pull DOWN
Because the composition is min, any layer can only ever lower (tighten) the
result. An operator rule (L3) of allowed is the maximum value — it can never
raise final above what the floor already permits; it is simply dominated unless
every layer already allows. An L3 of needs_approval or denied tightens. Therefore
a per-target rule can tighten but is structurally incapable of loosening a floor.
This is the central invariant made mechanical rather than merely promised.
Worked cases
- Floor says
needs_approval(a prod deploy); operator rule saysallowed→final = needs_approval. The operator cannot self-loosen prod. The UI shows "you set allowed, but the irreversible floor holds this at needs-approval." - Floor says
allowed(a dev-box drive); operator rule saysneeds_approval→final = needs_approval. The operator tightened a dev action to ask-me. Allowed. - Capability (L1) says
denied(no grant on box X) →final = denied→ the escalation path offers a one-shot elevation. An operator rule cannot pre-authorize what capability denies.
A consequence worth stating plainly: "make box A more permissive than box B" is
expressed in the grant model — grant box A the capability — plus a tightening rule
on box B. The rule store only ever carves restriction down from the floor; it is never
the place you add permission. Capability lives in the grant model; "and even if
capable, ask me / never" lives in the rule store. Keeping those two axes separate is
what makes the min-composition safe.
Honest UI: which layer is binding¶
Because the composition is a transparent min, the evaluator returns not only final
but the binding layer — which layer produced the minimum. The config UI uses this
to show the effective disposition per (target, action) and why, so a rule that is
dominated by the floor reads as "held by floor," never silently ignored. No operator
ever believes they loosened prod when they did not, and floor entries render as
read-only chips the UI refuses to offer as allowed.
Safe-by-default seed¶
A newly-registered box or workflow — and a user's default target — is seeded from a deny-by-default template, not an empty one:
- mutating Spawn verbs (deploy / stop / reset) →
needs_approval, - anything the prod/irreversible classifier flags →
needs_approval(and it can never be dropped below that), - Read-tier verbs (status / capture) →
allowed, - anything unknown or unlisted →
denied(fail-closed).
A new target is not a blank check. The operator then tightens per box, and can never seed themselves looser than the floor — that is structurally a no-op.
Specificity: which operator rule applies¶
When several gate_rules rows match one action (say box:B + tier:Kill and box:B +
verb:reset), the single most-specific rule is chosen first — exact action beats a
tier-wildcard; box:X beats commander:* beats * — and that one value is fed into
the min. Specificity resolves which operator rule applies; it never lets a specific
allowed beat the floor, because the min still dominates afterward.
Safety invariants¶
- Most-restrictive-wins, structurally —
final = min(floor, L2, L3); a per-target rule can only tighten. - The hard floor is un-loosenable — the prod/irreversible classifier, the forge-resistant operator markers, and deny-by-default capability cannot be raised by any rule row; operator-only actions always require the un-forgeable marker or a human decide.
- One evaluator, two planes, one store — executors A and B call the same function over the same rules; the box-local code-enforced hooks remain an independent backstop for the floor.
- Never-self-approve — every
needs_approvalroutes through the escalation engine; the AI proposes, only a human decides. - Fail-closed — unknown target/action → denied; an evaluator or DB fault → denied; a missing rule → the deny-by-default seed; an approval TTL → default-deny.
- Tenancy isolation — rules are keyed by owner; one user's rules can never affect another user's targets or loosen a shared floor.
- Attested — every decision and binding layer is auditable; Ward attests the store, the composition, and the floor integrity — report-only, against a signed baseline.
The unified gate ships behind a flag (HIVEMIND_UNIFIED_GATE_ENABLED) that defaults
OFF, and — per the platform's discipline — is only enabled in a live deployment after
Ward attests the composition and floor are intact.