Fail-Open vs. Fail-Closed — the local execution gate's reliability posture
Mergen sits inline in front of every tool call. That makes reliability a first-class safety property: a gate that misbehaves must not silently stop enforcing (fail-open) and must not permanently brick a developer's terminal (fail-closed with no escape). This document states, precisely, what the gate does in every failure mode, and how an operator recovers from a broken gate.
TL;DR — The gate is fail-closed on every internal fault (a fault resolves to BLOCK/HOLD, never a silent PASS). The bounded, operator-only, fully-audited panic bypass is the deliberate release valve when fail-closed itself becomes the problem.
Principle
A fault must never become a silent PASS. When the gate cannot prove a call is safe — because evaluation errored, the policy file is corrupt, or the gate's own liveness is unproven — it fails closed (BLOCK, or HOLD for human review). The only way a call passes without a real enforcement decision is when an operator has explicitly activated the panic bypass, and then every such pass is loudly audited.
This preserves the core guarantee ("no probabilistic guardrails, no silent enforcement downgrade") even when Mergen itself is the thing that broke.
Behavior in every failure mode
| Failure | Behavior | Where |
|---|---|---|
| Policy evaluation throws (malformed rule, unexpected input) | Fail-closed → BLOCK, logged, pipeline_block audit entry |
applyGate outer try/catch (tool-guard.ts) |
Gate heartbeat stale/dead (MERGEN_REQUIRE_GATE_HEARTBEAT) |
Fail-closed → BLOCK | assertGateHeartbeatFresh (gate-heartbeat.ts) |
enterprise-policy.json fails HMAC / schema validation |
Fall back to DEFAULT_ENTERPRISE_POLICY (still enforcing), policy_integrity_failure audit entry |
loadEnterprisePolicy (enterprise-policy-engine.ts) |
| Script/command can't be structurally parsed | Fail-closed → HOLD (obfuscation-as-signal, script-trust) | tool-guard.ts, shell-ast.ts |
| Gate never installed on the MCP server | Server refuses to boot | assertGateCoversRegisteredTools |
| Operator activates the panic bypass | Fail-OPEN → PASS, every call panic_bypass-audited |
panic-bypass.ts + passUnderPanic |
The only fail-open path is the last row, and it requires a deliberate, bounded, audited operator action.
Why fail-closed by default
An agent with write access to prod is the threat model. If the gate silently stopped enforcing whenever it hit a bug, an attacker (or a confused agent) would be incentivized to induce that bug. Fail-closed removes that incentive: breaking the gate stops work, it doesn't unlock it.
The cost of fail-closed, and the fix
Fail-closed has a real failure mode of its own: a bad policy edit, or a gate bug, can BLOCK every command and halt the developer until Mergen is fixed or restarted. That is exactly what the panic bypass exists to release — without abandoning the audit trail.
The panic bypass
An operator-only, time-bounded, fully-audited liveness escape hatch.
While active, the gate passes every tool call without enforcement, but records
each one as a loud, hash-chained panic_bypass entry in the Agent Blunder Log
noting what the policy would have done.
mergen-server panic on --minutes 15 --reason "bad policy edit blocking all commands"
mergen-server panic status
mergen-server panic off
Ephemeral/containerised alternative (no persisted file):
MERGEN_PANIC_BYPASS_UNTIL=$(( $(date +%s%3N) + 900000 )) # epoch-ms, 15 min out
MERGEN_PANIC_BYPASS_REASON="incident-1234"
Guarantees
- Bounded. Every window has an expiry; the TTL is clamped to a hard maximum (4h) so it can never be left on indefinitely by mistake. An expired window reads as inactive and its stale file is cleaned up.
- Loud. Activation logs at error level. Every bypassed call is a
hash-chained
panic_bypassaudit entry — visible inGET /agent-blunders, the SIEM stream, and evidence-pack exports — so any action taken while enforcement was off is fully reviewable after the fact. - Checked first. The bypass is evaluated before the heartbeat fail-closed and every policy check, so it can un-brick a gate whose evaluation itself is the fault (the "would have" verdict is best-effort and wrapped — a broken evaluator can't re-brick the call it's meant to release).
- Fails closed on ambiguity. A missing or corrupt panic file reads as inactive (enforce), never active. Absence of a proven bypass means enforce.
Threat-model honesty
The panic bypass is not a security boundary against a same-user process — an agent with the developer's shell can already write the panic file or set the env var, exactly as it could kill the Mergen process. That is acceptable because the bypass hides nothing: its entire contract is "trade enforcement for liveness, temporarily, in exchange for a complete audit trail." Abuse is loud, not silent. A hard security boundary against a hostile same-user process requires OS-level privilege separation, which is out of scope for the Layer-1 local gate (see the Inside-Out progression in the project README).
Operator runbook: "the gate is blocking everything"
- Confirm it's the gate:
mergen-server panic status(is it already on?), check recentGET /agent-blundersfor a flood ofpipeline_block/policy_integrity_failure. - Release the developer:
mergen-server panic on --minutes 15 --reason "<what happened>". - Fix the root cause — usually a bad
~/.mergen/enterprise-policy.jsonedit (mergen-server policy-rollback) or a stale heartbeat (restart Mergen). - Re-enforce:
mergen-server panic off. - Review what got through while off:
GET /agent-blunders?type=panic_bypass.