docs(claude-config): security-infra game plan + all decisions locked (CA-D11 VM boundary, CA re-scope, A1/A3/A7, boot-recovery)

- decisions/GAMEPLAN_security-infra-deploy.md: executable plan, Steps 0-6, freeing-capability-first
- DECISIONS.md: PROPOSED -> DECIDED x2 + small-calls entry; research/INDEX.md updated
- 09-07 outage root-caused: LAN-IP port binds before WiFi has the address (boot order, not a crash)
- context.md: NEXT SESSION PLAN rewritten (Opus executes Step 0; Fable harness track if credits remain)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9iCzmxK2zbb8H1Ld3f8AN
This commit is contained in:
Backtalk6858
2026-09-08 21:20:38 -05:00
parent d8ec3001a4
commit c9e33e5e81
4 changed files with 179 additions and 8 deletions
+12
View File
@@ -75,6 +75,18 @@ Update this file during every session debrief that touches this project — keep
"Directory layout", and "Known issues" current after every session.
## NEXT SESSION PLAN (rewritten 2026-09-08 21:40 — supersedes the 20:40 block below)
**Game plan + decisions DONE** (Fable, ~$0.10 spent): `decisions/GAMEPLAN_security-infra-deploy.md` is the
executable plan; DECISIONS.md has CA-D11 (VM boundary, autonomous runner first), CA re-scope, A1=(a), A3=(c),
A7(A)=no, recovery layer = host systemd level-0. Root cause of the 09-07 outage = boot order (LAN-IP port binds
before WiFi has the address), NOT a crash.
1. **Switch to Opus 4.8** and execute GAMEPLAN **Step 0** (control plane up + `ip_nonlocal_bind` + `control-plane-up`
timer; Step 0.4 daemon unit = HUMAN). Then Step 1 (rotate row 192 key), Step 2 (S1).
2. **Fable, if credits remain (expire 2026-09-19):** the harness/LifeOS track — personal_projects row logged tonight
(search name ilike '%harness grill-me%'); grill-me FIRST on what the user wants from a harness.
3. Stale-record fixes done tonight: rows 216/217 on_hold (superseded by CA-D11); row 206 blocker rewritten (SSD IS
mounted at /data). Bitwarden↔Vault loop (row 177) still needs a user decision — not in the plan.
## NEXT SESSION PLAN (set 2026-09-08 20:40 — user's explicit order, stay on FABLE 5.1 for 1–3; credits expire 2026-09-19, $99.95 left)
1. **Security-infra GAME PLAN** — detailed, buildable plan to get ALL security infrastructure deployed: boot/recovery
layer → `claude-runner` Incus KVM VM on server-01 → secrets-proxy S1/S2 → Agent-Sudo A1/A2/A5 → Hermes outside the
+13 -6
View File
@@ -15,8 +15,15 @@ entry links back to the research that drove it, so the reasoning survives.
---
## 2026-09-08 — PROPOSED (not decided): run Claude Code inside an isolation boundary + brokers
- **Decision:** PROPOSED adopt — Claude Code runs in an **Incus KVM VM on server-01** (non-root, no sudo, no
## 2026-09-08 — DECIDED: the four small Agent-Sudo / recovery calls (grill-me 21:15–21:30)
- **Decision:** **A1 = (a)** tier 1 captures scoped-undo (implement D4; fix the test that pins tier-3-only). **A3 = (c)** primary tier-2 stays 503 — dissolved by CA-D11. **A7(A) = no**, an explicit tier-4 SUDO.md rule is not an attack signal (audit-only). **Recovery layer = host systemd level-0** (`control-plane-up` oneshot+timer, fixed command list, `ip_nonlocal_bind=1`) with Hermes as level-1; row 193 recommendation = keep fail-closed once the daemon has backoff. A7(A) and the recovery shape were defaults I took — object to reopen.
- **Why:** the 09-07 outage was boot order (LAN-IP port binds before WiFi), not a crash; an irreversible tier-1 is a gate removed without a constraint added.
- **Research:** [../research/autonomy-isolation-evaluation.md](../research/autonomy-isolation-evaluation.md) §1, §5 #1/#4
- **Implementation:** [GAMEPLAN_security-infra-deploy.md](GAMEPLAN_security-infra-deploy.md) Steps 0, 4
- **Revisit when:** Ethernet + a redundant host exist (D3 long-term), or tier-1 undo latency is measured >2 s.
## 2026-09-08 — DECIDED (grill-me 21:10): run Claude Code inside an isolation boundary + brokers (CA-D11)
- **Decision:** ADOPT — **autonomous runner first** (background agents + unattended runs move into the VM; the interactive session stays on primary under auto mode + hook until brokers are proven, then migrates). Supersedes personal_projects rows 217 (jail on primary) + 216 (primary substrate). Detail: Claude Code runs in an **Incus KVM VM on server-01** (non-root, no sudo, no
docker.sock, no Vault creds, egress allowlist); all host reach goes through the existing brokers
(Agent-Sudo, secrets-proxy, Jenkins). Inside that boundary, auto mode / `--dangerously-skip-permissions`
is the Anthropic-sanctioned case. Docker Sandboxes rejected for now (unsupported on LMDE 7 / Debian 13;
@@ -24,12 +31,12 @@ entry links back to the research that drove it, so the reasoning survives.
- **Why:** today the agent shell is root-equivalent on the production host; the July design brokered
actions but never said where the agent runs. The hypervisor already exists (Incus qemu driver, KVM).
- **Research:** [../research/autonomy-isolation-evaluation.md](../research/autonomy-isolation-evaluation.md) §3–§4
- **Implementation:** pending — needs grill-me + a CA design amendment (CA-D11) first.
- **Implementation:** [GAMEPLAN_security-infra-deploy.md](GAMEPLAN_security-infra-deploy.md) Step 3; CA-D11 amendment lands with it.
- **Revisit when:** a host moves to Ubuntu 24.04+ (Docker Sandboxes becomes supported), or Incus VM
overhead proves too high (fallback: `@anthropic-ai/sandbox-runtime`).
## 2026-09-08 — PROPOSED (not decided): re-scope Constrained Autonomy around auto mode
- **Decision:** PROPOSED — keep `security-enforcement.py` as a **deny-only + training-log** layer; **drop**
## 2026-09-08 — DECIDED (grill-me 21:12): re-scope Constrained Autonomy around auto mode
- **Decision:** ADOPT — keep `security-enforcement.py` as a **deny-only + training-log** layer; **drop**
CA-P1-full / D2 structural classifier / CA-P4 learned-allowlist promotion (auto mode's classifier now does
the judgment half, default on Pro); keep CA-P3 (secrets path) and CA-P5 (Hermes recovery ladder). Add
explicit deterministic denies for irreversible prod verbs (force-push, prod `compose down`, DB migrations)
@@ -37,7 +44,7 @@ entry links back to the research that drove it, so the reasoning survives.
- **Why:** the prompt-theater tax CA-P1 attacked is solved upstream; regex should do what a model classifier
can't (unbypassable denies), not compete with it.
- **Research:** [../research/autonomy-isolation-evaluation.md](../research/autonomy-isolation-evaluation.md) §3a, §5 #5–6
- **Implementation:** pending.
- **Implementation:** GAMEPLAN Step 5 (hook v3, deny-only).
- **Revisit when:** auto mode is removed from Pro, or its false-negative rate is published as materially better.
## 2026-09-08 — Establish claude-config as the research + config home
@@ -0,0 +1,152 @@
# GAMEPLAN — get ALL security infrastructure deployed and built
**Written:** 2026-09-08 21:30 (Fable 5.1, grill-me'd) · **Status:** ACTIVE PLAN · **Executes on:** Opus 4.8
**Supersedes:** the *ordering* in `agent-builder/GAMEPLAN_agent-sudo_secrets-proxy.md` (its task
definitions A1–A7 / S1–S4 stay valid and are referenced by ID below). Read that file for the per-task detail.
**Inputs:** `research/autonomy-isolation-evaluation.md`, both design records, live measurements tonight,
personal_projects rows 176/187/191/192/177/202/206/216/217.
> **Everything below is "verified tonight" unless marked UNVERIFIED.** Per
> `feedback_superseding_records_need_a_pointer`, re-test any claim before acting on it after 2026-09-15.
---
## 0. Decisions locked tonight (logged in DECISIONS.md)
| # | Decision | Result |
|---|---|---|
| CA-D11 | **Isolation boundary = Incus KVM VM `claude-runner` on server-01** | ADOPTED — **autonomous runner first** (background agents + unattended runs, `--dangerously-skip-permissions`, non-root). Interactive session stays on primary under auto mode + hook until brokers are proven (Phase 2 migration). Supersedes rows **217** (jail on primary) and **216** (primary substrate) — primary needs no Incus. |
| CA re-scope | Hook = **deny-only + training log**; drop D2 structural classifier / CA-P1-full / CA-P4 promotion; keep CA-P3, CA-P5; add deterministic prod-verb denies | ADOPTED |
| A1 | tier-1 scoped-undo | **(a) implement D4** — tier 1 captures undo |
| A3 | primary tier-2 | **(c)** stays 503 on primary; dissolved by CA-D11 (Claude never runs *on* primary) |
| A7(A) | is an explicit tier-4 SUDO.md rule an attack signal? | **No** — `rule_match` tier-4 = operator said refuse, not an attack; stays audit-only. (Default taken; object if wrong.) |
| Recovery layer | shape | **systemd on the host = level-0**, Hermes = level-1 (see §2). Fixed command list = EXECUTE-vetted, no allowlist widening. (Default taken; object if wrong.) |
---
## 1. Ground truth that changes the order (measured 2026-09-08)
1. **Root cause of the 09-07 outage = boot order, not a crash.** Docker starts before the WiFi has its LAN IP;
every control-plane container binds `192.168.1.x:port` → `cannot assign requested address` → left stopped,
`RestartCount=0` (start failures never engage the restart policy). primary IP arrived 16:20:28 (Docker up 15:43);
server-01 WiFi is a USB `wlx…` adapter under wpa_supplicant, **not NetworkManager-managed → `network-online.target`
is meaningless there**. Vault failed the same way → daemon AppRole busy-loop for 24 h.
2. **Primary has ZERO working backups** (row 202: Timeshift dead since 07-05, cron silenced). Tier-3 snapshot on
primary has no substrate. Agent-Sudo may never auto-restore primary anyway (D3), but "snapshot before execute" is
currently a no-op there.
3. **server-01 1TB SSD (870 EVO) IS mounted** at `/data` (sda4, ext4, 17 G used: ollama, models, compose). Row 206
("not readable") is stale. SMART CRC count **UNVERIFIED** (needs root; last known 48 on 06-10, cable reseat pending).
Only one ATA line this boot (`ata8: failed to resume link` — an empty port, not sda). Treat `/data` as
*usable but untrusted for large sequential reads* until SMART is re-read.
4. **Row 192: a live server-01 Agent-Sudo `BRIDGE_API_KEY` is in git** (agent-builder/.claude/context.md). Anyone on
the LAN with it runs tier-0/1 as root on server-01. Still unrotated since 07-15.
5. **Row 177: Bitwarden↔Vault loop** makes every `secret/*` identity root-equivalent. Needs a Bitwarden collection
restructure (user decision). Not fixed by anything in this plan; it caps how much the VM boundary buys until done.
6. Incus on server-01 is VM-ready: qemu 10.0.11, `/dev/kvm` (group kvm), `incus-admin` membership, `images:debian/13`
VM image cached, `incusbr0` 10.85.172.0/24 NAT, 27 GB RAM / 24 cores free. Storage pool = `sandbox-dir` (dir) —
a VM needs a block-capable pool (btrfs/dir works for VMs via qcow2 in Incus 6; verify at build).
7. Hook v2.1 live; Agent-Sudo containers exited 128 both hosts; secrets-proxy `Exited (0)` since 07-09; Jenkins
provisioned but idle (row 181).
---
## 2. Build order (freeing-capability-first)
Each step = one Opus session or one background agent unless marked HUMAN. Rollback + blast radius per step.
### Step 0 — bring the control plane up **and make it survive the next reboot** 🔴 tonight/tomorrow
- 0.1 `sysctl net.ipv4.ip_nonlocal_bind=1` in `/etc/sysctl.d/90-docker-lan-bind.conf`, both hosts (via Agent-Sudo
once up, or sudo-bridge). Lets `192.168.1.x` binds succeed before the NIC has the address. **Rollback:** delete
file, `sysctl --system`. Blast radius: nil (kernel flag; no behaviour change when the IP is present).
- 0.2 `compose up -d` in order on each host: primary Vault → bitwarden-bridge → sudo-bridge → agent-sudo →
secrets-proxy (leave DOWN until S1) ; server-01 vault-sandbox → bitwarden-bridge-sandbox → agent-sudo → jenkins →
hermes → n8n prod/sandbox. Check `project_coolify_env_var_debt` `${VAR}` placeholders BEFORE each up.
- 0.3 **`control-plane-up.service` + `.timer`** (host systemd, root, both hosts): `ExecStartPre` loop until the LAN
IP is present (`ip -4 addr show | grep 192.168.1.<88|90>`), then the fixed ordered `docker compose up -d` list
above, then NTFY. Timer every 10 min (idempotent; a no-op when healthy). Log every action to
`/var/log/control-plane-up.jsonl` (**training data**: Hermes reads it later; no new schema).
- This is level-0 recovery and the only piece outside the failure domain. **Hermes = level-1** (stays a container;
revived by level-0; CA-P5 ladder unchanged).
- **Fixed command list, no arguments from anywhere** → EXECUTE-vetted per `feedback_autonomous_security_constrain_not_gate`.
- Rollback: `systemctl disable --now`. Blast radius: at worst it starts stacks that were deliberately stopped —
so secrets-proxy is **excluded** until S2 lands, and a `/etc/control-plane-up/skip` file suppresses a stack.
- 0.4 **HUMAN (tier-4 Set B):** `agent-sudo-daemon.service` both hosts: `After=network-online.target docker.service`,
`RestartSec=30`, `StartLimitIntervalSec=0`, `ExecStartPre=` wait-for-Vault (curl `/v1/sys/health`, 5-min cap).
Do A4 (host-neutral `EnvironmentFile=/etc/agent-sudo/daemon.env`) in the same pass — one human edit, not two.
Also decide row **193** (fail-open/closed when primary Vault unreachable) here: **recommend keep fail-closed** —
the backoff removes the busy-loop cost that made it hurt.
- 0.5 Find the 15-min Vault `POST /start` caller on primary (root crontab / a timer) — it's a half-built recovery
script; retire it into 0.3 or leave and document.
- 0.6 **Wire Ethernet (~09-09).** Makes 0.1 rarely matter; does not remove the need for 0.3.
- **Definition of done:** reboot server-01 (test host) → everything green within 15 min with no human touch.
Primary reboot test only after that passes.
### Step 1 — rotate the exposed key (row 192) 🔴 HUMAN + agent, 1 hour
Before any VM is given broker credentials. Enumerate consumers (Vault `secret/data/sudo-bridge-server01`,
server-01 `.env`, any injected context), rotate via `bw generate` pattern, update Vault, restart agent-sudo on
server-01, purge from git history or accept-and-document. Rollback: old key kept 24 h in Vault under `_prev`.
### Step 2 — secrets-proxy **S1 investigate → S2 finish** 🔴 agent (S1 zero-dep)
Unchanged from the July gameplan §4. Tonight's evidence for S1: graceful stop 07-09; compose carries 18 `${VAR}`
env values (Coolify-era trap); `app.py` 851 lines with `/shell` + `env_secrets` + `/shell/approve|deny` (an NTFY
approval gate — **D4 says kill it**; S2 scope item). S3 sign `proxy.md`; S4 restore the secrets rule.
**Why here:** it taxes every session AND becomes the *only* secret path from inside the VM.
### Step 3 — `claude-runner` VM on server-01 🔴 agent + HUMAN for firewall, ~1 session
- `incus launch images:debian/13 claude-runner --vm -c limits.cpu=4 -c limits.memory=8GiB` (+ a 40 G root disk;
`agent` package for `incus exec`). Non-root user `runner`, **no sudo, no docker, no Vault creds**.
- Node + Claude Code; login via the Pro subscription (OAuth) — **no API key**. `~/.claude` seeded from a git-tracked
subset: hooks (`security-enforcement.py` = deny-only), skills, settings. Memory dir: fresh; `/recall` points at
server-01 Ollama (localhost from the VM's view is *not* the host — use 192.168.1.90:11434).
- Repos = `git clone` from `gitea.local` (not virtiofs — repos live on primary, the VM is on server-01). All changes
flow **git push → Jenkins**. Non-git edits on either host → Agent-Sudo tier-1/2/3.
- **Egress allowlist at the VM NIC (Incus network ACL on `incusbr0`, or nftables on the host):** allow
api.anthropic.com, claude.ai, platform.claude.com, statsig/sentry as Claude Code needs, gitea.local, deb.debian.org,
registry.npmjs.org, **192.168.1.88:8084/8082 (Agent-Sudo, secrets-proxy), 192.168.1.90:8082/8090/11434**; deny all
other LAN + internet. **Never** expose `/var/run/docker.sock`. Docker FORWARD-DROP / DOCKER-USER fix per
`reference_incus_on_docker_host_networking` applies to the VM too.
- **Prove it:** from inside the VM `curl 192.168.1.88:8200` FAILS, `curl 192.168.1.88:8084/health` SUCCEEDS, Claude
Code starts as non-root with `--dangerously-skip-permissions` and runs a read-only task end-to-end.
- Rollback: `incus delete claude-runner` — nothing on either host changes. Blast radius: the VM's own disk + whatever
the brokers allow (= the whole point).
- **Training data:** every broker call from the VM already lands in `command_audit` / secrets-proxy audit;
`ca_decisions.jsonl` inside the VM is synced to the repo weekly.
- CA-D11 amendment written into `constrained_autonomy_design_decisions.md` as part of this step.
### Step 4 — Agent-Sudo **A1 → A2 → A7 → A5** 🟡 agent + HUMAN(A2 verify, A5)
Unchanged tasks; **A2 tier-3 snapshot substrate** now = `/data` on the server-01 SSD **after** SMART CRC is re-read
and the cable reseated (row 206, rewrite its description — it is not "unreadable"). If CRC is still climbing,
snapshots go to the NVMe until the cable is fixed. Primary tier-3 snapshot stays a no-op until row 202 (restic to NAS)
exists — document it, don't fake it.
### Step 5 — CA hook v3 (deny-only re-scope) 🟡 agent, small
Add deterministic denies: `git push --force*` to `main`, `docker compose down` on prod stacks (path-anchored),
`alembic upgrade`/`psql -c "DROP|ALTER"`, any `docker.sock` mount, `SUDO_MD_VERIFY_ENFORCE`/unit edits. Remove the
D2/CA-P4 TODOs. Keep `ca_decisions.jsonl` logging (the gate stays instrumented). 42→~55 self-tests.
### Step 6 — move the interactive session into the VM (Phase 2 of CA-D11) 🟢 when Steps 2–5 are proven
Trigger: one full week of background agents in the VM with zero broker-gap workarounds. Then migrate memory/skills,
retire the primary Claude Code install to read-only use.
### Parked / cross-cutting
Row 177 (Bitwarden↔Vault loop — user decision on collections), row 202 (restic backups on primary — before any
Wave-3 redeploy), Tailscale/Twingate (after Ethernet), Coolify-API N8N debt, row 181 (Jenkins is idle — Step 3
makes it load-bearing; finish the build-out inside Step 3).
---
## 3. Jenkins / Hermes fit (required check)
- **Jenkins:** Steps 0.2 (deploys), 3 (the only write path from the VM to either host), 4 (image builds). Row 181
must close inside Step 3 or the VM has no write path.
- **Hermes:** level-1 monitor of the control plane (reads `/var/log/control-plane-up.jsonl` + `command_audit`),
CA-P5 ladder, D10 watchdog. Never level-0 — it lives in the failure domain.
## 4. Sequencing rationale (the ordering paradox check)
The July order put the freeing capability (no prompts) last. Auto mode already removed most of the prompt tax; the
remaining recurring taxes are (1) **the control plane dying on every reboot** (Step 0) and (2) **secrets workarounds
every session** (Step 2). Both are zero-dependency and go first. The VM (Step 3) is what makes the *rest* finish
itself, so it precedes A1/A2 rather than following them.
## 5. XREF
personal_projects: 176, 187, 191, 192, 193, 202, 206, 216 (superseded), 217 (superseded), 181, 177 · DECISIONS.md
2026-09-08 entries · `research/autonomy-isolation-evaluation.md` §5–§6 · `agent-builder/GAMEPLAN_agent-sudo_secrets-proxy.md`
+2 -2
View File
@@ -7,7 +7,7 @@ re-verify versions, repos, and Linux support before acting on anything.
| Topic | File | Status | Key open decision |
|---|---|---|---|
| **Infrastructure synthesis** — the whole landscape + reconciliation of the new research against decisions already made | [infrastructure-synthesis.md](infrastructure-synthesis.md) | Synthesized 2026-09-08 | **Confirm the July→Sept reality** (Max upgrade / Voice-Chat / Tailscale-Twingate / Agent-Sudo) before acting on anything. |
| **Autonomy + isolation evaluation** — does the plan work, gaps, and the 2026 Docker/Anthropic isolation landscape (auto mode, Bash sandbox, sandbox-runtime, Docker Sandboxes microVMs) | [autonomy-isolation-evaluation.md](autonomy-isolation-evaluation.md) | Evaluated 2026-09-08 (Fable 5.1) | **Adopt the VM-boundary + brokers shape?** (Incus KVM VM on server-01 running Claude Code; Agent-Sudo/secrets-proxy/Jenkins as the only host reach). Also: what killed the control plane at 16:07 on 2026-09-07. |
| **Autonomy + isolation evaluation** — does the plan work, gaps, and the 2026 Docker/Anthropic isolation landscape (auto mode, Bash sandbox, sandbox-runtime, Docker Sandboxes microVMs) | [autonomy-isolation-evaluation.md](autonomy-isolation-evaluation.md) | Evaluated 2026-09-08 (Fable 5.1) | DECIDED 2026-09-08 (adopt, autonomous runner first) → [decisions/GAMEPLAN_security-infra-deploy.md](../decisions/GAMEPLAN_security-infra-deploy.md). 16:07 outage = boot order (LAN-IP port binds before WiFi), solved. |
| Local AI coding stack (inference engine, coding harness, LifeOS, voice, skill porting) | [local-ai-coding-stack-research.md](local-ai-coding-stack-research.md) | Surface-level, in progress | **Goal framing:** RESOLVED by the existing vision = cost-reduction + tooling-independence, Claude stays the brain (NOT fully-local). See synthesis Part 3. |
### Where the prior infrastructure research lives (memory corpus)
@@ -21,7 +21,7 @@ Not duplicated here — cited in [infrastructure-synthesis.md](infrastructure-sy
## Cross-cutting open decisions
Pulled up from the individual briefs so they don't get buried:
0. **Isolation boundary** — where does Claude Code itself run? Proposed: Incus KVM VM on server-01, non-root, egress allowlist, brokers only. Decides whether `--dangerously-skip-permissions`/auto mode is safe. (autonomy-isolation-evaluation.md §4)
0. ✅ DECIDED 2026-09-08 — **Isolation boundary**: Incus KVM VM on server-01, non-root, egress allowlist, brokers only. Decides whether `--dangerously-skip-permissions`/auto mode is safe. (autonomy-isolation-evaluation.md §4)
1. **The goal** — cost / independence / fully-local. Governs every other choice in the local
stack. (from local-ai-coding-stack-research.md §0)