ADR 0010: Rollback / verify pairing for the confirmation broker (BRK-2)¶
- Status: Proposed
- Date: 2026-07-15
Context¶
ADR 0009 added an opt-in, default-off
two-step confirmation broker for Tier-3 (IRREVERSIBLE) operations: a
Tier-3 call returns a single-use, TTL-bounded token instead of running, and a
distinct operation_confirm(token) arm step plus a re-issue of the exact same
call is required to execute. ADR 0009 explicitly deferred the other half of
the sibling brokers it drew from — pairing an operation with a rollback
command, a post-execution verify command, and an auto-rollback on
verify failure — as a materially larger surface needing its own ADR
(runbook §7.1, BRK-2). This is that ADR.
The pattern, as the sibling planes implement it: a destructive action is
submitted together with (a) a verify_command that checks the system reached
the intended state and (b) a rollback_command that undoes the action; after
execute, the broker runs verify, and if auto_rollback is set and verify
fails, runs rollback — a small orchestrated control loop of up to three
commands under one authorization.
The problem it would solve for relay-shell: an unattended / autonomous
Tier-3 operation where no human or second agent is watching to react if the
operation half-succeeds. A migration that leaves a service down, a
dd/mkfs/rm that partially applies — today the model that issued it must
itself notice and remediate. Rollback/verify binds the remediation to the
operation up front, so it runs even if the issuing turn never follows through.
Weighed honestly, that value is narrow, and the cost is not. Everything below is why this ADR's decision is to specify the design but keep it deferred, with explicit triggers that would move it to Accepted.
Decision¶
Defer BRK-2. Do not build the rollback/verify pairing now. Keep this ADR
Proposed as the design of record, and adopt it (status → Accepted, with an
implementing PR) only when a trigger below is real. When built, it must
satisfy the invariants below — they are the reason it is not a small change.
Triggers that would justify building it¶
Adopt when at least one is concretely true, not hypothetical:
- A supported autonomous / unattended deployment exists (a scheduled or agent-driven run with no human and no second agent in the loop to react to a failed Tier-3 op) — the one setting where binding remediation up front beats letting the issuing turn remediate.
- Operators ask for operation-bound remediation that survives the issuing turn (an audited guarantee that "if this migration's verify fails, this rollback runs") that the model sequencing three calls itself cannot provide.
- A post-incident review shows a real half-succeeded Tier-3 operation whose blast radius a bound auto-rollback would have contained.
Absent a trigger, the marginal value over what already ships is low: a model in control can already sequence operation → verify → rollback as three ordinary, individually policy-checked, individually audited tool calls. The only thing the broker adds is that the remediation is bound and automatic — which matters exactly and only when the issuing actor will not do it, and an actor that issued a destructive op and refuses to remediate is the residual-risk attacker ADR 0002/0009 already state is out of scope. Building the machinery before a trigger spends real surface for a benefit the current design already covers in the attended case.
Invariants any implementation MUST satisfy¶
If a trigger fires, the pairing is admissible only if it holds all of these. They are the design constraints, and collectively the reason this is a v2, not a parameter:
-
Opt-in, default-off, byte-identical when off. Gated behind its own flag (e.g.
RELAY_SHELL_CONFIRM_ROLLBACK, requiringRELAY_SHELL_CONFIRM_TIER3). When off, no new audit fields, no behavior change — the same promise ADR 0006/0007/0009 keep. -
Rollback and verify are themselves fully policy-gated commands. They are commands, not trusted broker internals. Each MUST pass through the same central
Relay.runadmission — deny list first, mode check, tier classification — as any other call. A rollback that is itself Tier 3 (undoingrmby restoring from backup could be) is classified and admitted as Tier 3. No path may let a rollback/verify string skip the deny list; that would turn the remediation channel into a policy-bypass primitive. This is the single most important invariant and the hardest to get right. -
Bound into the confirmation identity. ADR 0009 binds a token to
sha256(tool \0 op_key)whereop_key = policy_text \0 canonical(audit_args). The rollback and verify command text MUST fold into that binding (extendop_key), so a token armed for(op, rollback_A, verify_A)cannot execute withrollback_B— the same confused-deputy defense ADR 0009's BRK-3 follow-up established for SSH identity. The commands must be fixed at plan time and surfaced in theconfirm_planaudit record, never swappable at execute. -
Distinct, complete audit records. Execute, verify, and rollback are three separate audited operations, each with its own tier, exit code, and hashed output (ADR 0007), distinguished by new
actionvalues (confirm_executeunchanged; addconfirm_verify,confirm_rollback). The optional-field discipline (written only when non-empty) keeps default-off byte-identical. An auto-rollback that fires is the loudest possible line in the trail, never a silent correction. -
Bounded, terminating, non-recursive control loop. At most one verify and one rollback per confirmed operation. Rollback failure does not trigger a further rollback. Verify/rollback carry their own timeouts (clamped to
max_timeoutlike every executor) so the loop cannot hang the runner, and a rollback is never itself gated behind a new confirmation token (that would deadlock the remediation on a second arm step). -
Fail-safe on partial state. If the process dies mid-loop (after execute, before verify/rollback), the system is in the post-execute state with the plan on the audit trail — the same fail-safe posture as a bare ADR 0009 execute. No persisted rollback promise (a persisted "run this command later" is a replay/again-execution surface strictly worse than the ephemeral token ADR 0009 already argued for).
-
Verify defines failure explicitly. "Verify failed" must be a precise, documented predicate (non-zero exit, or an operator-supplied expected-output match) — not a heuristic — because it is the trigger for executing another command automatically. Ambiguity here is a footgun that auto-runs a rollback on a spurious signal.
Shape (illustrative, not binding until built)¶
Under the trigger, the least-surface realization is an extension of
operation_confirm, not new top-level tools: the arm step optionally carries
verify_command, rollback_command, and auto_rollback, which the broker
stores against the token (folded into op_key per invariant 3). On the bound
re-issue, the runner executes the main op (confirm_execute), then — if
supplied — runs verify via the same central runner (confirm_verify), and on a
failing verify with auto_rollback set, runs rollback (confirm_rollback).
Keeping it on operation_confirm preserves ADR 0009's "central gate, no
per-wrapper param" property and adds no new tool to the contract.
Consequences¶
- If deferred (this ADR's decision): no code, no new tool, no audit-shape change. The backlog item (runbook §7.1 BRK-2) points here for the design and the triggers; the vague "needs an ADR" is replaced by a concrete decision-with-conditions. Reviewers and operators have a written answer for "why doesn't the broker roll back?" — it can, the design is specified, and the cost/benefit says wait for a trigger.
- If later adopted: a new opt-in flag, up to two new
actionvalues, an extendedoperation_confirm, broker state carrying the bound commands, and the runbook §3.3 security-sensitive invariant list gains the seven above. The tool contract count is unchanged (extension, not a new tool). ADR 0009's validation battery is re-run plus new cases: verify-fail→auto-rollback, rollback-is-Tier-3-and-still-policy-gated, deny-list-still-blocks-a-rollback, token-bound-to-the-rollback-text, and process-death-mid-loop fail-safe. - Trust boundary unchanged either way. Like ADR 0009 this is a compensating control layered on ADR 0003 classification; it does not move the ADR 0002 boundary, and (invariant 2) it must never become a way around the deny list.
Rejected alternatives¶
- Build it now, alongside ADR 0009. ADR 0009 explicitly rejected this to land the confirmation gate cleanly first; nothing since has produced a trigger, so bundling it would add the largest-surface broker feature speculatively. Rejected until a trigger is real.
- Rollback/verify as free-form broker-executed strings, admitted once with the main op. Lets the remediation commands skip their own deny-list/tier check (they'd ride the main op's single admission). This is the tempting shortcut and it is a policy-bypass hole — it violates invariant 2. Rejected.
- New top-level
rollback_command/verify_commandtools. More surface, and it invites callers to run rollback/verify unbound from any operation (just more Tier-N tools), which is simply the model sequencing calls it can already do — with none of the binding that is the feature. Rejected in favor of extendingoperation_confirm(invariant/shape above). - Persist the rollback promise across restarts. A stored "run this later" is a re-execution/replay surface; ADR 0009 already argued ephemeral, process-local state is safer. The fail-safe (invariant 6) is post-execute state plus the audit trail, not a durable rollback queue. Rejected.