Find a file
Evgeny Boger 29c0ccc6c7 fix: address remaining code-review findings (sweep)
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>
2026-05-24 13:40:20 +03:00
bin fix: address remaining code-review findings (sweep) 2026-05-24 13:40:20 +03:00
crates/agent-vm fix: address remaining code-review findings (sweep) 2026-05-24 13:40:20 +03:00
images feat(phase7): --mount, clipboard bridge, ccusage wrapper, Chrome MCP 2026-05-24 06:04:08 +03:00
vendor secrets: inject_basic_auth=false on agent-vm secrets so the fast path fires 2026-05-18 02:14:49 +03:00
.gitignore Phase 0: scaffold microsandbox-based rewrite 2026-05-17 17:05:07 +03:00
.gitmodules Phase 0: scaffold microsandbox-based rewrite 2026-05-17 17:05:07 +03:00
ARCHITECTURE.md docs(plan): refine v1 scope; add Phase 5/6/7; renumber deferred phases 2026-05-24 04:59:46 +03:00
Cargo.lock feat(phase5): OpenCode auth + host-cred security snapshot 2026-05-24 05:23:28 +03:00
Cargo.toml Phase 0: scaffold microsandbox-based rewrite 2026-05-17 17:05:07 +03:00
LICENSE first commit 2026-01-28 01:15:09 +01:00
PLAN.md docs(plan): mark Phase 5/6/7/9 status after the implementation pass 2026-05-24 06:09:24 +03:00
README.md feat(phase9): auto-install runtime, --image / --no-update-check / runtime hook, README 2026-05-24 06:08:31 +03:00

agent-vm (microsandbox rewrite)

Run AI coding agents (Claude Code, Codex CLI, OpenCode) inside per-project microVMs that boot in ~2 seconds, with the host's real OAuth tokens never entering the VM and a per-launch GitHub-repo allow-list for git push / gh pr create.

This is the in-progress rewrite of wirenboard/agent-vm on top of microsandbox. The original Bash implementation continues to live on main; the rewrite lives on rewrite-microsandbox.

Why

The original agent-vm is mature but heavy: ~30 s cold start, 16 GB disk template, host-side mitmproxy chain, balloon daemon, custom GitHub App. This rewrite uses microsandbox's libkrun-backed microVMs and built-in TLS-intercept

  • placeholder-substituted secrets. ~2 s launches, ~17 MB runtime, no host proxy, and the "real tokens never enter the VM" guarantee is enforced at the network layer instead of by a Python middlebox.

Status

Phases 07 + 9 are done. Each phase is one PR; see PLAN.md for the running roadmap.

  • 02.x: scaffolding, image, launcher MVP, polish.
  • 34: host-rooted Claude / Codex credentials with OAuth-refresh support.
  • 5: OpenCode auth + host-cred mutation snapshot.
  • 6: gh / git credential injection with per-launch repo allow-list at the proxy.
  • 7: --mount, clipboard bridge, ccusage wrapper, Chrome DevTools MCP.
  • 8: fast-launch via detached mode (deferred — product-shape decision).
  • 9: distribution + polish + docs.

Verified end-to-end on Linux/KVM against real Claude (Anthropic) and Codex (ChatGPT OAuth) accounts.

Requirements

  • Linux with KVM (/dev/kvm readable/writable) — primary target. macOS-VZ works at the microsandbox level but isn't exercised here.
  • Docker, for building the base OCI image and running the local registry.
  • A Rust toolchain (rustup stable) to build agent-vm + the patched msb.
  • libcap-ng-dev and libdbus-1-dev on the build host (transitive deps of the microsandbox crate).

The microsandbox runtime libs (~/.microsandbox/{bin/msb, lib/libkrunfw.so.5.x}) are auto-installed on first launch — you don't need to download them by hand.

Build

git clone https://github.com/wirenboard/agent-vm
cd agent-vm
git checkout rewrite-microsandbox
git submodule update --init vendor/microsandbox
sudo apt-get install -y libcap-ng-dev libdbus-1-dev pkg-config
cargo build --release -p agent-vm
BIN=$(pwd)/target/release/agent-vm

