The Phase 7 chrome-devtools MCP entry (npx ... --headless --isolated) reported `✓ Connected` to `claude mcp list` but every tool call returned either `Protocol error (Target.setDiscoverTargets): Target closed` or `net::ERR_CERT_AUTHORITY_INVALID`. Two distinct causes: 1. Chromium's user-namespace sandbox refuses to initialize as root (the guest's only user), so the browser dies before the CDP pipe is read. 2. Chromium on Linux ignores `/etc/ssl/certs/ca-certificates.crt` and only honours its built-in root store + the per-user NSS DB, so the microsandbox MITM CA isn't trusted by chromium even though curl/openssl trust it via the bundle. Both of these had naïve fixes (`--no-sandbox` and `--acceptInsecureCerts`) but we'd rather keep chromium's nested sandbox active (defence in depth against untrusted content the agent navigates to) and only trust *our* one CA (not every untrusted cert). Approach: - **Dedicated `chrome` user (UID 9999)** baked into the image via direct /etc/passwd + /etc/shadow + /etc/group edits (debian-slim has no `useradd`; the shadow entry is needed because `su` calls PAM and rejects users without one). Sudoers rule grants password- less `root -> chrome`. - **Wrapper `/usr/local/bin/agent-vm-chrome-mcp`** that re-execs the MCP under chrome via `sudo -u chrome -H -n`. The env allow-list preserves agentd-set NODE_EXTRA_CA_CERTS / SSL_CERT_FILE / CURL_CA_BUNDLE / REQUESTS_CA_BUNDLE / PATH; without these, sudo's default env_reset strips them and node fails `npx -y` with SELF_SIGNED_CERT_IN_CHAIN fetching from registry.npmjs.org. - **Pre-initialized empty NSS DB** at /home/chrome/.pki/nssdb (built at image time with `certutil -N --empty-password`) plus `libnss3-tools` package so the launcher's runtime `certutil -A` has something to add to. - **Launcher prelude (run.rs)** runs `certutil -A -t TC -n microsandbox -i /usr/local/share/ca-certificates/microsandbox-ca.crt` on every boot. Can't bake the CA into the image — agentd writes it into the guest at boot time, and the cert is per-boot. Add-by-name is idempotent so re-running on every launch is fine. - **MCP config (secrets.rs)** swaps `command: npx` for `command: /usr/local/bin/agent-vm-chrome-mcp` with the original npx + args passed through. No more `--no-sandbox`, `--disable-dev-shm-usage`, or `--acceptInsecureCerts`. Verified e2e inside a fresh sandbox: `take_snapshot` after `navigate_page https://example.com` returns the real "Example Domain" heading text; `ps -ef | grep chrome` shows chromium running as `chrome`, not root; NSS DB lists only `microsandbox` with `CT,,`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| bin | ||
| crates/agent-vm | ||
| images | ||
| vendor | ||
| .gitignore | ||
| .gitmodules | ||
| ARCHITECTURE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| PLAN.md | ||
| README.md | ||
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 pushetc. 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-permissionsset 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--mountor pass--no-git.handshake read id_offset: timed out—free -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/NAMEor 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.