In-VM commits previously carried the hardcoded `agent-vm <agent-vm@msb.local>`
placeholder. Resolve the real author identity on the host (`gh api user`
first, then `git config --global user.name/email`) and write it into the
guest's `~/.gitconfig` so `git commit` inside the sandbox attributes to
the user.
The gh path also synthesizes the `<id>+<login>@users.noreply.github.com`
address when email privacy hides the public email, and the guest's
`gh-config/hosts.yml` `user:` field gets the real login.
If neither source yields anything usable, the `[user]` section is
omitted entirely — git refuses to commit rather than silently
mis-attribute.
Defensive guards:
- 3s timeout on `gh api user`, 1s on `git config` (bounded launch even
on flaky networks / hung credential helpers).
- Reject name/email/login containing ASCII control bytes — prevents
`\n[core]\n\tpager=…` gitconfig injection and YAML injection in
hosts.yml.
- `gh_login` validated against GitHub's `[A-Za-z0-9-]` charset.
- `as_u64` for user id (was `as_i64` with an incorrect comment about
the 2^53 limit).
- `String::from_utf8_lossy` for `git config` output so non-UTF-8
`user.name` values don't silently drop the identity.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Picks up wirenboard/microsandbox@1e9864e 'image/ext4: bump default
overlay size 4 GiB → 16 GiB'. Quadruples the writable overlay
ceiling for newly-created sandboxes — existing sandboxes keep their
4 GiB upper.ext4 since the create path skips re-format when the
upper already exists.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
run.rs hardcodes a PATH and forces it onto the guest at attach time
(comment explains: the image's ENV PATH is in /root's shell rc files,
but attach() execve's directly past the shell). That hardcoded value
was missing /usr/sbin, which silently undid the image-side PATH fix
in commit 57deafa — users in a fresh `agent-vm shell` see
`dockerd: command not found` even though the latest agent-vm-template
image has /usr/sbin on its ENV PATH and dockerd at /usr/sbin/dockerd.
Add /usr/sbin to the hardcoded list. Keep these in sync with
images/Dockerfile's ENV PATH (noted in the new comment).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous fix symlinked /usr/sbin/{dockerd,runc} into
/usr/local/bin so users could invoke `dockerd` directly, but that
was whack-a-mole — dockerd itself does PATH lookup at runtime for
its helper binaries:
- `iptables` (from `iptables` pkg, /usr/sbin/iptables): dockerd
aborts with "failed to register \"bridge\" driver: failed to
create NAT chain DOCKER: iptables not found".
- `docker-proxy` (from docker.io pkg, /usr/sbin/docker-proxy):
daemon.json validation fails with "userland-proxy is enabled,
but userland-proxy-path is not set" (the daemon resolves a
default path via PATH lookup before parsing the config).
- `runc` (also /usr/sbin/runc): containerd-shim PATH-looks it up
when starting containers.
Symlinking each one would be unending whack-a-mole as new helpers
get added in future docker.io revs. Adding /usr/sbin to PATH covers
all of them in one line; drop the redundant dockerd/runc symlinks
from the previous fix.
Verified end-to-end inside an agent-vm session built from the
broken (v0.1.11) image: with `PATH=$PATH:/usr/sbin dockerd &` the
daemon starts cleanly, `docker run hello-world` prints the welcome
message, `docker run alpine sh -c 'echo running uid=$(id -u)'`
runs a real container (uid=0, kernel 6.12.68), iptables NAT chain
DOCKER is populated, docker0 bridge comes up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
debian ships dockerd at /usr/sbin/dockerd and runc at /usr/sbin/runc,
neither of which are on the image's minimal PATH
(/root/.local/bin:/root/.claude/local/bin:/root/.opencode/bin:/usr/local/bin:/usr/bin:/bin).
Without the symlinks:
- the build-time sanity check `dockerd --version` exits 127 (broke
CI on the v0.1.11 image build);
- users invoking `dockerd &` from the in-VM shell would get
"dockerd: command not found";
- containerd's PATH-lookup of runc when spawning containers would
also miss (containerd binary is at /usr/bin/containerd which IS on
PATH, but it inherits PATH from its parent).
Cheapest fix: two symlinks into /usr/local/bin, leaving /usr/sbin off
PATH to keep sysadmin binaries from polluting the agent's namespace.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Pairs with the libkrunfw-overrides commits (POSIX_MQUEUE +
netfilter + nf_tables) — those make the kernel container-capable;
this gives the guest image the userspace to drive it.
What's installed: docker.io, docker-cli, containerd, runc,
fuse-overlayfs, iptables. No docker-buildx — the +20 MB compressed
isn't worth it for the small in-VM builds an agent would do, and
the legacy builder is a fine fallback. Storage driver pinned to
fuse-overlayfs in /etc/docker/daemon.json because overlay2 fails
on overlay-on-overlay (libkrun rootfs is already overlay).
Cost on top of the current shipped template (568 MB compressed,
1.81 GB on-disk per `docker manifest inspect`):
+87 MB compressed pull (+15.4%)
+281 MB on-disk (+15.5%)
Daemon is NOT auto-started; users invoke `dockerd &` themselves
(or wire it into `.agent-vm.runtime.sh`). Most sessions don't
touch docker, so paying ~50 MB RSS at every boot would be wasteful.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous hunk gave us the iptables-legacy backend, which works
but only after `update-alternatives --set iptables iptables-legacy`
— debian 13 (the base image we ship) defaults `iptables` to
`iptables-nft`. Without `NF_TABLES` + the `NFT_*` family, the
default debian iptables binary errors with
`Failed to initialize nft: Protocol not supported` on every rule.
`NFT_COMPAT` lets the xtables-nft userspace translate
`-j MASQUERADE`-style rules into the corresponding native nft
expression so dockerd's rule set applies cleanly without forcing
the user to switch backends.
Verified in a nested guest: both `iptables` (nft default) and
`iptables-legacy` accept `-A POSTROUTING -j MASQUERADE`.
Cost on top of the netfilter hunk: +8 KB bzImage, +64 KB on the
shipped `.so` (one more 64K page). +3 ms nested boot, within noise.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dockerd / podman default to bridge networking, which needs the
in-kernel bridge driver to set up `docker0` plus iptables (the
legacy backend here; the nf_tables backend is a follow-up hunk)
for SNAT/MASQUERADE between containers and the outside. Without
these the daemon starts but every container start fails on
`Table does not exist (do you need to insmod?)`.
Verified in a nested guest on the rebuilt libkrunfw:
- `ip link add docker0 type bridge` (was ENODEV)
- `iptables-legacy -t nat -A POSTROUTING -j MASQUERADE`
- `iptables -t nat -L` (no longer "Failed to initialize nft")
Cost on a clean v5.2.1 rebuild: +348 KB bzImage, +0 on the shipped
`.so` (the new code fits within the 64K page padding `bin2cbundle`
already adds). 15-run nested-boot mean +5 ms vs the mqueue commit
— within 1σ noise.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two small fixes from review:
- Use tempfile::tempdir() instead of hand-rolled temp_dir()+PID+nanos
(matches existing pattern at project_refresh_lock_blocks_second_acquire).
RAII cleanup on assertion-panic, guaranteed-unique mkdtemp.
- Print a SKIP diagnostic to stderr when AGENT_VM_NO_CHROME_MCP is set
in the test environment. The early-return is intentional (function
under test removes the chrome entry on that path, leaving nothing
to assert) but a silent return turned the guard into a no-op pass
for any dev who had the opt-out var exported in their shell.
Sets CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS=1 in the auto-written
chrome-devtools MCP entry so the upstream MCP doesn't fork its
detached "telemetry watchdog" node child to ship usage events to
Google's Clearcut endpoint. Drops one idle node process per Claude
session and stops a silent phone-home from a supposedly-sandboxed
environment.
The agent-vm-chrome-mcp wrapper's sudo --preserve-env list already
whitelists this variable, so the env block flows through to the
`chrome` user's process.
Verified inside an agent-vm sandbox:
- baseline: watchdog node child present (PID confirmed via ps)
- with env var: watchdog not spawned; MCP init, tools/list, and a
real navigate_page tool call all succeed; chromium still
lazy-launches only on the first browser tool call
Adds a regression-guard unit test so a future refactor of the json!
literal can't silently drop the env block.
runc/crun unconditionally mount /dev/mqueue from their default OCI
spec; with POSIX_MQUEUE off the guest kernel returns ENODEV and every
container start (docker run, podman, buildkit RUN) fails before user
code ever executes.
Cost measured on a clean v5.2.1 rebuild (Debian 13 gcc 14.2.0):
+4 KB stripped vmlinux, +12 KB bzImage, +64 KB on the shipped .so
(page padding). 15-run nested-VM cold-boot mean unchanged within
noise (1.069s baseline vs 1.057s with mqueue, sd ~25 ms).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Restores main-branch claude-vm.sh behavior that the Rust rewrite dropped:
submodule URLs and --mount host dirs' remotes/submodules now feed the
per-launch GitHub allow-list, so `git push` from inside a submodule no
longer hits api.github.com anonymous and 401s.
- detect_github_repos(project_dir, extra_mount_dirs): scans the project
dir + each mount; for each dir, reads `git remote -v` and parses
`.gitmodules` via `git config -f`.
- parse_extra_mounts moved up so the mount list feeds the scan.
- Startup logs the resolved scope: `==> GitHub repo scope (N): a/b, c/d`.
- parse_github_slug now rejects `.` / `..` segments so a path-traversal
URL never lands in the allow-list summary or --allowed-repo argv.
- parse_gitmodules_github_slugs uses symlink_metadata so a hostile
`.gitmodules -> ~/.config/git/config` symlink can't pull in stale
out-of-tree submodule URLs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the two-phase aggregate progress bar in agent-vm pull with the
same per-layer multi-bar layout msb pull uses (one line per layer, no
ETA). Bundles seven correctness fixes flagged during code review — see
the pull-progress commit body for details.
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>
The retain script queries package `agent-vm`, but the GHCR package
was renamed to `agent-vm-template` in ca6a2a3. The script has been
returning HttpError 404 ("Package not found") on every build since,
so 14-day pruning has not actually run.
Switch to the real name so the cleanup actually fires.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cuts the OCI→EROFS conversion on `agent-vm setup` by ~24× on
binary-heavy content (benched at 856 MB of /usr/lib: 143s → 5s).
- `images/build.sh`: docker build → buildx with
`--output type=registry,compression=zstd,compression-level=3,
force-compression=true`
- `.github/workflows/build-image.yml`: same flags via the
`outputs:` input of docker/build-push-action@v7, and gate the
moving `:latest` + `:YYYY-MM-DDTHH` tags to the integration
branches (`main`, `rewrite-microsandbox`) so feature-branch
workflow_dispatch can verify safely.
No microsandbox-side changes — `tar_ingest.rs:427` already
accepts the `application/vnd.oci.image.layer.v1.tar+zstd`
media type.
Verified end-to-end on the branch: build-image run #26417234380
pushed ghcr.io/.../agent-vm-template:sha-ce6b9ca with all 12
layers tagged `application/vnd.oci.image.layer.v1.tar+zstd`
(568 MB compressed).
The previous commit gated `:latest` and `:YYYY-MM-DDTHH` to `main`
only, but `rewrite-microsandbox` is the active integration branch
for the microsandbox-rewrite work (release-npm.yml:96 documents
that release dispatches happen from there). Restricting moving
tags to `main` alone means a dispatch on rewrite-microsandbox
produces only `:sha-…` — fine for ad-hoc verification, wrong for
the canonical image build that the binary release pins to.
Allow both branches.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds out-of-the-box nested KVM support (Docker-in-Docker, qemu inside
the guest) and reroutes msb's writable state off ~/.microsandbox/ into
$XDG_STATE_HOME/agent-vm/msb-home/. See 6b2fdea for the full commit
message.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror the gzip→zstd compression change from images/build.sh into
the GitHub Actions image build, so the published
ghcr.io/.../agent-vm-template image carries zstd layers that
microsandbox can ingest ~24× faster on a cold `agent-vm pull`.
`docker/build-push-action@v7` doesn't expose compression flags
directly, but it forwards `outputs:` straight to buildx. So:
outputs: type=registry,push=true,compression=zstd,
compression-level=3,force-compression=true
replaces the previous `push: true`. `force-compression=true` is
the important bit — without it, only the layers we add get
zstd-compressed and the base-image layers stay gzip.
Also gate `:latest` and `:YYYY-MM-DDTHH` (the moving tags) to
pushes on `main`. The immutable `:sha-<sha>` is still pushed
unconditionally. This lets `workflow_dispatch` on a feature
branch produce a verifiable image (inspect the `:sha-…` manifest)
without overwriting whatever's currently on `:latest`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two coupled changes to make nested KVM (Docker-in-Docker, qemu, etc.)
work inside the agent-vm guest without manual setup:
1. CI rebuilds libkrunfw with CONFIG_KVM=y instead of pulling the
upstream release. Upstream's config-libkrunfw_x86_64 has
`# CONFIG_KVM is not set` — the guest kernel ships only the
paravirt-guest helpers and /dev/kvm never appears. We clone
libkrunfw at LIBKRUNFW_VERSION, apply
libkrunfw-overrides/config-libkrunfw_x86_64.patch (enable KVM,
KVM_INTEL, KVM_AMD; olddefconfig pulls in the rest), build, and
bundle the resulting .so into the per-platform npm subpackage.
Result cached on (version, arch, patch-hash, workflow-hash) so the
+5min build only runs on a real change. CI sanity-checks
`nm vmlinux | grep kvm_dev_ioctl` so a silent patch failure becomes
a CI error instead of a runtime mystery.
2. agent-vm pins MSB_HOME to its own state dir at startup so msb's
writable state (db, cache, sandboxes, logs, tls/CA) lives under
$XDG_STATE_HOME/agent-vm/msb-home/ instead of ~/.microsandbox/.
The bundled `msb` + `libkrunfw` are read-only and discovered via
MSB_PATH + msb's existing "sibling-of-msb / ../lib/" resolution,
so no copy or sync into MSB_HOME is needed. Side-effect: a user's
stale upstream libkrunfw.so.X.Y.Z at ~/.microsandbox/lib/ can
never shadow ours.
Drops the `ensure_runtime_installed` / `microsandbox::setup::install`
download path entirely — the npm bundle is the source of truth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`agent-vm setup` calls `images/build.sh`, which built and pushed via
the legacy `docker build` + `docker push` path — leaving every layer
gzip-compressed. The OCI→EROFS conversion on the user's first launch
then spends ~95% of its CPU time in single-threaded gzip decode.
Bench against /usr/lib (856 MB) — see crates/agent-vm/examples/
erofs_bench.rs:
gzip layers: ingest = 143.08s (6.0 MB/s effective uncompressed)
zstd layers: ingest = 5.01s (171.0 MB/s effective uncompressed)
≈24× faster end-to-end pull pipeline with no microsandbox changes —
`tar_ingest.rs:427` already accepts `application/vnd.oci.image.layer
.v1.tar+zstd` automatically.
Switch the script to `docker buildx build --output type=registry,
compression=zstd,compression-level=3,force-compression=true,push=
true`:
- `--output type=registry,push=true` replaces the build + separate
push, because buildx with `--push` cannot also carry `--output`
attributes, but `type=registry,push=true` is equivalent and lets
us attach the compression options to the same step.
- `force-compression=true` re-emits base-image layers as zstd too;
without it, only the layers we add get re-compressed and the rest
stay gzip, halving the win.
- `compression-level=3` is zstd's default — bench shows diminishing
returns past that for binary-heavy layers.
- `registry.insecure=true` is needed because we push to a loopback
HTTP registry; `--push`-via-buildx doesn't inherit the daemon's
`insecure-registries` config.
Adds preflight checks: bail with a clear message if `docker` or
`docker buildx` aren't present (modern Docker bundles buildx; older
installs need an upgrade).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
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>
This reverts commit 90376ec.
git-restore-mtime ran cleanly but cargo still recompiled every
path-dep workspace crate on the next warm run. rust-cache's own
README documents the limitation: cargo's fingerprint check for
path-dep crates forces a rebuild regardless of mtime — likely
dep-info absolute-path canonicalization or build-script reruns.
The remaining wins (parallel jobs, relaxed LTO on msb,
Swatinem/rust-cache with workspace crates + save-if=true) still
get us 11m31s -> ~3m wall-clock cold and don't get hurt by warm
also being ~3m.
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).
actions/checkout sets every source file's mtime to "now". cargo's
fingerprint fast-path compares source mtime to .fingerprint
timestamps in target/; with the rust-cache-restored target/, sources
look newer than fingerprints and cargo recompiles every path-dep
crate + the final binary. ~90s/job wasted.
git-restore-mtime (from MestreLion/git-tools) walks git log and sets
each file's mtime to its last-touching commit date, so fingerprints
match between runs.
Also bumps `fetch-depth: 0` on both checkouts so git-restore-mtime
has the full history it needs (default depth=1 only sees one commit).
rust-cache@v2 defaults to cache-workspace-crates=false. It strips
target/release/{deps,build}/* entries belonging to path = "..."
crates before saving, so a warm-cache run still recompiles the
9 vendor/microsandbox workspace crates (~28s) and re-links the
agent-vm binary under LTO (~70s).
Set cache-all-crates and cache-workspace-crates to true so the
full target/ is preserved. Trades ~600 MB of cache space per
job for actually-warm builds.
rust-cache@v2 defaults to save-if: github.ref == default_branch.
Releases fire from v*.*.* tags or workflow_dispatch on
rewrite-microsandbox; neither is main, so the cache never
persisted and every run was effectively cold. set save-if: true
so subsequent runs can restore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three speedups stacked, expected to roughly halve wall time
(~10–11 min cold / ~5–7 warm → ~5 cold / ~2 warm):
1. **Split into parallel jobs.** `build-platform` was building
agent-vm + msb sequentially on one runner; now `build-agent-vm`
and `build-msb` are independent matrix jobs that run on
separate runners. A new `package` job downloads both artifacts,
fetches libkrunfw inline (~10s curl, not worth its own job),
and assembles the per-platform npm subpackage.
2. **Swatinem/rust-cache@v2** replaces our hand-rolled
actions/cache step. Independent caches per build job (workspace
root vs vendor/microsandbox), keyed automatically on rustc
version + Cargo.lock + a salt — drops the manual rustc-hash
step we wired in earlier this session. msb job also adds
`env-vars: CARGO_PROFILE_RELEASE_LTO ...` so a relax-LTO flip
never reuses a fat-LTO build's stale rlibs.
3. **Relax msb LTO in CI.** vendor/microsandbox's release profile
is `lto=true, codegen-units=1` (~3–4 min single-threaded link).
For a TLS proxy the runtime perf gain is negligible — override
via env at the job level:
CARGO_PROFILE_RELEASE_LTO=thin
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
Keep panic=abort and strip=true (those change behaviour /
reduce size, worth keeping).
`publish` now depends on `package` instead of `build-platform`.
The final artifact path is unchanged (`agent-vm-${platform}/{bin,lib}`)
so the publish steps need no changes.
v0.1.5 cuts a release to exercise the new layout end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores the MCP relay endpoint to the Anthropic placeholder's
allowed_hosts list. Previously added in dbe9210 and inadvertently
reverted by 31b24cc's stale-working-tree commit-a; not picked back
up by the subsequent rename / version-bump commits.
Without it, Claude Code's MCP traffic trips the secrets-handler
violation scan (the Anthropic placeholder appearing on a host
that's not in this secret's allow_host list) and the conn drops —
breaking MCP entirely for the in-VM agent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up `de9a1b2` which adds the missing doc comment that was
tripping the crate-level `#![warn(missing_docs)]` and printing a
warning during every release build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The name `agent-vm` was overloaded: it's both the runtime tool
(Rust binary on npm) AND the OCI image the tool boots inside each
microVM. Users seeing `ghcr.io/wirenboard/agent-vm:latest` would
reasonably guess that's where the *runtime* lives, but it's
actually the guest rootfs.
Rename the ghcr package to `agent-vm-template` and add OCI labels
(`org.opencontainers.image.title` / `description`) that explicitly
say "guest template image — install the runtime via npm". The
description shows on the ghcr.io package page.
Also rename the local-registry tag (`localhost:5000/agent-vm-template:latest`)
for consistency with the dev workflow.
Touches:
- DEFAULT_IMAGE_REF and every help/doctring referencing the ghcr
image (setup.rs, run.rs, pull.rs, image_api_version.rs,
image_check.rs).
- images/Dockerfile — new header + OCI labels.
- images/build.sh — local registry tag.
- .github/workflows/build-image.yml — IMAGE env.
- README.md + npm-dist/README.md.
- image_check tests updated to assert the new name.
Bumps to v0.1.4 so npm + ghcr converge on the new name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Workflow log shows v0.1.1 published cleanly with sigstore-signed
provenance + valid integrity, but the actual
agent-vm-linux-x64-0.1.1.tgz blob 404s on origin (Cloudflare
forwarded). 25 minutes post-publish, still missing. Likely an
npm-side upload glitch on the 36MB tarball. Re-publishing as a
fresh version is simpler than waiting on npm support.
v0.1.0: binaries lack execute bit. Deprecated.
v0.1.1: subpackage tarball missing on origin. Deprecated.
v0.1.2: ↑ this should be the first usable version.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.1.0 was published but the binaries inside the linux-x64
subpackage tarball landed at 0644 (no execute) — actions/
upload-artifact@v4 zips up the files without preserving POSIX
permissions, and npm publishes the round-tripped result as-is.
Users on v0.1.0 hit EACCES from the launcher's spawnSync.
Fix: add an explicit `chmod +x` step in the publish job after the
artifact download. Bump to v0.1.1 since npm versions are immutable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Node's `require()` only treats a path as relative when prefixed with
`./`. Bare `npm-dist/...` is interpreted as a node_modules /
built-in module spec and throws 'Cannot find module'. The
version-rewrite step above already used `fs.readFileSync` (which IS
cwd-relative) so it worked; this step used `require` and didn't.
Add the `./` prefix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
actions/download-artifact@v5 with `pattern:` + a single matched
artifact does NOT wrap into a per-artifact subdir (behaviour change
from v4) — files land flat at `path:`. With one platform that meant
bin/ + lib/ ended up at npm-dist/{bin,lib} instead of
npm-dist/agent-vm-linux-x64/{bin,lib}, and the verify step
short-circuited the whole publish.
Switch to one download step per platform with explicit `name:` +
`path:`. Robust to v5's single-vs-multiple-artifact behaviour, and
forces an explicit step per added platform (a soft reminder when
extending the matrix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>