Tests (+30, total 59):
**run::tests** (+16) — `parse_github_slug` matrix (https/http/scp/ssh
URL/ssh-with-port forms, extra path components, non-github URLs,
`.git` suffix stripped exactly once, empty owner/repo, double `.git`);
`parse_extra_mounts` (mirror, remap, relative-path rejection,
empty-side, nonexistent host); `mkdir_chain` (root, single segment,
prefix walk); `guest_path_is_safe` (normal paths, tmpfs prefixes,
lookalike non-prefix matches like /tmpfoo).
**secrets::tests** (+7) — `host_secret_dir` safe across trailing-
slash state_dirs (closes a code-review concern); placeholder-pairwise-
distinctness sanity (catches a future placeholder that's a substring
of another — would silently swap the wrong token);
`hash_file` deterministic + None on missing; `decode_id_token_account`
URL-safe + standard-alphabet fallback + missing-field paths;
`write_default_opencode_config` first-write defaults + merge-preserves-
user-`model`.
**intercept_hook::tests** (+6) — `parse_http_request`: basic GET, POST
with body, header value with embedded colons, missing-separator error,
empty-request-line error, whitespace-trimmed header values.
Also fixes a `.git`-suffix greediness bug in `parse_github_slug`
(same shape as the one already fixed in `github_smart_decision`):
`trim_end_matches(".git")` was greedy, so a `repo.git.git` URL
yielded slug `o/repo` instead of `o/repo.git`. Use `strip_suffix`
to strip exactly one suffix; the new test
`parse_github_slug_handles_dot_git_only_once` would have caught a
regression.
Feature: persistent per-project bash history.
`agent-vm shell` now preserves bash history across launches, scoped
to the project. `secrets::refresh` touches
`<state>/bash_history` if missing; the launcher patch builder
symlinks `/root/.bash_history -> /agent-vm-state/bash_history` so
bash's default HISTFILE points at the per-project state. Subsequent
launches inherit whatever `bash` appended on exit (clean `exit` or
Ctrl-D — Ctrl-C of the launcher won't save).
Verified end-to-end:
- 1st session writes `echo foo >> /root/.bash_history` → host file
contains foo;
- 2nd session reads it + appends another entry → host file
accumulates;
- Symlink target matches: `lrwxrwxrwx ... /root/.bash_history ->
/agent-vm-state/bash_history`.
59/59 tests green.
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.