agent-vm setup builds the base image (localhost:5000/agent-vm:latest), the patched msb from vendor/microsandbox, and verifies the image by booting a throwaway sandbox:

"$BIN" setup

Use

cd ~/your-project
"$BIN" claude        # Claude Code TUI
"$BIN" codex         # Codex CLI TUI
"$BIN" opencode      # OpenCode TUI
"$BIN" shell         # bash inside the sandbox

# One-shots (no TTY, output streamed live):
"$BIN" claude -p "fix the lint errors"
"$BIN" codex exec --skip-git-repo-check "Reply OK"
"$BIN" shell -- -c 'cargo test'

All four subcommands accept:

  • --memory N — VM memory in GiB (default 2)
  • --cpus N — vCPUs (default 2)
  • --image REF — override the OCI image
  • --no-update-check — skip the registry HEAD on launch
  • --no-git — opt out of gh / git injection
  • --repo OWNER/NAME — add to the GitHub allow-list (repeatable)
  • --mount HOST[:GUEST] — extra bind mount (subject to libkrun IRQ cap)

Plus:

  • "$BIN" pull — fetch the latest image into the microsandbox cache.
  • "$BIN" clipboard {get,put} [--sys] — exchange a string with the per-project sandbox via <state>/clipboard.txt (mounted at /agent-vm-state/clipboard.txt in the guest). --sys also pulls from / pushes to the host system clipboard.
  • bin/agent-vm-ccusage — wrapper that unions ~/.claude and every per-project session dir before running npx ccusage@latest.

Credentials

  • Claude: reads ~/.claude/.credentials.json. Sends a placeholder bearer from the guest; the proxy swaps to the real token on outbound api.anthropic.com traffic. OAuth refresh is MITM'd into a host-side claude -p invocation so long sessions survive token rotation.
  • Codex: reads ~/.codex/auth.json. Same model, with OpenAI's id_token JWT replaced with an alg-none placeholder so no PII enters the guest.
  • OpenCode: synthesizes an OpenCode-shaped auth.json from the host Codex credentials (same OpenAI account); a synthetic placeholder JWT goes into tokens.openai.access and is substituted on the wire.
  • gh / git: if gh auth status shows you're logged in, the host token is injected into the guest's ~/.gitconfig and ~/.config/gh/hosts.yml as a placeholder. The proxy substitutes for the real bearer on outbound traffic to GitHub.
    • Per-launch repo allow-list. git remote -v of the cwd gives the initial set; --repo OWNER/NAME widens it; api.github.com requests outside the list get a synthesized 403 from the proxy hook.

Real tokens live in ${XDG_STATE_HOME}/agent-vm/<hash>.secrets/ (mode 0700) on the host, outside the bind mount the guest sees. A SHA-256 snapshot of ~/.claude/.credentials.json, ~/.codex/auth.json, and ~/.local/share/opencode/auth.json is taken at launch and re-checked on exit; any mutation outside the Phase 4 refresh path prints a warning.

Project hook

If the project root contains an executable .agent-vm.runtime.sh, the launcher sources it inside the guest at the project's bind path before exec'ing the agent. Use it for npm install, env exports, dev-server startup, etc.

# .agent-vm.runtime.sh
set -e
npm install --silent
export FOO=bar

Troubleshooting

  • RegisterNetDevice(IrqsExhausted) on boot: libkrun's IRQ pool is tight. Drop a --mount or pass --no-git.
  • runtime error: handshake read id_offset: timed out: check free -h — boot needs more memory than the host has free. Try --memory 1.
  • Codex hangs at "Reading additional input from stdin...": kill, re-run. The launcher forces stdin to /dev/null in non-TTY mode; if you piped real input, codex consumed it before printing the banner.
  • gh api returns "Bad credentials" but the same call works on host: you're in a doubly-nested setup where an outer agent-vm bridge replaced your host token with its own placeholder. The inner substitution is working; the outer bridge isn't re-substituting on the nested VM's egress. Run from a non-nested host.

Docs

  • PLAN.md — phased roadmap with what landed when and what's still open.
  • ARCHITECTURE.md — per-phase design notes; the source of truth for why something looks the way it does.