Commit graph

42 commits

Author SHA1 Message Date
Evgeny Boger
05e8969215 v0.1.25: route guest egress through host HTTP proxy (CONNECT)
Some checks failed
CI / build-and-test (push) Has been cancelled
Headline changes since 0.1.24:
- network: all guest egress (HTTPS/HTTP/image pulls) is re-originated through
  the configured host HTTP proxy via CONNECT, honouring HTTPS_PROXY/HTTP_PROXY/
  ALL_PROXY and NO_PROXY, while preserving the TLS-intercept egress policy.
- fails closed: a configured-but-unreachable proxy errors instead of silently
  dialing direct.
- security: reject control chars/whitespace in the CONNECT target host and in
  parsed SNI, closing a guest-controlled CRLF/header-injection vector.
- run: verbose startup banner reporting the proxy route (and NO_PROXY bypass).
- vendor/microsandbox bumped to wb merge of microsandbox#4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:32:16 +03:00
Evgeny Boger
281d82ddb3 v0.1.24: microsandbox 0.5.7 (wb) + 7-feature integration + agentd clean teardown
Headline changes since 0.1.23:
- vendor/microsandbox bumped to the canonical wb line (0.5.7), agentd source
  built from the fork (no upstream prebuilt); SDK prebuilt download dropped.
- agentd exits its init/workload on shutdown instead of reboot(RB_POWER_OFF) —
  teardown ~8.4s -> ~0.45s, data-safe.
- 7-feature integration: GitHub Copilot agent (D1), baked LSP plugins (D2,
  seeded past the persistence symlink), token rotation + single-flight (A1/A2),
  CI smoke (B1), aarch64 binary (B2), DNS family ordering (B3).
- image: reproducible :latest manifest, version-keyed agent layers, WB diag
  CLIs + codestyle-pinned Python linters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-24 01:25:06 +03:00
Evgeny Boger
0d9d86e19a agent-vm: drop the microsandbox SDK prebuilt feature
The SDK's default `prebuilt` feature made `cargo build -p agent-vm` download the
upstream msb+libkrunfw bundle from superradcompany. agent-vm ships its own
from-source msb, so disable it (default-features = false, keep net + keyring).
Builds now require a local build/agentd (`just build-agentd`), matching the
release workflow; Cargo.lock loses the download-only deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-23 19:03:55 +03:00
Evgeny Boger
ea5821594e agent-vm: bump vendor/microsandbox to wb (rebased onto upstream)
Bumps the vendor/microsandbox submodule from the old wirenboard fork
(702f52a) to the wb branch — all downstream agent-vm features re-ported
onto current upstream microsandbox (sdk/rust @ upstream/main 69803ab3).
See wirenboard/microsandbox @ wb.

Two adaptations forced by upstream API/layout changes:
- Cargo.toml: the SDK crate moved crates/microsandbox -> sdk/rust.
- run.rs: Image::get now takes a LocalBackend handle (seed_pulled_marker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:58:07 +00:00
Evgeny Boger
b2cadc56d8 v0.1.23: msb jemalloc allocator (fix host RSS balloon)
Bumps the vendored msb to use jemalloc as its global allocator. On
many-core hosts glibc's per-thread malloc arenas (8*ncpu, 64 MiB each)
are retained and never returned to the OS under the sandbox supervisor's
Rust worker-thread churn (virtio-fs / block / blocking pools), ballooning
host RSS to 10-20+ GiB even for a `--memory 4` sandbox — the guest RAM is
a separate, correctly-capped mapping. jemalloc (background_threads on)
purges freed pages back to the OS, so host RSS tracks the live set.

Submodule bump: vendor/microsandbox -> msb-jemalloc-v0123 (jemalloc-only
diff on the v0.1.22 base). Verified: a fresh sandbox under heavy file/exec
churn held 0 glibc 64-MiB arenas at ~300 MiB RSS vs a live glibc sandbox
at 408 arenas / 5.2 GiB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:45:40 +03:00
Evgeny Boger
31fa95b67c v0.1.22: bump for Cyrillic path support
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:30:20 +00:00
Evgeny Boger
60a75ae0fc v0.1.21: bump for help review fixes
Ships the follow-up to the v0.1.x `--help` regroup: a code review of
that change surfaced one functional doc bug and several accuracy /
clarity gaps, all fixed in the merged `help: fix review findings`
commit.

  - `--help` example `agent-vm shell -- -c 'cargo test'` was wrong (the
    launcher already wraps a Shell agent's trailing args in `bash -c`,
    so the literal `-c` double-applied -> `bash: -c: command not found`).
  - the shared launch footer is now labelled "(claude shown;
    codex/opencode/shell take the same options)" instead of claiming to
    be command-agnostic while showing only claude examples.
  - Environment section gained AGENT_VM_STATE_DIR and
    AGENT_VM_INSECURE_REGISTRY (both honoured on the launch path).
  - `--publish` value_name is now `[BIND:]HOST_PORT:GUEST_PORT`.
  - networking cheatsheet rewritten to fit 80 cols under wrap_help.
  - crate `description` aligned to "microVMs".

Presentation-only; no flag, env var, or behaviour changed.
`cargo build -p agent-vm` on the merged tree: clean; binary reports
0.1.21. `cargo test -p agent-vm`: 123 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:38:30 +03:00
Evgeny Boger
6513050a31 v0.1.20: cut launch latency — cache gh identity, non-blocking update banner, chrome CA off the hot path
Profiling (LAUNCH-PROFILE.md) found the launch was dominated by PRE-BOOT host
work, not the VM boot. Three fixes, ~2.5s -> ~1.8s on a warm launch:

