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:
@@ -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.
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user