Find a file
Evgeny Boger ce745ece6d post-review cleanup: drop broken observability, tighten chrome MCP, finish placeholder rename
Folds in fourteen findings from the code review of the previous two
commits. Most are 'the commit advertised something it didn't actually
deliver' — straightforward to make honest.

**Observability commit was a no-op — revert it.**

- `main.rs` filter went back to the simple `warn` default. The previous
  `microsandbox_network::proxy=info,microsandbox_network::tls::proxy=info`
  targeted modules that (a) only emit `tracing::debug!` (so info
  filtered them out) and (b) run in the `msb sandbox` subprocess whose
  own tracing init reads no env at all. Net behaviour: zero proxy
  logs surfaced; the doc-comment claim was wishful thinking.

- `run.rs` post-mortem hint trimmed to one accurate line. Dropped the
  pointers to `msb.stderr.log` and `msb-exit.log` (vendor `spawn.rs`
  inherits stderr instead of teeing; `handle.rs::wait` writes no exit
  log). What remains: a single `(see <runtime.log> for the in-VM tail)`
  appended via `with_context`. Avoids the duplicated error output the
  old code produced — anyhow's main termination prints the chain once
  rather than the previous "eprintln before return then re-print on
  exit" combo.

- `runtime_log_path()` replaces `msb_sandbox_log_dir`; comment no
  longer claims to "mirror" upstream, and the final fallback matches
  `microsandbox_utils::resolve_home()` (`./.microsandbox`) rather than
  the previous incorrect `/var/lib/microsandbox`. Hint now also fires
  in the non-TTY branch's `with_context` and `anyhow::bail!` paths,
  not just TTY — CI users are the ones most likely to need the
  pointer.

**Chrome MCP cleanups.**

- `certutil` trust string `-t TC` → `-t C,,`. The previous form set
  the SSL column to `TC` (T = trusted issuer of *client* certs, C =
  trusted issuer of server certs). We only want server-cert trust;
  the T silently broadened scope. `C,,` is the textbook idiom.

- Wrapper's `--preserve-env` allow-list expanded to forward the env
  vars chrome-devtools-mcp itself documents
  (`CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS`, `CI`, `DEBUG`), agentd-
  set proxy config (`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` plus
  lowercase variants), and basic locale (`TZ`, `LANG`, `LC_ALL`,
  `TMPDIR`). Without these, user MCP-config `env:` blocks and
  documented opt-outs were silently stripped by sudo's `env_reset`.

- Wrapper now `cd /home/chrome` before exec'ing. Inherited cwd was
  `/workspace` (the project bind-mount), which the chrome user (UID
  9999) cannot write — any MCP tool emitting a relative path
  (`./trace.json`, `./screenshot.png`, `--logFile=./debug.log`)
  EACCES'd silently. `/home/chrome` is chrome-owned and writable.

- Pre-warm `chrome-devtools-mcp@latest` into chrome's npm cache at
  image build time (`/home/chrome/.npm/_npx/...`). Previously every
  first MCP call paid a multi-second `npx -y` fetch and broke
  entirely when registry.npmjs.org was unreachable. The image gains
  ~21 MiB; cold launches drop the fetch.

- Bash prelude's certutil block is now gated on `AGENT_VM_NO_CHROME_MCP`
  not being set. Previously the env-var opt-out only suppressed the
  MCP entry in `claude.json`; the sudo→chrome→certutil round-trip
  ran on every launch regardless, exposing the opt-out user to the
  exact sudoers/NSS failure modes they were trying to avoid.

**Sticky opt-out fixed.** `write_default_claude_root_state` now
always owns the `chrome-devtools` key: `AGENT_VM_NO_CHROME_MCP=1`
explicitly `mcp.remove("chrome-devtools")`s a previously-written
entry instead of leaving it sticky in the on-disk merged JSON.

**OPENCODE placeholder rename completed.** `OPENCODE_OPENAI_ACCESS_PLACEHOLDER`
JWT signature is now `msb-opencode-placeholder-a-v2` (was
`MSB_OPENCODE_v1`), matching the rename rationale spelled out on
`OPENAI_ID_PLACEHOLDER`: non-token-shaped sentinels so Anthropic
doesn't flag transcripts that contain them.

**Stale literals swept.** `ARCHITECTURE.md` reads the new names at
all five reference sites. Six tests in `intercept_hook.rs` that
hardcoded `MSB_PLACEHOLDER_GH_TOKEN_v1[_xxxxxxxx]` as a standalone
fixture now use `secrets::GH_TOKEN_PLACEHOLDER` directly — so a
future placeholder shape change is exercised by the existing
regression assertions instead of slipping past them.

End-to-end verified inside a fresh sandbox:
- `claude mcp list` → ✓ Connected
- `navigate_page` https://example.com + `take_snapshot` returns the
  real "Example Domain" content
- NSS DB lists only the microsandbox CA with trust `C,,` (not `CT,,`)
- `chrome-devtools-mcp` package present in /home/chrome/.npm/_npx/
  out of the box (no per-launch fetch)
