diff --git a/agent-builder/.claude/context.md b/agent-builder/.claude/context.md index 8b9fab9..02ef6b9 100644 --- a/agent-builder/.claude/context.md +++ b/agent-builder/.claude/context.md @@ -32,13 +32,32 @@ a **dead** root token as live, into every session, for 3 days. Prune = **#192**; - **Vault: production = primary `192.168.1.88:8200`.** server-01 is only a CLIENT of it. The sandbox Vault on server-01 is `:8201`. **Port 8200=prod / 8201=sandbox is the fastest tell** — never infer "server-01 is involved, therefore sandbox Vault." That inversion caused a real near-miss. -- **Design decisions D1–D10 + D-CB1–D-CB4 are LOCKED.** The design record outranks any task title — - read `agent_builder/agent_sudo_design_decisions.md` before re-litigating. +- **Design decisions D1–D10 + D-CB1–D-CB9 are LOCKED.** The design record outranks any task title — + read `agent_builder/agent_sudo_design_decisions.md` before re-litigating. **D-CB5–D-CB9 were added + 2026-07-15** (sliding per-host trip window; append-only replayed breaker log; Set B host_override; + which tier-4 sources latch; trip escalation). -**Next session (Wed 2026-07-15), in order:** **#174** (rotate the exposed root-equivalent token AND -narrow `claude-policy` — rotation alone re-arms the hole) → **#149** (wire tier-2/3 + circuit-breaker, -with **#173** fail-open/fail-closed folded in) → **#128** secrets-proxy (remove the ntfy approval -gate) → **#147/#146** (CA P2–P5 + primary cutover 8084→8082). +**⚠️ P3 CODE IS COMPLETE BUT NOT DEPLOYED — BOTH HOSTS STILL RUN THE OLD `sudo_rules.py`.** +`app.py` + `circuit_breaker.py` + Set B override are written and green (**85 tests OK**, mutants +caught) but **UNDEPLOYED**, so **the server-01 tier-0 `breaker.log` hole is OPEN IN PRODUCTION** +until `DEPLOY_RUNBOOK.md` runs. Two **verified** deploy blockers gate it (row **187**): +`Dockerfile` never COPYs `circuit_breaker.py`/`security/` (image would crash on boot), and +`docker-compose.yml` has no `/var/lib/agent-sudo` mount (breaker resettable by `docker restart` — +the exact D-CB6 bypass). **Both were invisible to a 85/85-green suite.** + +**Deploying is TIER-4 HUMAN-ONLY by design** (D-CB7 Set B): restarting `agent-sudo-daemon` is +refused in code, because the unit env holds `SUDO_MD_VERIFY_ENFORCE` — self-restart IS the +gate-disarm primitive. An agent physically may not deploy this. That is the constraint model +working, not a gap. + +**Recall corpus (2026-07-15):** `semantic_recall.py` now indexes design docs too (`DOC_ROOTS`), so +D-CB5–D-CB9 are searchable. `context.md` is **deliberately excluded** until **#192** prunes it. +**MEMORY_DIR: do NOT repoint `embed_memory_dir.py`** — its Postgres `claude_memories` table has +**no reader** (verified). See row **188**. + +**Next session, in order:** **#187** (Phase 0 blockers → `DEPLOY_RUNBOOK.md` phases 1–3; user runs +the tier-4 steps) → **#173** fail-open/fail-closed → **#128** secrets-proxy → **#147/#146** (CA +P2–P5 + primary cutover 8084→8082). **#205** (Vault+Bitwarden security design) is THU-first. **Then:** Thu 07-16 qui/qbit memory leak (#176) + media_pipeline (#177) · Fri 07-17 clear the runway (#178) · **Mon 07-20 resume business development (#179) — digital businesses deployed by Fri 07-24.** diff --git a/agent-builder/agent_prompts.md b/agent-builder/agent_prompts.md index fe50277..bdaa377 100644 --- a/agent-builder/agent_prompts.md +++ b/agent-builder/agent_prompts.md @@ -1199,3 +1199,107 @@ Old bridge has been on 8084 hot-standby since P4's swap, 14-day retention. Cooli ## Wrap-up notes must confirm: window-clean proof (incident count = 0); what was stopped/removed per host; image retained or removed; id=176 status. ``` + +--- + +# P3-WIRING — app.py tier-2/3 + circuit-breaker (2026-07-15) — projects id=187, task #149 part B + +> **⚠️ NAME COLLISION — READ THIS.** The "P3" section ABOVE (Vault AppRole + Timeshift + AI +> evaluator) is the **OLD P3** and is **COMPLETE** (task #142). P3 was **redefined** after its +> D-CB1…D-CB9 decisions were locked on 2026-07-15. THIS is the current P3. Do not run the old one. + +**Blast radius: ZERO.** Code + tests only. No deploy, no restart, no git push. Agent-Sudo is LIVE +and ENFORCING on both hosts; editing these files does not deploy them, and it must stay that way. + +**Human-gate protocol:** deploying this code requires restarting `agent-sudo-daemon`, which D-CB7 +classifies **tier-4 / human-only on both hosts** — the Set B override exists precisely to stop an +agent doing it. The agent must therefore STOP at any human-only step and report it, not route +around it. Resume the agent afterwards with SendMessage (its context survives). + +`--max-turns 25` + +``` +You are wiring the LAST piece of Agent-Sudo P3: tier-2 and tier-3 execution plus the circuit-breaker into app.py. Narrow scope: `/opt/appdata/docker/docker-compose/agent-sudo/app.py` and its tests ONLY. You will NOT modify sudo_rules.py or circuit_breaker.py (both are DONE and their APIs are LOCKED). You will NOT deploy, restart, redeploy, or git push. + +## THE DESIGN IS LOCKED — DO NOT REDESIGN + +Read `/home/administrator/Desktop/claude/agent-builder/agent_sudo_design_decisions.md` — sections D2, D3, D5, D10 and **D-CB1 through D-CB9**. Implement exactly that. If you believe a decision is wrong, implement it as written and say so in the wrap-up `notes` — do NOT silently pick a different reading. (Two previous agents on this project each caught a real spec contradiction that way; it is the behaviour we want.) + +## Context — current state, do not re-derive + +- `app.py` is UNTOUCHED: zero `circuit_breaker` imports. The wiring site is ~lines 161–209, and the two capability stubs are at ~186–191: + if d.tier == 2 and not SANDBOX_ENABLED: -> raise HTTPException(503, 'Tier-2 sandbox pipeline not enabled on this host (requires P3)') + if d.tier == 3 and not SNAPSHOT_ENABLED: -> raise HTTPException(503, 'Tier-3 snapshot pipeline not enabled on this host (requires P3)') + `SANDBOX_ENABLED` / `SNAPSHOT_ENABLED` are read from env at ~lines 49–50. +- `sudo_rules.classify(command, server_id)` returns a Decision with `.tier`, `.source`, `.matched_rule`, `.host_override`, `.verb`, `.state`. **Preserve that contract.** Its three tier-4 sources are `danger_veto`, `miss_primary_refuse`, `host_override_setb`. +- `circuit_breaker.py` is DONE, 28/28 tests pass. **LOCKED API — do not rename:** + replay(host) -> BreakerState # ONLY source of truth; state is NEVER stored, always replayed from the log + allows(tier, host) -> (bool, str) + record_tier3_failure(host, cmd_id) -> BreakerState + record_tier4_attempt(host, cmd_id) -> BreakerState # ALWAYS attack-latches + reset(host, actor) -> BreakerState + classify_trip(signal) -> str # D10 seam + Constants: BENIGN_FAIL_THRESHOLD=3, BENIGN_WINDOW_SECONDS=900, BASE_COOLDOWN_SECONDS=600, MAX_COOLDOWN_SECONDS=14400, TRIP_ESCALATION_WINDOW_SECONDS=21600, TRIP_ESCALATION_THRESHOLD=4. +- Existing security modules already present in `agent-sudo/security/`: `timeshift.py` (raises `PrimaryRestoreForbidden`), `scoped_undo.py` (`capture_or_refuse`), `evaluator.py` (`_append_proposed_rule`, propose-only — no activation path), `sudo_sign.py`, `vault_setup.py`. READ them before wiring; do not reimplement what exists. + +## D-CB8 IS THE LOAD-BEARING SUBTLETY — GET THIS RIGHT + +`record_tier4_attempt()` ALWAYS attack-latches. But app.py reaches tier 4 from THREE sources, and `miss_primary_refuse` is the DEFAULT for any unlisted command. If every tier-4 latched, one typo would take the host read-only until a human reset — verified against the 2026-07-15 session, which refused every privileged command with `source=miss_primary_refuse` and would have latched on the first one and stayed latched all day. + +Therefore: **ONLY `danger_veto` and `host_override_setb` call `record_tier4_attempt()`. `miss_primary_refuse` is AUDIT-ONLY — log it to command_audit, refuse the command, but do NOT touch the breaker.** The seam holds because only the CALLERS change, never the primitive. + +## Contract — what must be true when you finish + +1. app.py imports circuit_breaker and consults `allows(tier, host)` BEFORE executing anything. If denied, refuse with a clear error naming the breaker state + cause; do not execute. +2. Tier-2 routes through the sandbox-test path when `SANDBOX_ENABLED`, else the 503 stub stays. Tier-3 routes through the snapshot/scoped-undo path (`scoped_undo.capture_or_refuse`, `timeshift`) when `SNAPSHOT_ENABLED`, else the 503 stub stays. **The stubs must remain the behaviour when the capability is off** — this is what makes the change safe to land undeployed. +3. A tier-3 execution FAILURE calls `record_tier3_failure(host, cmd_id)`. +4. Tier-4 from `danger_veto` or `host_override_setb` calls `record_tier4_attempt(host, cmd_id)`. Tier-4 from `miss_primary_refuse` does NOT. +5. `timeshift.PrimaryRestoreForbidden` must still be honoured — never auto-restore primary. +6. Every decision + outcome is logged to `command_audit` (D2/D3: this is the local model's training data — decision_type is one of 'rule_match' | 'sandbox_test' | 'ai_eval_proposal' | 'execution' | 'circuit_breaker' | 'tier4_refuse'). +7. All existing app.py behaviour for tier 0/1 is unchanged. + +## Step-by-step + +1. Read app.py in full, then circuit_breaker.py, sudo_rules.py, and the security/ modules. Read the design doc sections named above. +2. Wire the contract above. Match the existing code's style and error-handling idiom. +3. Write/extend `test_app.py` (create if absent; if it exists, ADD and keep all existing tests green). Must cover: breaker denies before execution; tier-3 failure records; **danger_veto latches**; **host_override_setb latches**; **miss_primary_refuse does NOT latch** (the D-CB8 test — most important); 503 stubs still returned when capabilities are off; tier 0/1 unaffected. +4. Run `cd /opt/appdata/docker/docker-compose/agent-sudo && PYTHONDONTWRITEBYTECODE=1 python3 -m unittest test_app -v` AND re-run `test_sudo_rules` + `test_circuit_breaker` to prove no regression. Paste all three verbatim summary lines in the wrap-up. +5. **Mutation-check your own suite** (both previous agents did this and it caught real gaps): break the D-CB8 branch so `miss_primary_refuse` latches, confirm the suite FAILS, restore, confirm it passes. A test that cannot fail is not evidence. +6. Write `/opt/appdata/docker/docker-compose/agent-sudo/DEPLOY_RUNBOOK.md` — the exact commands the HUMAN must run to deploy, per host, in order, with the rollback command for each and how to verify success. This is a deliverable, not an action: do NOT run any of it. + +## HUMAN-GATE PROTOCOL — how to stop (this is a feature, not a failure) + +Some steps are human-only BY DESIGN (D-CB7 Set B): restarting/stopping `agent-sudo-daemon`, editing its unit file, editing SUDO.md, writing breaker.log, and the primary port swap (#146). You must NOT attempt these, work around them, or stall waiting. + +If you hit one, STOP IMMEDIATELY and emit the wrap-up with `status="partially_succeeded"` and populate `human_action_required`. The human will perform it and RESUME you — so `notes` must contain exactly what you were doing and what you need re-checked on resume. + +## MANDATORY WRAP-UP (required regardless of success or failure) + +Before stopping for ANY reason — task complete, error, human gate, or approaching turn limit — output this JSON as your final message. Do not stop without it. + +{ + "status": "succeeded|partially_succeeded|failed", + "actions_taken": ["action 1 — outcome"], + "actions_failed": ["action — reason"], + "human_action_required": [ + {"server": "primary (192.168.1.88) | server-01 (192.168.1.90)", + "command": "exact command to run", + "why": "why an agent may not do this", + "verify": "how the human confirms it worked"} + ], + "notes": "verbatim unittest summary lines for test_app, test_sudo_rules, test_circuit_breaker; verbatim mutant-run summary; confirmation that miss_primary_refuse does NOT latch while danger_veto and host_override_setb DO; where DEPLOY_RUNBOOK.md was written; exactly where to resume you if paused; anything ambiguous in the locked spec and what you chose" +} + +`human_action_required` MUST be `[]` when empty — never omit the key. + +## Constraints + +- ONLY `app.py`, `test_app.py`, `DEPLOY_RUNBOOK.md`. Nothing else. +- Do NOT modify sudo_rules.py or circuit_breaker.py. Do NOT deploy, restart, redeploy, or git push. +- stdlib + the project's existing deps only. +- Do NOT create `/var/lib/agent-sudo/breaker.log` on the host. +- Use `PYTHONDONTWRITEBYTECODE=1` when running python (a `__pycache__` permission error previously masqueraded as a syntax error). + +--max-turns 25 +If you issue the same tool call or command twice with identical arguments, STOP immediately and output the mandatory wrap-up with status=partially_succeeded. +``` diff --git a/agent-builder/agent_sudo_design_decisions.md b/agent-builder/agent_sudo_design_decisions.md index 6148cd6..83edd8b 100644 --- a/agent-builder/agent_sudo_design_decisions.md +++ b/agent-builder/agent_sudo_design_decisions.md @@ -132,3 +132,149 @@ Write the **phased** background-agent prompt SET (NOT one monolith — see reaso - P5: decommission old sudo-bridge. Opus+high prompt-craft upgrades over the Sonnet-era prompts (agent_prompts.md): bake in the 4-level test gate + agent_test_results logging (Sonnet's omitted it); split locked-contract from reference-implementation instead of embedding brittle literal scripts; right-size per-phase turn budgets; explicit "design is locked — do not redesign" block; per-phase rollback/abort criteria + idempotent pre-flight checks. + +--- + +## CIRCUIT-BREAKER — REMAINING DECISIONS LOCKED (2026-07-15, grill-me Q3/Q4/Q5) + +These close the last three open questions on task #149. D-CB1..D-CB4 (2026-07-14) fixed the +breaker's *concept*; these fix its *numbers, storage, and self-protection*. **#149 is now +design-complete — an implementing agent must NOT redesign any of this.** + +### D-CB5 — LOCKED: benign-trip threshold = 3 tier-3 failures / 15-min SLIDING window / PER-HOST +- **Sliding, not fixed.** A fixed window lets failures straddle the boundary (2 at the end of + window 1 + 2 at the start of window 2 = 4 failures, no trip). Sliding closes that. +- **Per-host, not global.** The hosts have different roles (primary = production, server-01 = + sandbox); a sandbox failure burst must not disarm production tier-3. Decisive reason: a global + counter needs cross-host state, which recreates the **#173 failure class** (server-01's safety + depending on primary ⇒ drop primary to disarm server-01). Per-host adds no new cross-host edge. +- **Known, accepted cost:** an attacker probing BOTH hosts gets 2× budget before a benign trip. + Accepted because probing is a **tier-4 signal**, which latches hard via D-CB1 regardless of this + counter. The benign counter only ever governs broken scripts — and broken scripts are per-host. +- Threshold governs the BENIGN path only. Blast radius + re-arm still per D-CB1/D-CB2: + benign ⇒ stop **tier-3 only**, auto re-arm after cooldown w/ exponential backoff on repeat; + attack (any tier-4 attempt, or repeated trips inside one window) ⇒ **full read-only, human-only + reset, NTFY urgent.** + +### D-CB6 — LOCKED: breaker state = LOCAL APPEND-ONLY EVENT LOG, state DERIVED by replay +`/var/lib/agent-sudo/breaker.log` (0600 root, append-only), one JSON event per line: +`{ts, host, event: tier3_fail|TRIP|REARM|RESET, cause: benign|attack, scope: tier3|readonly, cmd_id}` + +- **State is never stored — always `replay(log)`.** Consequence that matters: a daemon restart + replays the log and the breaker is STILL TRIPPED. In-memory state would make + **restart = free reset = bypass** (the trap Q4 existed to catch). +- **Local only. No Vault, no Postgres.** Both live on primary ⇒ server-01 would depend on a remote + host for its own local safety state, and Vault-sealed/pg-down would mean *no breaker at all*. + Same #173 class as D-CB5's global-counter rejection. State must survive restart AND not add a + cross-host edge; local storage is the only option meeting both. +- **Append-only over a mutable `breaker.json` — because the daemon is root.** Root can rewrite any + local file, so "tamper-proof" is theater ([[feedback-autonomous-security-constrain-not-gate]]). + A mutable state file can be silently set back to `armed` leaving no trace. An append-only log + cannot be *edited* into a lie, only **truncated** — and truncation is **evident** (log shrinks, + audit trail has a hole). This converts *impossible-to-tamper* (false) into *tampering-is-visible* + (true and enforceable). Constrain, don't gate. +- Free wins: trip/reset events are labelled **training data**; and the replay boundary is the **D10 + seam** letting Hermes assume trip-classification/reset later without a rewrite. + +### D-CB7 — LOCKED: self-cannibalization ⇒ host_override TIER-4 in CODE, BOTH hosts, whole Set B surface +Found live during #145: `systemctl restart agent-sudo-daemon` routed THROUGH Agent-Sudo has the root +daemon kill its own executor mid-request — post-exec verify/audit can never complete. **The command +destroys its own witness.** Structural; `SNAPSHOT_ENABLED` does not touch it. + +Refuse in code (a `host_override`, NOT merely a SUDO.md rule) on **both** hosts: +- `systemctl restart|stop|disable|mask agent-sudo-daemon` +- edits to `/etc/systemd/system/agent-sudo-daemon.service` +- writes to `agent-sudo/bridge/SUDO.md` +- writes to `/var/lib/agent-sudo/breaker.log` ← **new Set B member created by D-CB6** + +- **Why the whole surface, not just `restart`:** the reason restart is forbidden is D-CB4 **Set B** — + the unit env is where `SUDO_MD_VERIFY_ENFORCE` lives, so self-restart *is* the gate-disarm + primitive. But `stop`/`disable`/`mask`/unit-file-edit all reach the same place, and D-CB6's log + makes "reset the breaker" a one-line write. Blocking only `restart` blocks one door in an open frame. +- **Why code-level, not a SUDO.md rule:** *a rule that lives in the file it protects is not a + constraint, it is a suggestion.* (SUDO.md is signed, but host_override must not depend on the + gate it exists to protect.) +- **Why both hosts (rejecting tier-4-primary/tier-3-server-01):** server-01 is where Agent-Sudo + changes get PROVEN. A gate behaving differently in test than in prod means the sandbox stops + predicting production — the one host that proves the gate would be the one host not running it. +- **Known, accepted cost:** nobody can restart the daemon through Agent-Sudo, including for + legitimate deploys — a human needs a shell. That is **Set B working as designed**: human-only + *even though fully recoverable*, because automating it removes the ceiling. Per D6 (minimize, not + eliminate) this is a *near-zero-frequency* touchpoint, not a stall. + +**XREF:** #149 (impl), #146 (primary cutover), #173 (same cross-host class — still OPEN, and +D-CB5/D-CB6 both deliberately avoid adding to it), D-CB1/D-CB2 (benign-vs-attack drives both blast +radius and re-arm), D-CB4 Set B, D9, D10, CA-D7. + +### D-CB8 — LOCKED (2026-07-15): WHICH tier-4 sources are attack signals (closes D-CB1's open sub-decision) +D-CB1 says "any tier-4 attempt => attack latch" and explicitly left open: *"each trip signal must be +classified benign-noise vs attack at design time."* THIS IS THAT DECISION. It is load-bearing: +`app.py` reaches tier-4 from THREE different sources and **one of them is routine operation.** + +| tier-4 source (`d.source`) | breaker signal | why | +|---|---|---| +| `danger_veto` | **ATTACK LATCH** (read-only) | matched a DANGER_PATTERN — attack-shaped | +| host_override (D-CB4 **Set B**) | **ATTACK LATCH** (read-only) | an attempt to disarm the gate itself | +| `miss_primary_refuse` | **NONE** (audit only) | **just an unlisted command** | + +**Why `miss_primary_refuse` must NOT latch:** on primary it is the *default outcome for anything not +on the allowlist* — it is the system working, not an attack. Taken literally, D-CB1 would make every +typo and every new tool take the host to FULL READ-ONLY pending a human reset. **Verified against +reality:** during the 2026-07-15 session Agent-Sudo correctly refused every privileged command with +`source=miss_primary_refuse`; under the literal rule the breaker would have attack-latched on the +FIRST one and stayed latched all day. That is a false-positive machine and directly contradicts D9 +("if the user is unavailable, automation must not stall"). + +**Rejected — "repeated misses trip BENIGN" (3-in-15min via the D-CB5 counter):** attractive because +rapid-fire misses resemble enumeration, but normal exploratory work produces exactly that shape, so +it re-imports the false-positive problem one layer down. A miss is not evidence of intent. +**Rejected — "only host_override latches, danger_veto trips benign":** a real destructive attempt +would then keep write access. Danger patterns catching fat-fingering as often as malice is fine — +the response (stop writing) is correct for BOTH. + +**IMPLEMENTATION NOTE — the seam holds, no breaker change needed.** `circuit_breaker.record_tier4_ +attempt()` stays correct as a primitive that ALWAYS attack-latches. D-CB8 governs only its CALLERS +in `app.py`: call it for `danger_veto` and host_override; for `miss_primary_refuse` emit the +existing `tier4_refuse` audit row and NO breaker event. Classification lives behind +`classify_trip()` (the D10 seam) so Hermes can refine this later without a rewrite. + +**[[feedback-instrument-the-gate]] applies:** log every breaker decision WITH its reason and read the +log on a schedule. If `miss_primary_refuse` bursts turn out to correlate with real probing, D-CB8 can +be revisited **on logged evidence** — not on imagination. Widen/tighten on evidence. + +### D-CB9 — LOCKED (2026-07-15): escalation = 4+ benign trips / 6h sliding (fixes an incoherent constant pair) +**The bug this fixes was in the SPEC, not the code**, and it was caught by the implementing agent +rather than by me. As first specified: `BASE_COOLDOWN_SECONDS`=10min but +`TRIP_ESCALATION_WINDOW_SECONDS`=1h with an implicit 2-trip threshold. Since re-arm happens at 10min, +**any repeat benign trip is NECESSARILY inside the 1h window** ⇒ every repeat escalates to an attack +latch ⇒ **the exponential backoff (20/40/80min) is unreachable dead code.** Two explicitly-required +behaviours (backoff; escalate-on-repeat) were mutually unsatisfiable at those constants. + +**LOCKED VALUES:** +- `BASE_COOLDOWN_SECONDS = 10 * 60` +- `MAX_COOLDOWN_SECONDS = 4 * 60 * 60` +- `TRIP_ESCALATION_WINDOW_SECONDS = 6 * 60 * 60` (was 1h) +- `TRIP_ESCALATION_THRESHOLD = 4` (was implicitly 2) + +Benign trips 1→2→3 stay benign with doubling cooldown (10 → 20 → 40 min); the **4th within a 6h +sliding window** attack-latches. Two counters, deliberately different: **escalation** counts benign +trips inside the 6h window; **backoff** counts benign trips since the last `RESET`. + +**WHY 4/6h rather than latching on trip #2 — the key insight:** *exponential backoff IS the +anti-"attacker waits out the cooldown" mechanism.* 10→20→40→80min rate-limits a patient attacker +automatically, with no latch required. Meanwhile a broken cron job emits an IDENTICAL signal to a +patient attacker, and the breaker cannot tell them apart (same ambiguity as D-CB2). So latching on +the 2nd trip mostly punishes broken scripts and re-imports the false-positive problem D-CB8 just +removed. **A latch should mean "this is not resolving", not "you failed twice."** +- Rejected — *raise BASE_COOLDOWN above the 1h window*: makes backoff reachable, but one broken + script then kills tier-3 for 90+ min. Slow recovery fights D9 (automation must not stall). +- Rejected — *drop escalation, backoff only*: simplest and most autonomous, but a permanently broken + system would quietly self-throttle to the 4h cap forever and never ask for a human. A latch is how + the system says "I cannot fix this myself." + +**PROCESS NOTE — worth keeping:** the constants were set casually mid-session to avoid stalling on a +question, and were not checked against each other. The implementing agent flagged the contradiction +instead of silently picking a reading that made its tests pass. That is the behaviour we want from +build agents, and it is why the "if you think a decision is wrong, implement it anyway but say so" +instruction is load-bearing — a compliant-but-silent agent would have shipped unreachable backoff and +nobody would have known until an incident. XREF [[feedback-instrument-the-gate]].