- secrets.rs: cache the host git identity (24h TTL, validated strings only).
  `gh api user` was an uncached ~1.29s HTTPS round-trip on every launch's
  critical path (added in v0.1.14) — the main 1.3s->2.5s regression.
- run.rs: spawn the ghcr.io update-banner check instead of awaiting it
  (~0.9s off the hot path; banner still fires, just doesn't block boot).
- run.rs + images/Dockerfile: move the chrome-MCP MITM-CA `certutil` import
  out of the per-launch in-guest prelude into the agent-vm-chrome-mcp wrapper.
  It now runs once at MCP startup (as the chrome user that owns the NSS DB),
  off the launch path and skipped when chrome is unused (~270ms). Verified
  end-to-end: the proxy serves a `microsandbox CA`-signed cert that validates
  against the CA the wrapper imports.

Also: kernel A/B showed the nested-virt libkrunfw rebuild adds only ~190ms
(KVM ~100, conntrack ~62); DEFERRED_STRUCT_PAGE_INIT and split_irqchip have no
wall-clock effect. Build/measure scripts under profiling/.

NOTE: the Dockerfile (wrapper) and binary (prelude removal) must ship together
— the image must be rebuilt to :latest with the new wrapper before this binary
is published, or chrome MCP loses its CA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:22:34 +00:00
Evgeny Boger
f881e073c1 help: regroup and tighten the CLI usage output
The launch verbs (claude/codex/opencode/shell) share one `run::Args`,
and every flag carried its full multi-sentence explanation as the
*short* help. clap renders the first doc paragraph as `-h` text, so
`-h` was a wall: 13 flags, each 3-8 lines of run-on prose. Worse, the
crate built clap without `wrap_help`, so `--help` printed each long
paragraph as one unwrapped line — the terminal soft-wrapped it, but
copy/paste and narrow widths showed a single giant line per flag.

Split short vs long help, and group the flags:

  - Every option now leads with a terse one-line summary (the `-h`
    text); the original detail is kept as follow-on paragraphs that
    only show under `--help`.
  - Flags are bucketed with `help_heading`: Sandbox resources /
    GitHub access / Mounts & ports / Network egress / Image.
  - Added the `wrap_help` clap feature so `--help` prose wraps to the
    terminal width instead of emitting one line per paragraph.
  - Real value names: <GIB>, <N>, <HOST[:GUEST]>, <[BIND:]HOST:GUEST>,
    <IP|CIDR>, <OWNER/REPO>, <REF>.
  - `--help` gains an Examples block, a host<->guest Networking
    cheatsheet, and an Environment section (vars verified against the
    code).

Top level: wire up `-V/--version` (it errored before), add a
"Getting started" footer, make the subcommand descriptions terse and
parallel, and drop the redundant "See `clipboard --help`" line.
setup/pull `--image` get the same terse-first-line split.

Pure presentation change — no flag, env var, or behaviour added or
removed. `cargo test -p agent-vm`: 123 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 19:08:11 +00:00
Evgeny Boger
0fee58a8b9 v0.1.18: run: add --allow-host for host's 127.0.0.1 via host.microsandbox.internal
New `--allow-host` flag opens DestinationGroup::Host in the egress
policy, which is the group addr_classify reserves for the per-
sandbox gateway IP (the one resolve_host_dst rewrites to the
host's 127.0.0.1 at dial time). End result: a service bound to
`127.0.0.1:8080` on the host is reachable from inside the guest
as `host.microsandbox.internal:8080` — same name agentd already
puts in `/etc/hosts`, no extra setup.

Scope: opens the gateway IPs (v4 + v6) only. Not the host's LAN
IP, not the guest's own loopback, not metadata. Composes with
--allow-egress / --allow-lan / --publish / --auto-publish.

Security note in --help mirrors --allow-lan's: anything bound to
host loopback (admin UIs, dev DBs, TCP-exposed Docker socket)
becomes reachable to a possibly-compromised in-guest process.

Vendor bump (microsandbox 2dbfa38) carries the
allow_egress_group(Host)-opens-gateway-only test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 15:11:25 +00:00
Evgeny Boger
0de7159e0e v0.1.17: bump for the ghcr.io update-banner fix
Workspace version bump after merging `investigate-image-check`: the
launch-time "newer image available" probe now performs the OCI registry
Bearer-token handshake, so the banner actually fires against the default
ghcr.io image (it was silently 401'ing and never showing). Also bounds the
probe with an overall 5s budget so the extra round-trips can't stall boot.

Cargo.lock refreshed by the post-bump build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:24:29 +00:00
Evgeny Boger
17b6e5bb91 v0.1.16: bump for --publish / --auto-publish / --allow-egress + AGENTS.md
Workspace version bump for the merged
`worktree-vm-network-investigation` branch (Lima-style host ← guest
port mirroring + per-launch egress policy hole-punching).

New AGENTS.md captures the conventions I keep forgetting:
- bump workspace version after every feature merge
- merge submodule before superproject when both move
- don't relocate build output to tmpfs
- don't `rm -rf` inline from tool calls (use scripts)
- commit-message style on this branch is multi-paragraph Why/How

README now points at AGENTS.md alongside PLAN.md / ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 11:37:12 +00:00
Evgeny Boger
1479f5b8f3 Merge worktree-vm-network-investigation: --publish + --auto-publish + --allow-egress / --allow-lan
Brings in:
- `--publish HOST:GUEST[/proto]` for host → VM port forwarding (incl. [::1]
  IPv6 bind form; /udp rejected).
- `--auto-publish` for Lima-style host ← guest port mirroring (mirrors
  every 0.0.0.0:* and 127.0.0.1:* listener inside the guest to the host).
- `--allow-egress <IP|CIDR>` (repeatable) and `--allow-lan` to punch
  per-launch holes through the default `public_only` egress policy.

Vendor submodule bumped to f56b822 (merge of auto-publish into
agent-vm-secret-file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

# Conflicts:
#	vendor/microsandbox
2026-05-29 17:19:43 +00:00
Evgeny Boger
c60b76597f run: --allow-egress <IP|CIDR> + --allow-lan to open egress per-launch
Two new CLI flags to punch holes through the default `public_only`
policy without forcing users to rebuild a full NetworkPolicy:

  --allow-egress 10.100.1.75
  --allow-egress 10.100.1.0/24      # CIDR
  --allow-egress fd00::1/128        # IPv6
  --allow-egress [::1]              # bare IP gets a /32 or /128
  --allow-lan                       # opens the entire Private group

Repeatable; per-IP and group flags compose. Loopback, link-local,
and metadata groups stay denied even under --allow-lan — Private
is intentionally disjoint from those.

5 unit tests for the parser (bare v4, v4 CIDR, bare v6, v6 CIDR,
rejects garbage). Live e2e:

  baseline (no flag):
    169.254.169.254:80   ECONNREFUSED
    10.100.1.75:80       ECONNREFUSED
  --allow-egress 169.254.169.254 --allow-lan:
    169.254.169.254:80   OPEN
    10.100.1.75:80       OPEN
    192.168.1.1:80       OPEN
    127.0.0.1:80         ECONNREFUSED   (loopback group not opened)
  --allow-egress 10.100.1.0/24:
    10.100.1.75:80       OPEN
    10.100.2.1:80        ECONNREFUSED   (out of CIDR)

Vendor bump (f230c29) carries the matching NetworkBuilder helpers
and the new microsandbox::microsandbox_network re-export so we can
name DestinationGroup without a direct path dep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:30:20 +00:00
agent-vm
65f7d0e624 v0.1.15: bump for split-irqchip + 16K cmdline (--mount cap ~11 → ~100+)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:33:01 +00:00
Evgeny Boger
a850ec8edb Merge irq-multi-mount: lift --mount cap from ~11 to ~100+ on x86_64
Two coordinated changes that together raise the per-sandbox virtio
device cap on x86_64 by ~10x:

1. Enable msb_krun's userspace split irqchip in the runtime
   (vendor/microsandbox bump). Lifts the libkrun virtio-IRQ allocator
   ceiling from IRQ 15 (KVM in-kernel IOAPIC, 24 pins) to IRQ 223
   (userspace IOAPIC, 256 pins). Requires the wirenboard/libkrun fork
   (pinned in Cargo.toml [patch.crates-io] at 38e46f2) with three
   correctness fixes on top of upstream msb_krun 0.1.13.

2. Patch libkrunfw to bump x86 COMMAND_LINE_SIZE 2048 → 16384.
   New `libkrunfw-overrides/cmdline-size_x86_64.patch` lifts the cap
   so the assembled cmdline (~36 bytes per virtio_mmio device) doesn't
   silently truncate past ~10 user mounts.

CI workflow hardening that landed alongside:
 - Cache key now keyed per-arch (an aarch64 patch edit no longer
   invalidates the x86_64 cache).
 - `config-libkrunfw*` skip pattern uses a bare suffix so future
   `config-libkrunfw-tdx_${ARCH}.patch` / `-sev_` variants are also
   excluded from the kernel source-patch pipeline.

User-facing knobs (`--mount` doc in run.rs, README troubleshooting,
ARCHITECTURE/PLAN narrative) updated to reflect the new ceiling.
2026-05-28 14:30:53 +00:00
Evgeny Boger
4eafe4bbac lift --mount cap from ~11 to ~100+ via split_irqchip + cmdline-size patch
Two coordinated changes that together raise the per-sandbox virtio
device cap on x86_64 by ~10x:

1. **Enable msb_krun's userspace split irqchip** in the runtime
   (vendor/microsandbox submodule bump). KVM's in-kernel IOAPIC is
   hardcoded to 24 pins, of which libkrun's allocator hands out only
   IRQs 5..15 — saturated by a typical agent-vm config (rootfs, upper,
   runtime fs, network, vsock, console, balloon, rng) plus a couple of
   `--mount`s, so an extra mount has historically tripped
   `RegisterNetDevice(IrqsExhausted)` at boot. The userspace IOAPIC
   exposes 256 pins, lifting the libkrun allocator ceiling to
   IRQ_MAX_SPLIT = 223. Requires a libkrun fork (pinned in
   Cargo.toml) with three correctness fixes on top of upstream 0.1.13;
   without them, enabling split_irqchip crashes the VMM during boot or
   silently truncates the kernel cmdline.

2. **Patch libkrunfw to bump x86 COMMAND_LINE_SIZE 2048 → 16384**.
   Each virtio_mmio device adds ~36 bytes of cmdline; past ~10 user
   mounts the assembled cmdline crosses the stock 2048 cap, the kernel
   silently truncates the tail (which includes virtio-console), and
   the guest hangs in early boot with `kernel.log` stuck at 0 bytes.
   New `libkrunfw-overrides/cmdline-size_x86_64.patch` lifts the cap;
   CI workflow picks it up via libkrunfw's `patches/0*.patch` glob
   (the `0999-overrides-` prefix sorts strictly after libkrunfw's own
   numbered patches).

CI workflow hardening that landed alongside:
 - cache key now keyed per-arch via `*_${ARCH}.patch` glob (an
   aarch64 patch edit no longer invalidates the x86_64 cache);
 - `config-libkrunfw*` skip pattern uses a bare suffix so future
   `config-libkrunfw-tdx_${ARCH}.patch` / `-sev_` variants are also
   excluded from the kernel source-patch pipeline.

User-facing knobs (`--mount` doc in run.rs, README troubleshooting
guide, ARCHITECTURE/PLAN narrative) updated to reflect the new
practical ceiling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 08:26:28 +00:00
agent-vm
55548a9717 v0.1.14: bump for host gh/git author identity in guest commits
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:31:42 +00:00
agent-vm
975f91683a v0.1.13: bump for 16 GiB ext4 overlay default
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:22:56 +00:00
agent-vm
7967981222 v0.1.12: bump for /usr/sbin in attach PATH (dockerd discoverable)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:15:42 +00:00
agent-vm
5ddf26ffd8 v0.1.11: bump for docker engine + libkrunfw netfilter overrides
Image gains dockerd/cli/containerd/runc/fuse-overlayfs/iptables;
kernel gains POSIX_MQUEUE + netfilter + nf_tables. See merge
commit 0d60870 for the cost/verification summary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 18:17:04 +00:00
agent-vm
8eff1fadbe v0.1.10: bump for chrome-devtools-mcp telemetry off 2026-05-26 20:13:17 +00:00
agent-vm
10a0707df5 v0.1.9: bump for submodule + extra-mount GitHub scope
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:09:09 +00:00
agent-vm
e99a02f886 v0.1.8: bump for msb-style per-layer pull progress
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:33:00 +00:00
agent-vm
6e593379e8 agent-vm: pull progress now mirrors msb pull (per-layer bars, no ETA)
Rewrite crates/agent-vm/src/pull_progress.rs to render one progress bar
per layer instead of a two-phase aggregate, matching the upstream
PullProgressDisplay in vendor/microsandbox/crates/cli/lib/ui.rs.

While porting, fix seven defects flagged by code review:

  - Per-layer cache-EROFS fast path (registry.rs:701, :753) emits
    LayerMaterializeComplete with no prior Started — bar previously
    stayed magenta "downloading" then jumped straight to ✓. Introduce
    `materialize_styled: Vec<bool>` + ensure_materialize_style() and
    call it from all four materialize-phase arms so the bar always
    transitions through the correct style.

  - Bounded try_send progress channel can drop LayerMaterializeStarted
    under load; the same helper restyles on the next surviving
    materialize event rather than leaving the bar in download style.

  - `style("✓").green()` defaulted to for_stderr=false so the checkmark
    was rendered against stdout's TTY state. Add .for_stderr().

  - `render.await.ok()` silently swallowed JoinError. Add a small
    `await_render` helper that logs panics, and refactor pull.rs /
    setup.rs / run.rs to always await render before propagating pull
    errors so display.finish() runs mp.clear() to restore the terminal.

  - header.enable_steady_tick() fired even when the multi-progress
    draw target was hidden (non-TTY). Guard with `if is_tty`.

  - BRAILLE_TICKS had 11 entries; index 10 duplicated index 0,
    producing a one-frame stutter every full spinner rotation. Drop the
    trailing entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:32:35 +00:00
agent-vm
1f9d9ce1cd v0.1.7: bump for zstd-compressed OCI layers 2026-05-25 20:04:15 +00:00
Evgeny Boger
2cb978809b v0.1.6: bump for agent-vm shell <cmd> fix
82d97fc fixed shell-passthrough so `agent-vm shell ls /tmp` runs
`bash -c 'ls /tmp'` instead of bash trying to exec ls as a script
(exit 126: "cannot execute binary file"). Bump for release.
2026-05-25 18:46:16 +03:00
agent-vm
82d97fc4c3 agent-vm: run shell trailing args via bash -c
Before: `agent-vm shell ls` invoked `bash -O histappend ls`. bash
treats the first non-option positional as a script filename and
PATH-searches for it, lands on `/usr/bin/ls`, hits the ELF magic,
and exits 126 with `/usr/bin/ls: cannot execute binary file`.

Now: when `Agent::Shell` has any trailing args, shell-escape and
join them into a single `-c` command. `agent-vm shell ls -la /tmp`
becomes `bash -O histappend -c "'ls' '-la' '/tmp'"`. No-arg
`agent-vm shell` keeps its interactive-bash path. Other agents
(claude/codex/opencode) are unchanged.

Cargo.lock: workspace agent-vm version bumped 0.1.4 -> 0.1.5 to
match Cargo.toml (stale lockfile entry refreshed on first build).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:08:26 +00:00
Evgeny Boger
cc506b7fff agent-vm: allow concurrent launches in the same project dir
Before: `ProjectSession::sandbox_name` was just `agent-vm-<hash>` and
`Sandbox::builder(...)` was called with `.replace()`. Launching a
second `agent-vm` in the same dir would SIGTERM/SIGKILL the first
one's VMM (the second create's "replace" path) — the first attach
saw "agent stream closed (VMM/agentd disconnected)" and died.

Now:

- session.rs: sandbox name carries the launcher PID
  (`agent-vm-<hash>-<pid>`) so two concurrent invocations boot
  independent VMs. State dir is still hash-keyed, so per-project
  bind-mounted state (claude/, codex/, opencode/, bash_history) is
  still shared across launches.

- run.rs: drop `.replace()` on the builder (no name collision left to
  override), and add `reap_stale_project_sandboxes` which scans
  `~/.microsandbox/sandboxes/agent-vm-<hash>-*` before each launch
  and `Sandbox::remove`s any entry whose PID-suffix is no longer in
  /proc/. That replaces the implicit GC the old deterministic name
  used to get from `.replace()`. It also pre-empts the new failure
  mode where a stale entry happens to match our own PID (PID reuse)
  and `Sandbox::create` would otherwise hard-error with "already
  exists".

- secrets.rs: `secrets::refresh` is now serialized across concurrent
  launchers in the same project via an exclusive flock on
  `<state_dir>/.refresh.lock` (RAII guard). Several files under
  state_dir (claude.json, claude/settings.json,
  opencode-config/opencode.json) are read-modify-write — without the
  lock, two simultaneous launches in the same project would both
  read the same baseline and the later write would silently clobber
  the earlier launch's mutation (lost project-trust flag, lost
  onboarding state, etc.).

- run.rs: `Agent::Shell::default_args` now carries `-O histappend`
  so two concurrent interactive shells *append* (not overwrite) the
  shared bind-mounted `~/.bash_history` on exit.

Adds three unit tests: sandbox-name format encodes PID; refresh
lock blocks a second LOCK_EX while the first guard is alive;
existing session/secrets suites still pass (87/87).

Adds `libc = "0.2"` as a direct dep for `flock(2)` (already present
transitively).
2026-05-25 16:40:40 +03:00
Evgeny Boger
169dfb9c16 v0.1.4: bump for image rename to agent-vm-template 2026-05-25 14:40:28 +03:00
Evgeny Boger
31b24ccbdb ci: bump all GH actions to Node-24 majors + add Dependabot
The Node 20 → 24 cutover is coming (forced June 2 2026, removal
Sept 16 2026). Every action we use already has a Node-24
major released — we were pinned to stale ones, hence the deprecation
warnings in the prior runs:

  actions/cache@v4              → @v5
  actions/upload-artifact@v4    → @v7
  actions/download-artifact@v5  → @v8
  actions/github-script@v7      → @v9
  docker/build-push-action@v6   → @v7
  docker/login-action@v3        → @v4
  docker/setup-buildx-action@v3 → @v4

The big leaps (artifact actions v4→v7, github-script v7→v9) are
because the maintainers cut MULTIPLE majors between Node 20 and
Node 24; we're just catching up.

Also enable Dependabot for github-actions on a weekly cadence so
we don't drift again. Submodule + Rust deps explicitly NOT
auto-updated — they need careful audits.

Bumps to v0.1.3 to exercise the upgraded actions end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:27:41 +03:00
Evgeny Boger
9a66b4bfa8 vendor: bump msb to apply review fixes (handler state machine + tee lifecycle)
Bumps the submodule pointer to fd1b31b, which is the tip of
`agent-vm-secret-file` after folding in two review-driven fix commits:

  c5e3004  secrets/handler: fix state machine bypasses + cross-chunk
           boundary + keep-alive reset
  fd1b31b  runtime: harden stderr tee lifecycle + idempotent exit-log

Closes 13 of the 15 findings the max-effort review produced on the
prior submodule tip (db98655) — including the four CONFIRMED-via-
probe state-machine bypasses that re-introduced the ECONNRESET
false-positive the original narrowing was meant to eliminate, the
keep-alive security regression where request 2's partial headers
skipped the violation scan entirely, and the stderr-tee deadlock vs
VMM thread when host stderr is wedged. Full rationale in the vendor
commits' messages.

Cargo.lock bump (0.1.0 → 0.1.2) absorbed from the previous
rewrite-microsandbox merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:25:01 +03:00
Evgeny Boger
23cba5663b distribution: ship via npm + ghcr.io image, drop local-build flow
Replaces the build-from-source quickstart (Rust toolchain + docker +
local registry container + manual `images/build.sh`) with a single
`npm install -g @wirenboard/agent-vm` and a `ghcr.io/wirenboard/
agent-vm:latest` image pulled from CI.

## What ships via npm

`npm-dist/` scaffolds the established per-platform-subpackage
pattern (esbuild / ruff / biome):

- `@wirenboard/agent-vm` — JS launcher that detects
  `${platform}-${arch}` at runtime and execs the right native binary
  from the matching subpackage. `optionalDependencies` lists each
  platform.
- `@wirenboard/agent-vm-linux-x64` — bundles a prebuilt `agent-vm`,
  the patched `msb`, and `lib/libkrunfw.so.5.2.1`.

Smoke-tested locally via `npm link`: launcher resolves the subpackage
correctly and execs the bundled binary; `MSB_PATH=/usr/bin/cat
agent-vm pull` correctly rejects an unpatched msb with the
actionable "reinstall agent-vm" hint.

## msb discovery (msb_install.rs rewrite)

`point_at_workspace_msb` → `point_at_msb` with a fallback chain:
1. `MSB_PATH` env (explicit override).
2. `<exe-dir>/msb` (npm bundle layout — sibling of agent-vm).
3. `<workspace>/vendor/microsandbox/target/release/msb` (dev mode).

`msb --version` is verified to contain `+agent-vm` — refuses to
launch if a user's separate microsandbox install has somehow ended
up on our discovered path. Catches the bug where shadow installs
would silently lose SecretValue::File and the request-interceptor
hook, producing inscrutable in-VM failures instead of one clear
"reinstall" message.

## Image flow (separate cadence)

- Default image ref: `ghcr.io/wirenboard/agent-vm:latest`.
- `images/build.sh` and local-registry flow stay as the
  source-checkout dev path (pass `--image localhost:5000/...`).
- `agent-vm setup` becomes "pull + verify"; no more docker/registry
  dependency for end users.
- New `image_api_version` module reads `/etc/agent-vm-image-version`
  inside the booted sandbox and requires it within
  `MIN_SUPPORTED_IMAGE_API..=MAX_SUPPORTED_IMAGE_API` (both 1
  initially). Out-of-range → actionable error pointing at the right
  side (binary vs image) to update.
- Image registry hostname heuristic (`is_plain_http_registry`)
  keeps `.insecure()` for localhost-style refs but drops it for
  ghcr.io / docker.io / public registries that require HTTPS.

## CI workflows

- `.github/workflows/release-npm.yml` — on `v*.*.*` tag, cross-
  compile binaries per platform (linux-x64 only initially; arm64 /
  darwin commented out, ready to enable when microsandbox grows
  those backends), bundle into subpackages, publish all packages
  to npm with provenance. Subpackages publish before the main
  package so optionalDependencies resolve.
- `.github/workflows/build-image.yml` — hourly cron + push-to-
  images/ trigger. Builds Dockerfile, tags `latest` + timestamped
  `YYYY-MM-DDTHH` + `sha-<sha>`, pushes to ghcr.io. Retention
  policy prunes hourly tags older than 14 days.

## README

Quickstart leads with `npm install -g`. Source-build instructions
moved to a dedicated section. Image-cadence section explains the
hourly rebuild + pinnable date tags + image-API contract.

## Tests (84/84 pass)

- msb_install: 4 (version-marker accept/reject + env-override behaviour).
- pull: 2 (local-registry detection: localhost/127.0.0.1/0.0.0.0/
  *.local/*.localhost vs ghcr.io/docker.io/example.com).
- image_api_version: 5 (parse trim, parse rejects garbage,
  in-range accept, too-new with actionable hint, too-old with hint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:02:56 +03:00
Evgeny Boger
b7829bf81b fix(phase6): close keep-alive bypass in github smart-HTTP hook
msb's Interceptor handles one request per TLS/TCP connection. After
the first dispatch on a streaming rule the state goes to
State::Disabled and every subsequent chunk on the same connection
returns Verdict::Forward — which means the secret-substitution layer's
output (real GitHub token already swapped into the Authorization
header) flows STRAIGHT to upstream, bypassing the hook entirely.

Real-world impact: gh repo clone of a non-allow-listed private repo
SUCCEEDED. libcurl (git's HTTP backend) reuses TLS connections
aggressively. The first GET /info/refs request gets stripped by the
hook (no auth → 401 from GitHub), but libcurl's follow-up retry /
the POST /git-upload-pack request rides the same connection — the
hook is now Disabled, the real token reaches GitHub, clone proceeds.

The fix: inject `Connection: close` into every request the hook
emits, both Authenticated (allow-listed) and Anonymous (third-party)
paths. The server closes the TCP after responding; libcurl opens a
fresh connection for the next request; the fresh connection creates
a fresh Interceptor that re-evaluates the policy.

Also strips hop-by-hop headers (Connection / Keep-Alive /
Proxy-Connection) so we emit exactly one Connection: close header
regardless of what the guest sent.

Tests added:
- connection_close_injected_when_header_absent
- connection_close_replaces_existing_keep_alive
- connection_close_preserves_body_verbatim
- anonymous_passthrough_forces_connection_close (e2e: secrets →
  hook simulation; real token must not reach wire AND Connection:
  close must be present)
- authenticated_passthrough_forces_connection_close (e2e: same for
  the allow-listed path — token allowed through, but connection
  still torn down)
- keep_alive_second_request_bytes_contain_real_token_pre_hook
  (hypothesis-confirmation test: documents the lower-level leak in
  the secrets layer alone — the Connection: close fix is what makes
  it unexploitable)
- private_repo_clone_does_not_leak_real_token_through_pipeline
  (full end-to-end: build the exact bytes a git clone sends, run
  through real SecretsHandler + real hook helpers, assert no token
  in the wire bytes)
- allowlisted_repo_clone_does_pass_real_token_through_pipeline
  (counterpart: token must reach upstream when the repo IS in the
  allow-list)

agent-vm/Cargo.toml gains a dev-dep on microsandbox-network so the
e2e tests can drive the real SecretsHandler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 18:30:41 +03:00
Evgeny Boger
f66a0b6e75 feat(phase5): OpenCode auth + host-cred security snapshot
OpenCode is the third v1 agent. Phase 4 covered Claude + Codex; this
finishes the auth/secret story.

**OpenCode auth.**
- `secrets::refresh_opencode` derives an OpenCode-shaped auth.json
  from the host's `~/.codex/auth.json` (same OpenAI account, different
  on-disk format). It writes `<state>/opencode/auth.json` with
  `{openai:{type:oauth, refresh:<placeholder>, access:<synthetic JWT>,
  expires:<far-future>, accountId:<from id_token>}}`.
- The `access` JWT is a *placeholder* — OpenCode parses it
  client-side (only `chatgpt_account_id` and `exp`) and sends it as
  `Authorization: Bearer <jwt>` on api.openai.com / chatgpt.com
  requests. The proxy substitutes the placeholder JWT for the real
  OpenAI access token (same on-disk file as Codex uses).
- `accountId` is pulled from the host Codex JWT's id_token payload via
  `decode_id_token_account` (adds `base64 = "0.22"`).
- New constants: `OPENCODE_OPENAI_ACCESS_PLACEHOLDER` (a synthetic
  alg-none JWT, deliberately minimal — see upstream issue #8 in PLAN
  about long-placeholder boot failures) and
  `OPENCODE_OPENAI_REFRESH_PLACEHOLDER`.
- `CredsState` grows `opencode_openai_access_token_file: Option<PathBuf>`
  pointing at the same host-only file as `openai_token_file`. The
  launcher uses this to register a third proxy-substitution entry
  in `run.rs` keyed off the OpenCode placeholder.
- `host_paths::host_opencode_auth_path` (`$HOME/.local/share/opencode/
  auth.json`) added for symmetry with the Claude/Codex helpers and
  consumed by the snapshot below.

**Security snapshot.**
- `snapshot_host_creds()` SHA-256s the three host credential files at
  launcher start. `verify_snapshot(&snap)` re-hashes after the sandbox
  exits and prints a one-line warning naming each file that changed.
- The Phase 4 OAuth refresh hook *legitimately* rewrites these mid-
  session; the warning surfaces anything else that mutates them — a
  bug to investigate.
- Wired into `run.rs` after the `Sandbox::remove` call, non-fatal.
- `CredsState::snapshot: Option<HostCredsSnapshot>` carries the
  pre-run state between the two calls.

Verified end-to-end on this host:
- `agent-vm opencode run "..."` reaches api.openai.com — the real
  OpenAI returns model-level errors (auth was accepted), proving the
  synthetic-JWT placeholder substitution works on the wire.
- `agent-vm claude -p "Say PHASE5_OK"` still returns a real response;
  no false-positive snapshot warning.
- 6/6 cargo tests still green.

PLAN.md: new upstream issue #8 documenting the long-placeholder
sandbox-boot failure that constrained the JWT design choice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 05:23:28 +03:00
Evgeny Boger
8cc036bb96 Phase 3: host-rooted Claude/Codex credentials never enter the VM
The big architectural payoff of microsandbox. Real OAuth access tokens
stay on the host; the in-VM agent only ever sees placeholder strings
in its credentials.json files, env vars, and outgoing request headers.
The substitution into the real token happens at the network layer in
microsandbox's TLS-intercept proxy.

Two-layer placeholder dance per provider:

1. agent-vm reads host ~/.claude/.credentials.json and
   ~/.codex/auth.json at every launch, extracts the access token, and
   passes it to microsandbox as a SecretValue::Static(...) on the
   sandbox builder with allow_host for the API + OAuth endpoints.
2. agent-vm writes a "placeholder credentials" JSON into the per-project
   state dir using a stable placeholder string and the host file's
   non-secret fields (expiresAt, scopes, account_id, etc.) so the
   in-VM agent sees a plausible shape.
3. microsandbox's TLS intercept splices the real token in on the way
   out — the guest never holds the real value.

What landed:

- vendor/microsandbox submodule bumped to agent-vm-secret-file branch
  (commits b87d6bb + b95614c): adds SecretValue { Static, File } enum
  with bare-string + NUL-sentinel wire format so existing prebuilt msb
  daemons keep working with Static values. File variant is forward-
  looking infrastructure for Phase 4.
- crates/agent-vm/src/secrets.rs: snapshots host creds, writes
  placeholder credentials files atomically (0600), redacts in Debug.
- crates/agent-vm/src/run.rs: wires .network(|n| n.tls(...).secret(...))
  per provider with the right allow-host lists, and sets IS_SANDBOX=1
  so Claude Code's "don't run as root" guard yields.
- AGENT_VM_DEBUG_CONFIG=1 dumps the effective SandboxConfig JSON for
  debugging.
- serde_json bumped to "preserve_order" so the dumped config is
  diffable in a sensible order.

Smoke verified end-to-end up to the network layer (TLS cert chain
shows CN=microsandbox CA on api.anthropic.com requests, debug config
dump confirms the secret value reaches msb, the substitute path is
exercised). The final "Anthropic returns a real response" leg can't
be verified on this nested test host because of an outer credential
bridge; structurally equivalent to the Bash agent-vm's
credential-proxy flow.

Phase 4 will rebuild ~/.microsandbox/bin/msb from our fork to
activate SecretValue::File, add the inotify/proactive refresh loop,
and short-circuit the platform.claude.com/v1/oauth/token endpoint
so long sessions survive token rotation.
2026-05-17 21:21:28 +03:00
Evgeny Boger
bfab9d3daf feat: explicit agent-vm pull + per-launch update-available banner
User feedback: re-checking the registry on every launch (PullPolicy::
Always) made fast launches feel surprising — when there *was* a new
image, the next innocuous `agent-vm shell` blew up into a multi-minute
pull. Move pulls onto an explicit `agent-vm pull` verb and just notify
on launch when the registry has something newer.

- pull.rs: new `agent-vm pull` subcommand. Image::remove(force=true)
  the cached entry first (PullPolicy::Always empirically re-fetches
  layer blobs but does NOT update the cached manifest digest record,
  so subsequent Image::get returns the stale digest forever) then
  boot a throwaway sandbox to trigger a fresh pull, then stop+remove.
- image_check.rs: cheap manifest probe. Compare microsandbox's cached
  per-platform manifest digest against the registry's. Critical
  subtlety: microsandbox stores the per-platform (linux/amd64) digest
  while a HEAD on a tag returns the multi-arch *index* digest. A
  `docker tag X Y; docker push Y` churns the index digest but reuses
  the per-platform manifest, so naively comparing the two would warn
  forever. We GET the index, find the linux/amd64 entry, take its
  digest, compare that.
- run.rs: switch PullPolicy back to IfMissing, call
  notify_if_update_available before create. Failures (registry down,
  etc.) are silently swallowed so offline launches still work.
- setup.rs: chain an explicit pull_image() call after build.sh so
  setup itself updates the cache.
- Cargo.toml: add reqwest + serde_json.

Verified end-to-end: (1) churn list digest only -> no banner.
(2) Real Dockerfile change -> "A newer image is available (cached X,
registry Y)" banner. (3) After `agent-vm pull` -> banner gone.
2026-05-17 20:09:47 +03:00
Evgeny Boger
2489168def feat(run): show pull progress bars during image fetch + materialize
A cold-cache or new-tag launch downloads ~350 MiB of compressed layers
plus a 1-2 minute EROFS materialize step, and until now produced no
output between "Booting sandbox..." and the agent actually starting.
That looked indistinguishable from a hang for the first ~2 minutes.

Wire microsandbox's create_with_pull_progress into both the launcher
(run.rs) and the setup verify path (setup.rs). A small renderer
(pull_progress.rs) collapses microsandbox's per-layer event stream into:

- a manifest-resolve spinner
- one overall byte-progress bar (sized from Resolved.total_download_bytes)
- one layer-count bar for the materialize step
- a spinner for the stitching tail (fsmeta + VMDK writes)

Per-layer bars are deliberately omitted; the OCI image has 7+ small
layers and a wall of per-layer bars is louder than useful.

indicatif's default ProgressDrawTarget::stderr() correctly detects when
stderr isn't a TTY (CI, pipes, smoke tests under `sg`/`sudo -c`) and
silently no-ops there, so this never produces noisy output for scripts.
Verified rendering under a pty (script -qc) and silence under a pipe.
2026-05-17 19:15:52 +03:00
Evgeny Boger
7608f273ec polish: wire tracing-subscriber so RUST_LOG works
Without this the microsandbox stack is silent because no one initializes a
subscriber. Defaults to `warn` so normal output stays clean; bump via
`RUST_LOG=agent_vm=debug,microsandbox=info` (or whatever) to see boot/init
timing and any other tracing emissions from the SDK.
2026-05-17 18:04:54 +03:00
Evgeny Boger
4bc3de3539 Phase 2: claude/codex/opencode/shell launcher with project-scoped state
Add the launcher subcommands that boot a per-project microsandbox VM,
mount the working directory at /workspace, and attach to the requested
agent. Wiring is end-to-end verified via `agent-vm shell`; the live
Anthropic API smoke from PLAN.md's DoD is intentionally deferred to
Phase 3 (host-OAuth) — see ARCHITECTURE.md "What Phase 2 deliberately
doesn't do".

- session.rs: project hash (SHA256-12 of canonical cwd), state dir under
  ${AGENT_VM_STATE_DIR-${XDG_STATE_HOME-$HOME/.local/state}/agent-vm}/<hash>/,
  deterministic sandbox name `agent-vm-<hash>` so re-invoking in the same
  project replaces the prior VM via `.replace()`.
- run.rs: one bind for cwd → /workspace, one bind for state-dir →
  /agent-vm-state, plus rootfs-patched symlinks for ~/.claude and
  ~/.local/share/opencode and CODEX_HOME=/agent-vm-state/codex (codex's
  binary lives under /root/.codex/packages so a symlink would shadow it).
  This keeps virtio device count under libkrun's IRQ cap.
- TTY-conditional dispatch: attach() when stdin is a real terminal,
  exec_with(..., cwd=/workspace) otherwise so pipes/redirects/sg/sudo -c
  smoke tests still work (attach requires /dev/tty).
- Credentials: ANTHROPIC_API_KEY / OPENAI_API_KEY forwarded via .env();
  refreshable host-rooted secrets land in Phase 3.
- Knobs via env vars for now (AGENT_VM_IMAGE_TAG, AGENT_VM_MEMORY_MIB,
  AGENT_VM_CPUS); promoting to clap flags is Phase 5 polish.

Docs updated together with the code per working-agreement #5: README
status list moved Phase 2 to done; PLAN.md notes the deferred live smoke;
ARCHITECTURE.md grew a Phase 2 section covering the mount cap, the
TTY/exec branch, the explicit PATH, and the deliberate env-var-only auth.
2026-05-17 17:55:02 +03:00
Evgeny Boger
d23c42167b Phase 1: base OCI image + agent-vm setup
Add the minimal Debian 13 image with Claude Code, OpenCode, and Codex
preinstalled, and wire `agent-vm setup` to build/push/verify it via a host-
local Docker registry.

- images/Dockerfile: Debian 13 slim + node22 + ripgrep/fd/jq/git + the three
  agent CLIs installed via their canonical installer scripts so we track
  upstream release channels instead of pinning npm package versions.
- images/build.sh: ensure an `agent-vm-registry` (registry:2) container is
  running on 127.0.0.1:5000, docker build, docker push.
- agent-vm setup: clap-based subcommand that shells to images/build.sh, then
  boots `localhost:5000/agent-vm:latest` under microsandbox (insecure
  registry) and runs the three `--version` commands. --no-verify and
  --image/AGENT_VM_IMAGE_TAG escape hatches included.
- ARCHITECTURE.md: Phase 1 section with the registry-vs-Bind/qcow2 decision,
  the deliberately minimal package list, and why the build step is shelled
  out to Bash while verification stays in Rust.

End-to-end run: image builds, pushes to the local registry, sandbox boots
from it, and reports Claude 2.1.143 / OpenCode 1.15.3 / codex-cli 0.130.0.
2026-05-17 17:32:13 +03:00
Evgeny Boger
cb4be40448 Phase 0: scaffold microsandbox-based rewrite
Start a from-scratch rewrite of agent-vm on top of microsandbox. The original
Bash/Python tree is removed on this branch and continues to live on main until
the rewrite reaches v1.

This phase only sets up the build:

- PLAN.md and ARCHITECTURE.md document the phased roadmap and the
  design-decision log; ARCHITECTURE grows after each phase.
- microsandbox added as a git submodule at vendor/microsandbox so the Phase 3
  SecretValue::File extension can branch the fork in place.
- Cargo workspace at the root with crates/agent-vm/ as the binary crate.
- Hello-world main.rs that boots an alpine sandbox via the microsandbox SDK
  to prove the wiring is correct end-to-end (cargo check passes).
2026-05-17 17:05:07 +03:00