Find a file
Evgeny Boger 7e8ad37589 release-npm: build guest agentd in build-agent-vm; make arm64 legs non-blocking
Two fixes so the v0.1.24 release can publish:

1. build-agent-vm was missing the "Build agentd from source (musl)" step
   that build-msb already has. With the SDK's `prebuilt` feature dropped,
   the filesystem crate embeds vendor/microsandbox/build/agentd at compile
   time, so `cargo build -p agent-vm` panics in build.rs ("agentd binary
   not found ... Run `just build-deps`") without it. Add the identical
   musl agentd build before the cargo build (mirrors ci.yml).

2. The arm64 (cross) legs are not yet shippable — the libkrunfw arm64
   kernel-config seed (config-libkrunfw_aarch64.patch) hasn't been ported
   and the arm64 multiarch dev-lib install is flaky. Mark the cross legs
   of build-agent-vm / build-msb / build-libkrunfw / package
   continue-on-error so they fail-tolerated instead of blocking the x64
   release, tolerate the missing arm64 artifact in publish, and skip any
   platform subpackage that has no built binary (its dir exists from the
   checkout but would otherwise publish a broken, binary-less package).
   arm64 is a cpu-gated optionalDependency of the main package, so x64
   installs are unaffected; the legs re-enable automatically once green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-24 01:43:34 +03:00
.cargo B2: ship a native aarch64-linux release binary via npm 2026-05-31 18:57:07 +00:00
.github release-npm: build guest agentd in build-agent-vm; make arm64 legs non-blocking 2026-06-24 01:43:34 +03:00
bin fix: address remaining code-review findings (sweep) 2026-05-24 13:40:20 +03:00
crates/agent-vm agent-vm: drop the stale --mount IRQ-pool advisory note 2026-06-23 22:04:32 +03:00
images Merge origin/main into bump-microsandbox-wb 2026-06-23 23:13:20 +03:00
libkrunfw-overrides Merge irq-multi-mount: lift --mount cap from ~11 to ~100+ on x86_64 2026-05-28 14:30:53 +00:00
npm-dist B2: ship a native aarch64-linux release binary via npm 2026-05-31 18:57:07 +00:00
profiling v0.1.20: cut launch latency — cache gh identity, non-blocking update banner, chrome CA off the hot path 2026-05-31 14:22:34 +00:00
vendor agent-vm: repoint vendor/microsandbox to the wb-clean line 2026-06-23 20:44:43 +03:00
.gitignore run: clarify --auto-publish help text + bump vendor/microsandbox 2026-05-29 13:41:28 +00:00
.gitmodules Phase 0: scaffold microsandbox-based rewrite 2026-05-17 17:05:07 +03:00
AGENTS.md v0.1.16: bump for --publish / --auto-publish / --allow-egress + AGENTS.md 2026-05-30 11:37:12 +00:00
ARCHITECTURE.md lift --mount cap from ~11 to ~100+ via split_irqchip + cmdline-size patch 2026-05-28 08:26:28 +00:00
Cargo.lock v0.1.24: microsandbox 0.5.7 (wb) + 7-feature integration + agentd clean teardown 2026-06-24 01:25:06 +03:00
Cargo.toml v0.1.24: microsandbox 0.5.7 (wb) + 7-feature integration + agentd clean teardown 2026-06-24 01:25:06 +03:00
LAUNCH-PROFILE.md v0.1.20: cut launch latency — cache gh identity, non-blocking update banner, chrome CA off the hot path 2026-05-31 14:22:34 +00:00
LICENSE first commit 2026-01-28 01:15:09 +01:00
PLAN.md PLAN: replace phase roadmap with remaining-work plan vs main 2026-05-30 20:31:06 +00:00
README.md README: document --publish / --auto-publish / --allow-egress / --allow-lan / --allow-host 2026-05-30 17:54:38 +00: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) — your user must be in the kvm group: sudo usermod -aG kvm $USER and re-login.
  • Node.js 18+ (already there if you use Claude Code / Codex CLI / OpenCode — they're all npm-distributed).

~/.microsandbox/lib/libkrunfw.so.5.x auto-installs on first launch.

Quick start

npm install -g @wirenboard/agent-vm        # or: npx @wirenboard/agent-vm <cmd>

agent-vm setup            # pulls the latest image from ghcr.io and verifies it boots

cd ~/your-project
agent-vm claude           # or codex / opencode / shell

The npm package bundles a prebuilt agent-vm binary, the patched msb, and libkrunfw. agent-vm finds them via current_exe()-relative paths, so a user's separate ~/.microsandbox/bin/msb (if any) never shadows the patched build.

Build from source

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
cargo build --release --manifest-path vendor/microsandbox/Cargo.toml \
    -p microsandbox-cli --bin msb
./target/release/agent-vm setup       # uses the locally-built msb sibling

agent-vm setup pulls ghcr.io/wirenboard/agent-vm-template:latest by default; pass --image localhost:5000/agent-vm-template:latest to use a local image you've built via images/build.sh.

Subcommands

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

Image release cadence

The base OCI image (ghcr.io/wirenboard/agent-vm-template:latest) is rebuilt hourly by CI, picking up the latest Claude Code, Codex CLI, and OpenCode releases automatically. Pin a specific build with --image ghcr.io/wirenboard/agent-vm-template:YYYY-MM-DDTHH (date tags are immutable; the last 14 days are retained).

The agent-vm binary and the image are version-locked through an image-API-version integer (/etc/agent-vm-image-version inside the image). Mismatch → clean error at launch instead of mysterious in-VM failures.

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 (one virtio-fs each, ~210 mount headroom)

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

Env-var knobs (all opt-in; set to any value, empty included):

var what
RUST_LOG tracing filter; default warn. e.g. RUST_LOG=agent_vm=debug
AGENT_VM_PROFILE print per-phase wall-time (create/run/stop/remove)
AGENT_VM_DEBUG_CONFIG dump the SandboxConfig JSON before boot
AGENT_VM_NO_CHROME_MCP skip the Chrome DevTools MCP entirely (no entry in claude.json, no chrome-user setup at boot)
AGENT_VM_IMAGE_TAG override the OCI image (same as --image)
AGENT_VM_MEMORY_GIB / AGENT_VM_CPUS same as --memory / --cpus

Chrome DevTools MCP

The image ships chromium and a chrome-devtools MCP entry pinned to chrome-devtools-mcp@1.0.1. To keep chromium's nested user-namespace sandbox active (we'd rather not pass --no-sandbox) the MCP runs as a dedicated chrome user via a sudo wrapper at /usr/local/bin/agent-vm-chrome-mcp. The launcher installs the per-boot microsandbox MITM CA into chrome's NSS DB at startup so chromium accepts the intercepted TLS chain without --acceptInsecureCerts (which would trust any untrusted cert).

