docs(agent-sudo): lock D-CB5-D-CB9, add the P3-WIRING agent prompt, record P3 state

- agent_sudo_design_decisions.md: D-CB5 (3 tier-3 fails / 15min sliding / per-host),
  D-CB6 (append-only breaker log, state always replayed — tamper-EVIDENT beats
  tamper-PROOF when the daemon is root), D-CB7 (Set B host_override in code),
  D-CB8 (which tier-4 sources latch), D-CB9 (trip escalation). Each with rationale,
  rejected alternatives, and accepted costs.
- agent_prompts.md: add the P3-WIRING prompt. Flags a real NAME COLLISION — the
  existing "P3" section is the OLD P3 (Vault AppRole + Timeshift + evaluator, done
  as #142); P3 was redefined after D-CB1-D-CB9 were locked. Running the old one
  would redo finished work. Today's inline agent prompts were never persisted, which
  is why this one is.
- context.md: P3 code complete but UNDEPLOYED — the server-01 tier-0 breaker.log hole
  is open in production until DEPLOY_RUNBOOK.md runs. Two verified deploy blockers
  (Dockerfile never COPYs circuit_breaker.py/security/; compose has no
  /var/lib/agent-sudo mount => breaker resettable by docker restart, the exact D-CB6
  bypass). Both were invisible to an 85/85-green suite.
- context.md: MEMORY_DIR answered — do NOT repoint embed_memory_dir.py; its
  claude_memories table has no reader. Recall corpus now includes design docs;
  context.md stays excluded until #192 prunes it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Backtalk6858
2026-07-15 19:04:38 -05:00
parent dcc4cc4ef2
commit c0293fa96f
3 changed files with 275 additions and 6 deletions
+25 -6
View File
@@ -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: 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 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. "server-01 is involved, therefore sandbox Vault." That inversion caused a real near-miss.
- **Design decisions D1D10 + D-CB1D-CB4 are LOCKED.** The design record outranks any task title — - **Design decisions D1D10 + D-CB1D-CB9 are LOCKED.** The design record outranks any task title —
read `agent_builder/agent_sudo_design_decisions.md` before re-litigating. read `agent_builder/agent_sudo_design_decisions.md` before re-litigating. **D-CB5D-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 **⚠️ P3 CODE IS COMPLETE BUT NOT DEPLOYED — BOTH HOSTS STILL RUN THE OLD `sudo_rules.py`.**
narrow `claude-policy` — rotation alone re-arms the hole) → **#149** (wire tier-2/3 + circuit-breaker, `app.py` + `circuit_breaker.py` + Set B override are written and green (**85 tests OK**, mutants
with **#173** fail-open/fail-closed folded in) → **#128** secrets-proxy (remove the ntfy approval caught) but **UNDEPLOYED**, so **the server-01 tier-0 `breaker.log` hole is OPEN IN PRODUCTION**
gate) → **#147/#146** (CA P2P5 + primary cutover 8084→8082). 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-CB5D-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 13; user runs
the tier-4 steps) → **#173** fail-open/fail-closed → **#128** secrets-proxy → **#147/#146** (CA
P2P5 + 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 **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.** (#178) · **Mon 07-20 resume business development (#179) — digital businesses deployed by Fri 07-24.**
+104
View File
@@ -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. ## 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 161209, and the two capability stubs are at ~186191:
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 4950.
- `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.
```
@@ -132,3 +132,149 @@ Write the **phased** background-agent prompt SET (NOT one monolith — see reaso
- P5: decommission old sudo-bridge. - 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. 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]].