Find a file
Evgeny Boger b58165ffe1 test+feat: expand test coverage; per-project .bash_history persistence
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>
2026-05-24 15:58:07 +03:00
bin fix: address remaining code-review findings (sweep) 2026-05-24 13:40:20 +03:00
crates/agent-vm test+feat: expand test coverage; per-project .bash_history persistence 2026-05-24 15:58:07 +03:00
images feat(phase7): --mount, clipboard bridge, ccusage wrapper, Chrome MCP 2026-05-24 06:04:08 +03:00
vendor fix(phase6): close github.com smart-HTTP bypass via streaming intercept 2026-05-24 14:20:06 +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 feat(phase5): OpenCode auth + host-cred security snapshot 2026-05-24 05:23:28 +03:00
Cargo.toml Phase 0: scaffold microsandbox-based rewrite 2026-05-17 17:05:07 +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.