Find a file
Evgeny Boger f40e6df2bc fix(phase7): run chrome-devtools MCP under a dedicated user with scoped CA trust
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>
2026-05-24 20:39:55 +03:00
bin fix: address remaining code-review findings (sweep) 2026-05-24 13:40:20 +03:00
crates/agent-vm fix(phase7): run chrome-devtools MCP under a dedicated user with scoped CA trust 2026-05-24 20:39:55 +03:00
images fix(phase7): run chrome-devtools MCP under a dedicated user with scoped CA trust 2026-05-24 20:39:55 +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 docs(plan): refine v1 scope; add Phase 5/6/7; renumber deferred phases 2026-05-24 04:59:46 +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.