Skip to content

The unified per-target gate

This is where "one gate model" becomes literal. A Spawn verb from executor A (the Commander control surface) or a curl from executor B (the workflow harness) 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.

Not literally every action yet. A human confirming a model's own proposal (orchestration_authz::confirm) is a third action path that predates this gate and is not wired into it — that route's disposition is decided entirely by the pre-existing confirmation flow, not by evaluate_gate. If you are trying to observe this gate's effect by flipping the flag and then confirming a proposal, you will see no change, correctly: that path never consults it, flag on or off.

Two more gaps behind the flag, not just 'inactive'

Flipping HIVEMIND_UNIFIED_GATE_ENABLED on is not a no-op waiting to be activated later — it changes real behavior today, and not always the behavior this page describes above:

  • Executor A does not route needs_approval to the escalation engine yet. routes::control::unified_gate_consult hard-denies (a flat 403) on ANY disposition other than allowed, including needs_approval — the code's own comment names this "the documented flag-on follow-up," not yet built. Only executor B's workflow harness currently distinguishes a queued approval from a denial.
  • On a deployment that hosts the control listener, an instance with no configured gate_rules is not "no-op until configured." L3's seed disposition is needs_approval for every Steer/Spawn/Kill verb class (gate_rules::seed) — combined with the point above, turning the flag on with zero per-target rules set up will start returning a flat 403 to every Spawn, Nudge, Kill, and Reset call through executor A immediately, not once an operator gets around to writing rules. The compose product does not host that listener, so enabling the flag there produces no 403 at all: executor A is not reachable in the first place.

Status

The unified gate is merged on main but dark behind a default-OFF flag (HIVEMIND_UNIFIED_GATE_ENABLED) and is not activated in production. The server now logs this flag's posture once at boot, the same as every other dark-launched flag in this crate — check for a feature ENABLED / feature disabled line naming unified gate before assuming a value in .env reached the process. Building and testing against it is dev-safe; turning it on is an operator-only act. Read the composition below as designed and merged, not as generally available or fully wired end-to-end.

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.

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 allowed → 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 says needs_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

  1. Most-restrictive-wins, structurally — final = 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. Auditable — every decision records the layer that bound it, so a disposition can be traced to the specific floor, grant, policy or rule that produced it, and the decision lands on the hash-chained ledger.

The unified gate ships behind a flag (HIVEMIND_UNIFIED_GATE_ENABLED) that defaults OFF. Enabling it in a live deployment is a deliberate, separate operator act, taken after the deep security review of the surface it governs — it is not switched on by an upgrade.