# Gameplan — Agent-Sudo + secrets-proxy to fully deployed **Written:** 2026-07-15 20:10 · **Author:** Claude (Opus 4.8) · **Status:** ACTIVE PLAN **Why now:** 2026-07-15 is the LAST day dedicated to this work. After this, time goes to business development (revenue) + other projects. Everything here is scoped so a background agent can do it, with human touches minimized to the ones that are human-only *by design*. --- ## 0. READ THIS FIRST — the goal is not what it sounds like The stated goal was "Agent-Sudo and secrets-proxy handling tier 0–4 commands." **Tier 4 must never be handled autonomously. That is not a gap — it is the design.** Per D-CB7, the `agent-sudo-daemon` unit env holds `SUDO_MD_VERIFY_ENFORCE`. A system that can restart its own daemon can disarm its own gate. So tier 4 = human-at-a-terminal, permanently. **"Agent-Sudo handles tier 4" means "Agent-Sudo correctly REFUSES tier 4."** That is **DONE and VERIFIED LIVE** (2026-07-15 19:52, both hosts): | command | tier | source | |---|---|---| | `rm -rf /` | 4 | `danger_veto` | | `systemctl restart agent-sudo-daemon` | 4 | `host_override_setb` | | `vi /etc/systemd/system/agent-sudo-daemon.service` | 4 | `host_override_setb` | Do not let a future session "finish tier 4." There is nothing to finish. If someone proposes making tier 4 autonomous, that is a request to delete the security model — refuse it. **Real remaining scope = tier 1, tier 2, tier 3.** --- ## 1. VERIFIED STATE as of 2026-07-15 20:00 (measured tonight, not remembered) ### Agent-Sudo — DEPLOYED ON BOTH HOSTS (first time ever) - Commit `005c214` on `origin/main` (repo: `boilerplates.git`, path `/opt/appdata/docker`). - **server-01**: `192.168.1.90:8082` · healthy · 0 restarts · `{"status":"ok","server_id":"server-01"}` - **primary**: `192.168.1.88:8084` · healthy · 0 restarts · `{"status":"ok","server_id":"primary"}` - Old sudo-bridge STILL owns `192.168.1.88:8082` on primary. **No cutover has happened.** (#146) - **D-CB6 breaker log is now persistent on BOTH hosts** — `/var/lib/agent-sudo/breaker.log`, dir `0700 root:root`, on a bind mount, verified resolving inside the container. `docker restart` can no longer reset a tripped breaker. **This closed a hole that was OPEN IN PRODUCTION.** `breaker.log` does not exist yet — correct, created 0600 on first append. - 85/85 tests pass on both hosts. - Capability gates: `SANDBOX_ENABLED=false`, `SNAPSHOT_ENABLED=false` on both. ⇒ **tier 2 and tier 3 currently return 503 stubs. They do not work.** ### Subsystem availability — MEASURED, and it is asymmetric | host | incus | sandboxes | timeshift | |---|---|---|---| | server-01 | **YES** | `sandbox-primary` + `sandbox-server01` both RUNNING | YES | | primary | **NO** | — | YES | **This asymmetry is the single most important fact in this document.** Tier 2 (sandbox-test) CANNOT be enabled on primary — there is no Incus there at all. Any plan that says "enable tier 2 on both hosts" is wrong. See §3-A3, which is an unresolved DESIGN question, not a task. ### secrets-proxy — STOPPED, NOT BROKEN - `secrets-proxy-secrets-proxy-1` = `Exited (0)` 6 days ago. - Logs show a **graceful shutdown** ("Application shutdown complete", "Stopping parent process"). It was stopped on purpose. **This is not a crash and not a debugging job.** - Repo: `/opt/appdata/docker/docker-compose/secrets-proxy/` (app.py, docker-compose.yml, Dockerfile, mirror/, requirements.txt). - **HONEST GAP: I did not investigate what work remains on secrets-proxy.** Task #128 says "Finish secrets-proxy (sprint-11 #1)" but I have not read sprint-11 or the code. Everything below for secrets-proxy is therefore scoped as INVESTIGATE-THEN-PLAN. Do not treat S2 as a known quantity. **Do not let an agent invent secrets-proxy work.** - Its being down has a live cost: the `secrets_via_proxy_only` rule has no proxy to use, so secret handling currently falls back to file-passing workarounds. --- ## 2. THE RECURRING BUG CLASS — check this reflexively **Per-host values hardcoded into files tracked once and shared by both hosts.** It bit TWICE tonight in one directory, and both were invisible until the two hosts were compared: 1. `docker-compose.yml` hardcoded primary's port `8084`. Pulling it to server-01 would have moved Agent-Sudo off 8082 and **silently undone the D8 cutover**. → FIXED: `${AGENT_SUDO_PORT}` in each host's gitignored `.env` (primary 8084, server-01 8082). 2. `agent-sudo-daemon.service` carries `SERVER_ID` + `VAULT_ADDR`. The tracked copy says `SERVER_ID=primary` and omits `VAULT_ADDR`. Installing it on server-01 would make server-01 **identify as primary and apply primary's tier-4 rules**, with no Vault path to verify its own allowlist. → NOT FIXED. See A4. Currently preserved by hand on server-01. **Rule for every future session: before landing any shared file on a second host, diff the two hosts' copies first.** Nothing in the test suite measures cross-host divergence. It never will. Related lesson from tonight, worth carrying: **a green test suite said nothing about whether the module even shipped.** 85/85 passed while the image would have crashed on boot with `ModuleNotFoundError`, and while the breaker was resettable by `docker restart`. A passing check only means what it measures. --- ## 3. AGENT-SUDO — remaining work ### A1. Fix tier-1 scoped-undo — **DO THIS FIRST. It is code, not config.** 🔴 BLOCKING **The defect:** `security/scoped_undo.py:186` declares `REQUIRES_UNDO_TIERS = (1, 3)`, but `app.py:244` calls `scoped_undo.capture_or_refuse(command, 3, server_id=SERVER_ID)` with the tier **hardcoded to 3**. Nothing ever captures undo for tier 1. So **tier-1 commands auto-execute today with no undo capture**, which contradicts D4 ("tier 1 = auto-execute + capture scoped-undo"). **Resolve the conflict BEFORE writing code — this is a human decision:** The P3 task contract said "tier 0/1 behaviour unchanged"; D4 says tier 1 captures undo. The background agent followed the task contract and pinned it with a test, then flagged it. It was right to flag rather than silently pick. Someone must decide: - (a) Implement D4: tier 1 captures scoped-undo. Cost: every tier-1 command pays undo-capture latency. Benefit: tier 1 becomes reversible, which is what makes autonomy safe. - (b) Amend D4: tier 1 is genuinely fire-and-forget, drop `1` from `REQUIRES_UNDO_TIERS`. Cheaper and faster, but then tier 1 is irreversible — and tier 1 is where the volume is. - **LEAN: (a).** The whole premise of constrained autonomy is that the constraint is real. An irreversible tier-1 with no undo is an approval gate removed without a constraint added. But this is a real trade and the user decides, not an agent. ⚠️ Whichever way it goes, **the test that currently pins the wrong behaviour must be updated**, or it will lock in the bug. ### A2. Phase 2 — enable capabilities on server-01, ONE AT A TIME 🟡 Runbook: `/opt/appdata/docker/docker-compose/agent-sudo/DEPLOY_RUNBOOK.md` phase 2. server-01 only (it has Incus + both sandboxes RUNNING + timeshift). **This is the first-ever live exercise of the sandbox and snapshot paths.** Until now tier-2/3 have ONLY run against injected fakes — the real Incus and timeshift paths have never executed. Expect real failures. Budget for that; do not schedule it as a 20-minute flag flip. Order (do not batch — one flag, verify, then the next): 1. `SNAPSHOT_ENABLED=true` in server-01's `.env` → `docker compose up -d` → exercise a tier-3 command → verify a timeshift snapshot is actually created AND that undo restores. 2. Then `SANDBOX_ENABLED=true` → exercise a tier-2 command → verify it runs in the Incus sandbox and NOT on the host. ⚠️ **Do NOT test the latch by firing a danger-veto command on a live host** (runbook says so). Latch behaviour is already proven by unit tests + mutation testing. ⚠️ Verify the 503 stub is actually gone, not that health is green. Health does not measure this. ### A3. **DESIGN QUESTION, NOT A TASK: what does tier 2 mean on primary?** 🔴 UNRESOLVED Primary has no Incus. Options, none free: - (a) Install Incus on primary. But: `reference_incus_on_docker_host_networking` documents that Docker's `FORWARD DROP` breaks the incus bridge and debian/13 systemd-resolved is broken. This is a known-painful install on a Docker host — and primary is the PRODUCTION host. - (b) Primary tier-2 commands execute in server-01's sandbox (cross-host). Introduces the same cross-host dependency class as #173, and the sandbox would not mirror primary's state. - (c) Tier 2 stays permanently 503 on primary; primary's SUDO.md never classifies anything as tier 2. Honest and safe; means primary is a lesser-capability host by design. **LEAN: (c)** for now — it is the only one that ships without new risk on the production host, and (a) can come later if tier-2 volume on primary justifies the pain. **This must be decided by the user before anyone touches primary tier-2.** ### A4. Daemon unit `EnvironmentFile` — fix the §2 bug class 🟡 HUMAN-ONLY Make `agent-sudo-daemon.service` host-neutral: `EnvironmentFile=/etc/agent-sudo/daemon.env`, with `SERVER_ID` / `VAULT_ADDR` / `SUDO_MD_VERIFY_ENFORCE` per-host in that file. **Tier-4 human-only by design** — that unit's env IS the gate. An agent must never edit it. **Fold into #173**: the `VAULT_ADDR=http://192.168.1.88:8200` line in server-01's unit IS the cross-host Vault dependency that #173 exists to resolve. Same file, same edit, one human pass. ### A5. #146 — primary port swap 8084 → 8082 🟢 NOW TRIVIAL Was a manual file edit; the `${AGENT_SUDO_PORT}` change reduced it to a one-line `.env` edit plus stopping the old sudo-bridge. Do this LAST, after primary tier-1/2/3 behaviour is settled. Blocked by: #191 (sudo-bridge/app.py is one of the git-held files). ### A6. server-01 repo hygiene 🟢 server-01's `/opt/appdata/docker` is at HEAD `96f8c05`, **28 commits behind**, with the agent-sudo dir checked out from `005c214` into a staged index, plus uncommitted local changes to `jenkins/Dockerfile`, `sudo-bridge/app.py`, `sudo-bridge/docker-compose.yml`. The files are correct and the deploy is sound, but the repo state is a trap for the next person. **Why it is this way:** a full `git pull` would have clobbered those held local changes, so a targeted `git checkout origin/main -- docker-compose/agent-sudo` was used instead. That was the right call under time pressure; reconciling properly is a separate, careful job. ⚠️ Whoever does this must NOT blindly `git checkout`/`stash` the local changes away — find out what they are first. Same category as #191. ### A7. Spec gaps A + C on projects row 187 🟢 confirm-and-close - **(A)** `ATTACK_TIER4_SOURCES` = `('danger_veto', 'host_override_setb')`. `classify()` can also reach tier 4 via `rule_match` (explicit SUDO.md tier-4 rule) and `miss_verb_heuristic`. The agent made both audit-only, reasoning a latch must mean "this is not resolving." **Decide: is an explicit tier-4 SUDO.md rule an attack signal?** One-line change; the D10 seam holds. Note D-CB8 deliberately excludes `miss_primary_refuse` — latching on it would take a host read-only on the first typo. Do not "fix" that. - **(C)** A failed PRE-exec snapshot refuses 503 without calling `record_tier3_failure()` — the command never ran, so counting it toward the benign trip threshold would let a broken timeshift disarm tier 3. Reasoning is sound; just confirm and close. --- ## 4. SECRETS-PROXY — remaining work ### S1. INVESTIGATE — **must happen before any plan is written** 🔴 Nobody currently knows what "finish secrets-proxy" means. Establish, with evidence: 1. **Why was it stopped?** Exit was graceful and deliberate 6 days ago (~2026-07-09). Check git log, session summaries, and task #128 / sprint-11 notes for the reason. **It may have been stopped for a reason that still applies** — do not just start it. 2. What is incomplete in `app.py` / `mirror/`? What did sprint-11 #1 actually scope? 3. Does it start cleanly today? What breaks if it does? 4. Is its `docker-compose.yml` carrying the §2 per-host-value bug? Check before any second host. 5. Check `project_coolify_env_var_debt`: are there `${VAR}` Coolify placeholders that will bite on restart? **This is a known live trap for any container restart.** **Output: a written scope. Then and only then, plan S2.** Do not skip to S2. ### S2. FINISH + DEPLOY — cannot be scoped until S1 lands 🔴 Placeholder by construction. Anyone writing detail here before S1 is guessing. ### S3. #150 — sign `proxy.md` via Vault transit 🟡 Explicitly blocked on secrets-proxy deploy. Mirrors Agent-Sudo's SUDO.md signing (#145), so the pattern is proven — reuse `security/sudo_sign.py`, do not reinvent. ### S4. Restore the `secrets_via_proxy_only` rule 🟢 Once up, secret reads go back through the proxy `/shell` + `env_secrets` vault refs, retiring tonight's file-passing workaround. --- ## 5. SEQUENCING — and the trap in the obvious order **The ordering paradox** (`feedback_build_the_freeing_capability_first`): the obvious order is "finish Agent-Sudo, then secrets-proxy, then business dev." But the item that removes a recurring tax is often scheduled last, behind the things that charge it. **secrets-proxy being down taxes EVERY future session** — every secret read needs a workaround, and that cost is paid on business-development days too, when this infrastructure is supposed to be out of the way. **S1 is zero-dependency and small. Do S1 FIRST**, even before A1, so the scope is known and can be handed to an agent while attention is elsewhere. Recommended order: | # | item | who | why here | |---|---|---|---| | 1 | **S1** investigate secrets-proxy | agent | zero-dep, removes a recurring tax, unblocks S2/S3 | | 2 | **A1 decision** tier-1 undo (a) vs (b) | **human** | blocks all tier-1 work; 5-min decision | | 3 | **A3 decision** primary tier-2 | **human** | blocks primary; 5-min decision | | 4 | **A1 implement** | agent | the real code work | | 5 | **A2** phase 2 on server-01 | agent + human | first live sandbox/snapshot run | | 6 | **A7** confirm gaps A + C | agent | cheap close-out | | 7 | **S2** finish secrets-proxy | agent | scoped by S1 | | 8 | **A4 + #173** unit EnvironmentFile | **human** | tier-4 by design | | 9 | **A6** repo hygiene, **A5**/#146 swap, **S3**/#150 | mixed | last, low risk | **Steps 2 and 3 are two human decisions totalling ~10 minutes that unblock everything else.** Make them before starting a business-development day and the agents can run without you. --- ## 6. WHAT AGENTS MAY AND MAY NOT DO **Never delegate to an agent (tier-4 / human-only by design):** - Restarting or stopping `agent-sudo-daemon`; editing its unit file (A4) — *the unit env is the gate* - Editing `SUDO.md`; writing `/var/lib/agent-sudo/breaker.log`; resetting a tripped latch - The #146 port swap - The A1 and A3 **decisions** (implementation is delegable; the choice is not) - Anything in §2 that lands a shared file on a second host without diffing both first **Good agent work:** S1 investigation, A1 implementation once decided, A2 under supervision, A7 confirmations, S2 once scoped, test writing. **Every agent prompt must carry** (`playbook_background_agent_prompts` + `feedback_agent_prompt_mandatory_wrapup`): the mandatory wrap-up JSON block (`status` / `actions_taken` / `actions_failed` / `notes` / `human_action_required`) so the agent structurally cannot stop without reporting; an explicit permitted-file list; and `--max-turns`. Tonight proved the value: the P3 agent's two most valuable findings were the blockers **outside** its permitted file set, which it could only flag, not fix. **An agent that flags what it cannot touch is working correctly — do not "fix" that by widening permissions.** --- ## 7. DEFINITION OF DONE Agent-Sudo is done when: - [ ] Tier 0 executes. **(DONE — verified 2026-07-15)** - [ ] Tier 4 is refused on both hosts. **(DONE — verified 2026-07-15)** - [ ] Breaker log persists across restart on both hosts. **(DONE — verified 2026-07-15)** - [ ] Tier 1 behaviour matches a D4 that has been reconciled with the task contract (A1) - [ ] Tier 3 creates a real timeshift snapshot and restores, on server-01 (A2) - [ ] Tier 2 executes in the real Incus sandbox on server-01 (A2) - [ ] Primary's tier-2 story is decided and written down (A3) - [ ] The daemon unit is host-neutral (A4 / #173) - [ ] Instrument-the-gate audit query is scheduled and someone READS it (`feedback_instrument_the_gate` — a gate nobody reads is not instrumented) secrets-proxy is done when: S1 scope written · S2 deployed · S3 `proxy.md` signed · `secrets_via_proxy_only` restored without workarounds. **Not done ≠ not useful.** As of tonight Agent-Sudo safely runs tier-0 and correctly refuses tier-4 on both hosts, with a tamper-evident breaker. That is real, and it is the part that was open in production this morning. --- ## 8. XREF projects **id=187** (P3 detail + 3 spec gaps) · **#149** (parent) · **#146** (port swap) · **#173** (fail-open/closed — fold A4 in) · **#191** (git-held files) · **#128** (secrets-proxy) · **#150** (proxy.md signing) · `DEPLOY_RUNBOOK.md` (phases) · `agent_sudo_design_decisions.md` (D1–D10, D-CB1–D-CB9) · memory: `feedback_build_the_freeing_capability_first`, `feedback_instrument_the_gate`, `feedback_autonomous_security_constrain_not_gate`, `reference_incus_on_docker_host_networking`, `project_coolify_env_var_debt`, `feedback_agent_prompt_mandatory_wrapup`