- `AGENT_VM_NO_CHROME_MCP=1`: claude.json mcpServers is `{}` (entry
  removed, not just absent on new files); NSS DB is empty (certutil
  prelude skipped)
- Wrapper preserves CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS, CI,
  DEBUG, HTTP_PROXY, TZ, NODE_EXTRA_CA_CERTS across the sudo hop
- 73 cargo unit tests pass (incl. the intercept_hook tests now using
  the constant)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:41:53 +03:00
bin fix: address remaining code-review findings (sweep) 2026-05-24 13:40:20 +03:00
crates/agent-vm post-review cleanup: drop broken observability, tighten chrome MCP, finish placeholder rename 2026-05-25 00:41:53 +03:00
images post-review cleanup: drop broken observability, tighten chrome MCP, finish placeholder rename 2026-05-25 00:41:53 +03:00
vendor vendor: bump msb to surface boot-time panics in agent-vm errors 2026-05-24 19:29:23 +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 post-review cleanup: drop broken observability, tighten chrome MCP, finish placeholder rename 2026-05-25 00:41:53 +03:00
Cargo.lock fix(phase6): close keep-alive bypass in github smart-HTTP hook 2026-05-24 18:30:41 +03:00
Cargo.toml cargo: relax release profile, add dist profile for distribution builds 2026-05-24 17:00:53 +03:00
LICENSE first commit 2026-01-28 01:15:09 +01:00
PLAN.md docs(plan): record Phase 6 follow-up commits (gh scope, sweep, github.com streaming) 2026-05-24 14:21:20 +03:00
README.md docs(readme): rewrite — tighten, drop status/why, add subcommand table 2026-05-24 14:52:10 +03:00

agent-vm

Run Claude Code / Codex / OpenCode inside a per-project libkrun microVM, booting in ~2 seconds, with:

  • Host OAuth tokens never enter the VM. The TLS-intercept proxy in microsandbox substitutes the real bearer for a placeholder on the way out. OAuth refresh is MITM'd so multi-hour sessions survive token rotation.
  • Per-launch GitHub repo allow-list. Auto-detected from git remote -v; extend with --repo OWNER/NAME. gh pr create, git push etc. are filtered at the proxy — off-list calls get a 403 before they reach GitHub.
  • Sandbox is the boundary. Root inside the VM, project bind-mounted at its host path, --dangerously-skip-permissions set by default (the microVM is the only thing actually keeping the agent on rails).

This is the Rust rewrite of the original Bash wirenboard/agent-vm on top of microsandbox. Living on rewrite-microsandbox until v1.

Requirements

  • Linux with /dev/kvm (rw)
  • Docker, for building the base image + running the local registry
  • Rust toolchain (rustup stable)
  • libcap-ng-dev, libdbus-1-dev, pkg-config

~/.microsandbox/{bin/msb, lib/libkrunfw.so.5.x} auto-install on first launch.

Quick start

git clone -b rewrite-microsandbox https://github.com/wirenboard/agent-vm
cd agent-vm
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

"$BIN" setup                    # build + push image, build patched msb

cd ~/your-project
"$BIN" claude                   # or codex / opencode / shell

Subcommands

claude | codex | opencode | shell   launch an agent in a per-project sandbox
pull                                refresh the cached image
setup                               build base image + patched msb
clipboard {get,put} [--sys]         exchange a string with the project sandbox

Each launcher accepts:

flag what
--memory N VM memory 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 skip gh/git auth injection (still respects --repo)
--repo OWNER/NAME add to the GitHub allow-list (repeatable)
--mount HOST[:GUEST] extra bind mount (subject to libkrun IRQ cap)

Trailing args go to the agent: agent-vm claude -p "say hi", agent-vm shell -- -c 'cargo test'.

Credentials

Reads from the host:

  • ~/.claude/.credentials.json (Claude)
  • ~/.codex/auth.json (Codex, OpenCode)
  • gh auth token (git/gh)

The guest gets placeholder strings; the proxy substitutes on the wire. Real tokens live in ${XDG_STATE_HOME}/agent-vm/<hash>.secrets/ (0700) on the host, outside the bind mount the guest sees. A SHA-256 snapshot of the three credential files is taken at launch and re-checked on exit; unexpected mutations print a warning.

For Claude/Codex, when the in-VM agent's bearer expires the hook MITMs the OAuth refresh, runs claude -p/codex exec on the host to rotate, and feeds the new placeholder back to the guest — no re-attach required.

Project hook

If the project root contains an executable .agent-vm.runtime.sh, the launcher sources it inside the guest before exec'ing the agent. Use for npm install, env exports, dev-server startup. Non-zero exit aborts the launch.

Troubleshooting

  • RegisterNetDevice(IrqsExhausted) at boot — libkrun's virtio IRQ pool is saturated by project + state + net + secrets. Drop a --mount or pass --no-git.
  • handshake read id_offset: timed outfree -h; the VM needs more memory than is available. Try --memory 1.
  • GitHub 403 from the proxy — repo isn't in the allow-list. Pass --repo OWNER/NAME or run from a project with the right remote.

See also

  • PLAN.md — phased roadmap, what's done, what's deferred.
  • ARCHITECTURE.md — design notes; why things look the way they do.