If the CA install fails (e.g. someone broke the in-image sudoers rule) the launcher prints a warning naming the symptom — without it, every HTTPS navigate would silently return ERR_CERT_AUTHORITY_INVALID. Set AGENT_VM_NO_CHROME_MCP=1 to skip the whole setup.

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.

Ports & egress

The default network policy (public_only) lets the guest reach the public internet plus DNS, and denies everything else (loopback, RFC1918 LAN, link-local, cloud-metadata, the host). Open holes per-launch with these flags — they compose:

flag what it opens guest-side address
--publish HOST:GUEST[/proto] host port HOST → guest port GUEST (tcp default; /udp for UDP) inbound to the guest
--auto-publish every 0.0.0.0:* / 127.0.0.1:* listener inside the guest is mirrored to the host loopback (Lima-style) host: 127.0.0.1:<guest-port>
--allow-egress IP|CIDR (repeatable) one IP or one CIDR through the egress deny dial directly by IP
--allow-lan the whole DestinationGroup::Private (10/8, 172.16/12, 192.168/16, 100.64/10, fc00::/7) dial any LAN IP
--allow-host the per-sandbox gateway IP, which the smoltcp stack rewrites to host 127.0.0.1 host.microsandbox.internal:<port> (already in guest /etc/hosts)

Loopback (guest's own 127.0.0.1), link-local, and cloud metadata (169.254.169.254) stay denied even with --allow-lan — they're disjoint groups by design. --allow-host is the narrowest way to reach a dev server bound to host 127.0.0.1; --allow-lan is the broadest. A compromised in-guest process gets full access to whatever you open, so prefer the narrowest flag that fits.

Troubleshooting

  • RegisterNetDevice(IrqsExhausted) at boot — the userspace split irqchip raises the cap to ~219 IRQs, so this should only happen with hundreds of --mounts or on a host whose KVM lacks KVM_CAP_SPLIT_IRQCHIP (pre-Linux 4.7 or some nested-virt / seccomp-restricted setups). Drop a --mount to recover.
  • 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.
  • AGENTS.md — conventions for coding agents (Claude Code, Codex, etc.) working on this repo: post-merge version bump, submodule-merge ordering, what not to do.