Skip to content

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:

evaluate_gate(principal, owner, target, action, params)
    → allowed | needs_approval | denied
  • 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:

denied (0)  <  needs_approval (1)  <  allowed (2)

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 says allowedfinal = 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 says needs_approvalfinal = 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

  1. Most-restrictive-wins, structurallyfinal = min(floor, L2, L3); a per-target rule can only tighten.
  2. 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.
  3. 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.
  4. Never-self-approve — every needs_approval routes through the escalation engine; the AI proposes, only a human decides.
  5. 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.
  6. Tenancy isolation — rules are keyed by owner; one user's rules can never affect another user's targets or loosen a shared floor.
  7. 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.