From 7b6adc23327d619c87a601a290c297e5d2ca3137 Mon Sep 17 00:00:00 2001 From: Mike Kobit Date: Mon, 13 Jul 2026 22:37:39 -0500 Subject: [PATCH 1/2] chore: fix dangling doc references and delete retired tool tombstones - docs/HANDOFF-sandboxr-runtime-decision.md: continuation prompt pointed at feat/sandboxr-two-layer-design, a branch that no longer exists; point at the merged PRs instead. - src/chezmoi/dot_config/mise/AGENTS.md: locking script was renamed (run_after_onchange_06_... -> run_onchange_after_06_...) and its MISE_LOCKED value flipped 1->0 per #574 (relax locking) without the doc being updated; repo-root .mise.toml was also renamed to mise.toml. - docs/DESIGN-keybindings.md: managed-files table had stale paths for zellij config, Ghostty conf.d files, and VS Code/Cursor keybindings (referenced a modify_ prefix that was never shipped). - Delete the claude_statusline and agent_run tombstone entries from the BOM and local_bin_tools catalog: both were retired via rename (termstatus, sandboxr respectively) over 2 weeks ago and confirmed applied on all machines. --- docs/DESIGN-keybindings.md | 10 +++++----- docs/HANDOFF-sandboxr-runtime-decision.md | 5 ++++- src/chezmoi/.chezmoi.toml.tmpl | 6 ------ src/chezmoi/.chezmoidata/local_bin_tools.toml | 7 ------- src/chezmoi/dot_config/mise/AGENTS.md | 6 +++--- 5 files changed, 12 insertions(+), 22 deletions(-) diff --git a/docs/DESIGN-keybindings.md b/docs/DESIGN-keybindings.md index fc44c579..75ff57d5 100644 --- a/docs/DESIGN-keybindings.md +++ b/docs/DESIGN-keybindings.md @@ -53,11 +53,11 @@ Use `Ctrl+X Ctrl+E` as the open-in-editor alternative in Claude Code when locked | File | What it controls | |---|---| -| `src/chezmoi/dot_config/zellij/config.kdl` | All Zellij keybindings | -| `src/chezmoi/conf.d/macos.conf` (Ghostty) | `macos-option-as-alt = true` | -| `src/chezmoi/conf.d/keybinds.conf` (Ghostty) | `Cmd+Left/Right/Backspace` → readline sequences | -| `Library/Application Support/{Code,Cursor}/User/modify_keybindings.json.tmpl` | Shift+Enter in VS Code/Cursor (macOS) | -| `dot_config/{Code,Cursor}/User/modify_keybindings.json.tmpl` | Shift+Enter in VS Code/Cursor (Linux/WSL) | +| `src/chezmoi/dot_config/zellij/config.kdl.tmpl` | All Zellij keybindings | +| `src/chezmoi/dot_config/ghostty/conf.d/macos.conf` | `macos-option-as-alt = true` | +| `src/chezmoi/dot_config/ghostty/conf.d/keybinds.conf` | `Cmd+Left/Right/Backspace` → readline sequences | +| `src/chezmoi/Library/Application Support/{Code,Cursor}/User/keybindings.json.tmpl` | Shift+Enter in VS Code/Cursor (macOS) | +| `src/chezmoi/dot_config/{Code,Cursor}/User/keybindings.json.tmpl` | Shift+Enter in VS Code/Cursor (Linux/WSL) | --- diff --git a/docs/HANDOFF-sandboxr-runtime-decision.md b/docs/HANDOFF-sandboxr-runtime-decision.md index b8a1f013..209e4b93 100644 --- a/docs/HANDOFF-sandboxr-runtime-decision.md +++ b/docs/HANDOFF-sandboxr-runtime-decision.md @@ -84,4 +84,7 @@ srt is the only process-level option with fail-closed egress (domain allowlist v ## Continuation prompt for the next session -See PR/branch `feat/sandboxr-two-layer-design` for both documents and the design-decision trail. +The design-decision trail lives in merged PRs, not a branch — the source branch and both +design documents referenced above (`DESIGN-sandboxr-two-layer.md`, `seatbelt.py`) were +deleted after srt acceptance. See #695 (srt-primary decision, backend), #705, #706, #707, +#708, #709 for the implementation history. diff --git a/src/chezmoi/.chezmoi.toml.tmpl b/src/chezmoi/.chezmoi.toml.tmpl index 97e5f9b1..df34bf60 100644 --- a/src/chezmoi/.chezmoi.toml.tmpl +++ b/src/chezmoi/.chezmoi.toml.tmpl @@ -138,9 +138,6 @@ installation_method = {{ if eq .chezmoi.os "linux" }}"apt"{{ else }}"none"{{ end installation_method = {{ if eq .chezmoi.os "linux" }}"apt"{{ else }}"none"{{ end }} # Local Python Tools (via uv) Installation Methods -[data.local_bin_tools.claude_statusline] -installation_method = "uninstall" - [data.local_bin_tools.termstatus] installation_method = "dotfiles.uv" @@ -153,9 +150,6 @@ installation_method = "dotfiles.uv" [data.local_bin_tools.termbud] installation_method = "dotfiles.uv" -[data.local_bin_tools.agent_run] -installation_method = "uninstall" - [data.local_bin_tools.sandboxr] installation_method = "dotfiles.uv" diff --git a/src/chezmoi/.chezmoidata/local_bin_tools.toml b/src/chezmoi/.chezmoidata/local_bin_tools.toml index 493cce31..2fbe1b21 100644 --- a/src/chezmoi/.chezmoidata/local_bin_tools.toml +++ b/src/chezmoi/.chezmoidata/local_bin_tools.toml @@ -3,10 +3,6 @@ # source_dir: path relative to local_bin_tools_root (defaults to workingTree) # package_name: uv package name (used for uninstall) -# Renamed to termstatus; the non-uv method triggers the uninstall branch. -[local_bin_tools.claude_statusline] -package_name = "claude-statusline" - [local_bin_tools.termstatus] source_dir = "src/python/termstatus" package_name = "termstatus" @@ -23,9 +19,6 @@ package_name = "transcribe" source_dir = "src/python/termbud" package_name = "termbud" -[local_bin_tools.agent_run] -package_name = "agent-sandbox" - [local_bin_tools.sandboxr] source_dir = "src/python/sandboxr" package_name = "sandboxr" diff --git a/src/chezmoi/dot_config/mise/AGENTS.md b/src/chezmoi/dot_config/mise/AGENTS.md index 7b7516e1..f0446b18 100644 --- a/src/chezmoi/dot_config/mise/AGENTS.md +++ b/src/chezmoi/dot_config/mise/AGENTS.md @@ -17,8 +17,8 @@ ## Lockfile `~/.config/mise/mise.lock` is regenerated locally by `mise lock` and is **not tracked in this repo** — `.chezmoiignore` blocks it (`**/.config/mise/mise.lock`) so per-machine drift doesn't pollute the chezmoi diff. -Locking is **enforced during deployment** via `MISE_LOCKED=1` in the `run_after_onchange_06_trust-install-global-mise-tools.sh.tmpl` script, so whatever lockfile is locally present is honored at install time. -The deployed `config.toml` defaults to `locked = false` so local projects aren't forced to use lockfiles by default. +Locking is **relaxed during deployment** via `MISE_LOCKED=0` in the `run_onchange_after_06_trust-install-global-mise-tools.sh.tmpl` script, so global tool installs aren't blocked by a missing or stale lockfile. +The deployed `config.toml` has no explicit `locked` setting, so mise's own default (unlocked) applies. ### Updating tools and relocking @@ -35,4 +35,4 @@ Do not substitute alternative commands — especially in Jules, where `mise lock ``` MISE_LOCKED=0 mise -C ~ lock --global ``` - The `-C ~` flag is required — running from the dotfiles repo root causes global-only tools (e.g. `uv`) to be excluded because the local `.mise.toml` claims them. + The `-C ~` flag is required — running from the dotfiles repo root causes global-only tools (e.g. `uv`) to be excluded because the repo-root `mise.toml` claims them. From e15866f6eef77903634f0f7c6269c4049bd61261 Mon Sep 17 00:00:00 2001 From: Mike Kobit Date: Mon, 13 Jul 2026 22:44:39 -0500 Subject: [PATCH 2/2] docs(sandboxr): delete the handoff doc, its job is done docs/HANDOFF-sandboxr-runtime-decision.md was a session-continuation artifact from PR #695 ("Audience: fresh agent session executing follow-ups"), not curated documentation. Every follow-up it listed is resolved (#706, #707, #708, #709); the decision rationale it recorded is already duplicated in memory. Repoint the two comment references that cited it at the actual PR instead. --- docs/HANDOFF-sandboxr-runtime-decision.md | 90 ----------------------- src/chezmoi/.chezmoidata/ai/sandbox.toml | 2 +- src/chezmoi/.chezmoidata/bin/mise.toml | 2 +- 3 files changed, 2 insertions(+), 92 deletions(-) delete mode 100644 docs/HANDOFF-sandboxr-runtime-decision.md diff --git a/docs/HANDOFF-sandboxr-runtime-decision.md b/docs/HANDOFF-sandboxr-runtime-decision.md deleted file mode 100644 index 209e4b93..00000000 --- a/docs/HANDOFF-sandboxr-runtime-decision.md +++ /dev/null @@ -1,90 +0,0 @@ -# Handoff: sandboxr runtime decision (srt-primary, codex fallback) - -Date: 2026-07-05. -Status: analysis complete, direction chosen, zero implementation done. -Audience: fresh agent session executing follow-ups. - -## Governing criteria (user-stated, binding) - -1. **Explicit opens, fails closed in the most general sense.** - Every capability (read, write, egress) is deny-by-default; each exposure is a named, deliberate open. -2. **No hand-rolled enforcement.** - sandboxr is a policy compiler; enforcement belongs to a maintained vendor tool. -3. **This chezmoi repo is higher privilege.** - It is the control plane that generates sandbox policy; agents working on *other* projects must never see it writable, and policy inputs (`~/.config/ai-policy`, srt settings) must not be writable from inside any sandbox. -4. Whole-agent-loop sandboxing: sandboxr wraps a shell execution inside which an entire agentic loop (claude/gemini/etc.) runs. - The loop needs egress to its LLM API, so binary network on/off is insufficient — this is why domain allowlisting decides the runtime choice. -5. macOS is not a requirement; delete the seatbelt stub. - -## Decision - -**Primary: adopt `@anthropic-ai/sandbox-runtime` (srt) as the enforcement runtime.** -sandboxr compiles `SandboxSpec`/`sandbox.toml` profiles into srt settings JSON and the srt invocation. - -**Fallback design (two-layer bwrap-mask + `codex sandbox`) retired.** -User rejected maintaining it as a standing fallback ("maintaining two designs is wasted effort"); the design doc (`docs/DESIGN-sandboxr-two-layer.md`) was deleted once srt acceptance passed. - -### Why the recommendation flipped from the codex two-layer spec - -The earlier srt dismissal rested on two beliefs, both now falsified empirically (2026-07-05, srt via npm, Linux/WSL2, node 22.14): - -- Believed: srt `denyRead` is path-based denial (weaker than mount absence, bypass history). - **Measured: `denyRead: ["~"]` yields ENOENT ("No such file or directory") — mount-level absence via bwrap, the same primitive as the hand-rolled mask.** - Deny-then-allow works: `allowRead: ["~/.claude"]` re-exposed that subtree while the rest of home stayed absent. -- Believed: "meh" (user's initial take, since retracted). - -And the fails-closed criterion exposed the codex path's structural gap: codex network is all-or-nothing; an agent loop needs it on, and "on" is unrestricted egress with no allowlist mechanism. -srt is the only process-level option with fail-closed egress (domain allowlist via proxy). - -## Verified facts (do not re-derive) - -### srt (tested via local npm install in scratchpad) - -- Defaults with no settings: writes fail closed (even cwd read-only); egress fails closed (proxy 403); reads fail open (home readable) — so **`denyRead: ["~"]` + explicit `allowRead` list is mandatory in our profiles**. -- Settings schema (confirmed in dist): `filesystem.{denyRead,allowRead,allowWrite,denyWrite}`, `network.{allowedDomains,deniedDomains}`, `proxyPort`. -- Linux architecture (from `-d` debug output): bwrap + vendored native `apply-seccomp` binary (blocks Unix sockets) + host-side mux proxy (HTTP+SOCKS) on localhost, bridged into the network namespace via `socat UNIX-LISTEN:/tmp/claude-http-*.sock → TCP:localhost:`; `HTTP_PROXY`-style env points processes at localhost:3128 inside. -- srt also `/dev/null`-masks config-injection vectors inside writable roots (`.gitconfig`, `.bashrc`, `.zshrc`, `.mcp.json`, `.ripgreprc`, …) — protection the codex path only has for `.git`/`.codex`. -- **Open bug**: with `denyRead: ["~"]`, curl inside got "Failed to connect to localhost port 3128" for both allowed and denied domains. - socat IS installed on this machine, so that's not it. - Suspect: home-deny masks srt state the bridge needs, or the bridge socket path is masked. - This is follow-up #1; the domain allowlist worked in the default (no-settings) run, so the mechanism itself functions. -- **2026-07-08, follow-up #4 live acceptance**: running the real `claude` CLI through `srt-claude` intermittently hung (once for 78 minutes) with zero corresponding domain-allow/deny log lines — confirmed via `strace -f` this was not network-related at all: the event loop stayed alive, the hang was a purely local, non-deterministic stall. Strace caught one real, separate bug along the way: `.claude.json` is bind-mounted as an individual file (a top-level dotfile, not nested under a directory bind), making it its own mountpoint, so Claude Code's atomic-write pattern (`rename()` a temp file over it) always fails `EBUSY` — handled gracefully by Claude Code's own fallback (non-atomic open+truncate+write), but a real correctness downgrade for a credentials file shared with every other concurrent claude session on the host. Documented as a known limitation in `sandboxr/backend/bwrap.py` near `RW_HOME_PATHS`, not fixed (would require binding all of `$HOME` to preserve rename semantics, which defeats the deny-then-allow model). The hang itself did **not** reproduce on retest (4/4 clean runs immediately after, zero code changes) — working theory is lock contention on `.claude.json`'s `mkdir`-based lock against other live claude sessions on the host, made worse by the forced non-atomic write path above; not a deterministic sandboxr config bug. Don't assume a repro attempt will hang or succeed on any given try. - -### codex sandbox (tested via release binary, v0.142.5) - -- Standalone, no auth; Apache-2.0; static musl binary (~286 MB) with `.sigstore` attestations. -- Write-scoping (`workspace-write` + `writable_roots`) and network on/off verified working. -- **No read-hiding of any kind** (`~/.gnupg`, `~/.claude.json` readable under default profile); no deny-read config exists in the binary. -- No domain allowlisting. -- Nesting under an outer bwrap tmpfs-home mask works (needs writable `/tmp` for its mount-registry lock). -- Config schema mid-migration upstream (`[permissions]` tables erroring); `--full-auto` already deprecated once — churn-prone surface. - -### Rejected outright - -- **firejail**: Linux-only setuid-root binary, recurring local-root CVEs (CVE-2022-31214 et al.), no programmatic policy API. -- **Hand-rolled hardening** of `bwrap.py`: viable but keeps us in the enforcement business; declined on principle. -- **Container / dedicated WSL distro tier**: strongest fail-closed, zero sandbox code, but env duplication; keep as escalation path, not current direction. - -## Policy layer (settled, applies regardless of runtime) - -- Signing: off inside the sandbox; human signs at merge/push from host ("sign at the gate"); unsigned-inside remains the provenance marker. -- Push/PR: no path out (no gh token, no git-push egress); export = human acting from the host on the bind-mounted worktree. -- Hooks/plugins/skills: per-tool-family read-only allowlist entries as data in `sandbox.toml` profiles (overlay-extensible). -- Higher-privilege chezmoi: default profiles must not expose this repo rw; a distinct privileged profile (or simply working unsandboxed) covers work on the dotfiles themselves. - -## Follow-ups (file as beads or execute in order) - -1. **Debug the srt proxy-under-deny-home failure.** - Reproduce: settings `denyRead:["~"]`, run `srt -d -c 'curl https://api.github.com/zen'`; bisect by adding `allowRead` entries (srt state dirs, `/tmp/claude-*.sock` paths) until the bridge works; identify the minimal allow set. -2. **Security audit of srt at a pinned version** (per repo practice for onboarding upstream code): npm supply chain, the vendored native `apply-seccomp` binary, proxy MITM/cert handling. -3. **Provisioning design**: pinned srt without global npm — e.g. `npm pack`ed tarball as chezmoi external, node via existing mise toolchain; must satisfy "policy inputs not writable from inside sandbox". -4. **Acceptance test**: run a real `claude` loop inside srt with `allowedDomains` limited to its API + statsig/sentry endpoints; extend `doctor.py` probes (absence through layers, write scoping, allowed vs denied domains, proxy health). -5. **Map `SandboxSpec`/`sandbox.toml` → srt settings generator**; keep the pure-function compilation + unit test pattern from `test_bwrap.py`. -6. **Housekeeping**: `seatbelt.py` stub deleted; `DESIGN-sandboxr-two-layer.md` deleted. `bwrap.py`'s fate still open — 4 of 6 configured profiles still resolve to it (`backend="auto"` on Linux), so it stays until those migrate too. - -## Continuation prompt for the next session - -The design-decision trail lives in merged PRs, not a branch — the source branch and both -design documents referenced above (`DESIGN-sandboxr-two-layer.md`, `seatbelt.py`) were -deleted after srt acceptance. See #695 (srt-primary decision, backend), #705, #706, #707, -#708, #709 for the implementation history. diff --git a/src/chezmoi/.chezmoidata/ai/sandbox.toml b/src/chezmoi/.chezmoidata/ai/sandbox.toml index 904e46db..e2057001 100644 --- a/src/chezmoi/.chezmoidata/ai/sandbox.toml +++ b/src/chezmoi/.chezmoidata/ai/sandbox.toml @@ -64,7 +64,7 @@ gpg_agent = false [ai.sandbox.profiles.srt-claude] enabled = true # srt (not "auto"): domain-allowlisted egress, no hand-rolled network -# enforcement. See docs/HANDOFF-sandboxr-runtime-decision.md. +# enforcement. See #695 (srt-primary decision, backend). backend = "srt" project_write = true network = "allowlist" diff --git a/src/chezmoi/.chezmoidata/bin/mise.toml b/src/chezmoi/.chezmoidata/bin/mise.toml index 8fb868ef..bb63cd0f 100644 --- a/src/chezmoi/.chezmoidata/bin/mise.toml +++ b/src/chezmoi/.chezmoidata/bin/mise.toml @@ -64,7 +64,7 @@ package_manager = "bun" [mise.global_tools.gemini-cli] version = "0.42.0" - # sandboxr's srt backend (docs/HANDOFF-sandboxr-runtime-decision.md): + # sandboxr's srt backend (#695: srt-primary decision, backend): # enforcement runtime for domain-allowlisted sandboxing. No registry # shorthand exists for this package, so the tool key is the fully # qualified npm backend spec. Resolved at runtime via `mise where`