Catches the rest of the xhigh review's verified findings, focused on
security + reliability + correctness. Each bullet references the
finding from the JSON output in this conversation.
**Security**
- intercept_hook: strip Set-Cookie / Set-Cookie2 / WWW-Authenticate /
Proxy-Authenticate from upstream responses. Prevents in-VM agent
from harvesting GitHub session cookies and replaying them on the
unfiltered github.com path to authenticate as the host user
(review #3).
- intercept_hook: substitute_authorization_header handles both Bearer
(literal substring) AND Basic auth (decode → replace → re-encode
base64). Closes the "git Basic-auth always 401s" path while keeping
the gh CLI Bearer path working. Also injects Bearer with the real
token if the guest sent no Authorization header at all, so the
forwarder never sends anonymous-but-substituted-looking requests
(review #12).
- run.rs detect_github_repos_in_cwd hardens the git invocation against
repo-local config that can execute code on the host (core.fsmonitor,
core.pager, core.editor, alias.*). Pass `safe.directory=*` so a
foreign-UID checkout doesn't fail closed, `protocol.allow=never` so
the call can't fetch anything, and force GIT_CONFIG_SYSTEM /
GIT_CONFIG_GLOBAL to /dev/null so user-global git config can't
inject an attacker-controlled include path. Closes the "host RCE
before sandbox boot via untrusted .git/config" finding (review #6).
- run.rs adds a long inline note above the gh secret entry calling
out the github.com smart-HTTP gap (review #1). Filtering git
smart-HTTP requires a streaming intercept primitive microsandbox
doesn't expose (current intercept buffers full request, capped at
64 KiB; git push pack data exceeds that). Documented; the threat
model is now explicit.
**Reliability**
- intercept_hook forward_github_api: reqwest::Client gets a 60 s
timeout and `Policy::none()` redirect policy — hung api.github.com
no longer freezes the sandbox; 3xx responses are surfaced to the
guest verbatim instead of silently following (review #7).
- intercept_hook trigger_host_refresh: 90 s wait-with-timeout on the
host `claude -p` / `codex exec` so a stuck CLI doesn't hold the
intercept hook indefinitely (review #8).
- run.rs streaming exec: distinguish "stream closed without Exited
event" (infra disconnect) from "agent exited with code 1" (real
failure). The former bails with a clear error instead of returning
exit 1 (review #9).
**Correctness**
- run.rs adds a SnapshotGuard with `impl Drop` so the Phase-5 host-
cred mutation check runs on every exit path from launch(),
including `?` propagation from attach/exec_stream errors. Previously
the safety net only fired on the happy path (review #10).
- run.rs: --no-git no longer silently discards explicit --repo
arguments. The flag suppresses cwd auto-detection but explicit
--repo entries are kept; the launcher warns when --no-git +
--repo are combined (review #11).
- secrets.rs: `Ok(_)` → `Ok(Some(()))` in the opencode token-file
arm. `Ok(None)` (host file missing) no longer registers the
placeholder pointing at a token file that wasn't written (review
#13).
- secrets.rs decode_id_token_account: try URL_SAFE_NO_PAD first
(conformant JWT base64url) then fall back to STANDARD with manual
'+'/'_' alphabet conversion. Closes the silent zero-UUID fallback
when an upstream library emits standard-alphabet JWT payloads
(review #14).
- bin/agent-vm-ccusage: switch from `paste -sd,` to a
null-delimited find→read loop, and skip any path containing `,`
with a clear warning. CLAUDE_CONFIG_DIR's comma-separator has no
escape mechanism so any path with a comma silently corrupts the
union (review #15).
Verified end-to-end:
- `gh api graphql ...` reaches GitHub (allow-list extended in the
previous commit; this commit didn't touch path filtering, but the
pipeline still works: clean 403 for denied repos, real responses
for allowed paths).
- `git status` works on the bind-mounted project — no dubious-
ownership (safe.directory) and the hardened cwd-detection git
call still finds the remote.
- gh hosts.yml is written, gh auth status shows the placeholder
account is active (Bad credentials when reaching GitHub is the
same documented outer-bridge nested-host limit).
- 6/6 cargo tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DX additions from the original Bash agent-vm that the user wants in
v1. Four independent pieces; one commit because they all touch the
same layer (launcher + image + a new subcommand).
**`--mount HOST[:GUEST]`** (repeatable).
- New `Args::mount: Vec<String>` parsed via `parse_extra_mounts` into
`(host, guest)` pairs. `HOST` alone defaults `GUEST` to mirror.
Both must be absolute; the host path is canonicalized (follows
symlinks) and checked to exist before the launcher proceeds.
- For each mount we call `.volume(guest, |m| m.bind(host))` and
`mkdir_chain(guest)` in the patch builder, mirroring what the
project bind already does. The launcher prints a per-mount line.
- Note: libkrun's virtio IRQ pool is *tight* on this build —
empirically even one extra `--mount` on top of project + state +
network + agentd trips `RegisterNetDevice(IrqsExhausted)` at boot,
with or without secrets registered. So the feature ships with a
warning eprintln rather than a client-side cap (libkrun configs
vary) and the error message is clear when the cap is hit. See
discovered upstream issue #3 in PLAN — the right long-term fix
is to raise libkrun's IRQ budget upstream.
**Clipboard bridge.**
- New `agent-vm clipboard {get,put}` subcommand (`clipboard.rs`).
`put` reads stdin (or `VALUE` arg) into `<state>/clipboard.txt`;
`get` prints the file content to stdout. `--sys` on either side
also exchanges with the host system clipboard via
`wl-copy/wl-paste`, `xclip`, or `pbcopy/pbpaste` — first one on
PATH wins.
- The launcher's existing `/agent-vm-state` bind exposes the file at
`/agent-vm-state/clipboard.txt` inside the guest. The in-VM agent
reads/writes that path directly; no in-guest helper script
needed.
**`agent-vm-ccusage` wrapper.**
- Standalone shell script at `bin/agent-vm-ccusage` that resolves
the state root via the same precedence the Rust launcher uses
(`$AGENT_VM_STATE_DIR` → `$XDG_STATE_HOME/agent-vm` →
`$HOME/.local/state/agent-vm`), enumerates per-project
`<hash>/claude` session dirs, and runs
`npx ccusage@latest "$@"` with `CLAUDE_CONFIG_DIR` set to the
comma-joined union of host + every project. Ported 1:1 from the
original.
**Chrome DevTools MCP.**
- `images/Dockerfile` adds `chromium` + `fonts-liberation` from the
Debian repo and the usual `google-chrome` / `google-chrome-stable`
/ `/opt/google/chrome/chrome` symlink dance so tools that hard-
code those paths find the binary.
- `secrets::write_default_claude_root_state` force-sets the
`mcpServers["chrome-devtools"]` entry in the guest's
`~/.claude.json` (`{"command": "npx", "args": ["-y",
"chrome-devtools-mcp@latest", "--headless=true",
"--isolated=true"]}`). User-set MCP servers are preserved
(merge semantics on the surrounding map). Opt out with
`AGENT_VM_NO_CHROME_MCP=1`.
Verified:
- `agent-vm clipboard put` then `agent-vm clipboard get` round-trips
on the host.
- Inside the guest with the rebuilt image: `which chromium` →
`/usr/bin/chromium`, `which google-chrome` →
`/usr/bin/google-chrome`, `chromium --version` → Chromium 148.
- `~/.claude.json` in the guest contains the chrome-devtools MCP
entry.
- `agent-vm-ccusage --help` exits cleanly, ccusage CLI loads.
- `--mount` parsed correctly; print + fail-on-missing work; boot
fails with the libkrun IRQ error as documented.
- 6/6 cargo tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Start a from-scratch rewrite of agent-vm on top of microsandbox. The original
Bash/Python tree is removed on this branch and continues to live on main until
the rewrite reaches v1.
This phase only sets up the build:
- PLAN.md and ARCHITECTURE.md document the phased roadmap and the
design-decision log; ARCHITECTURE grows after each phase.
- microsandbox added as a git submodule at vendor/microsandbox so the Phase 3
SecretValue::File extension can branch the fork in place.
- Cargo workspace at the root with crates/agent-vm/ as the binary crate.
- Hello-world main.rs that boots an alpine sandbox via the microsandbox SDK
to prove the wiring is correct end-to-end (cargo check passes).