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:
@@ -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 D1–D10 + D-CB1–D-CB4 are LOCKED.** The design record outranks any task title —
|
- **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.
|
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
|
**⚠️ 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 P2–P5 + 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-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
|
**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.**
|
||||||
|
|||||||
@@ -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 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.
|
||||||
|
```
|
||||||
|
|||||||
@@ -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]].
|
||||||
|
|||||||
Reference in New Issue
Block a user