The publish "Verify artifact layout" step globbed npm-dist/agent-vm-*-*/bin
and required agent-vm + msb in each. The arm64 subpackage's bin/ exists in
the checkout (a committed .gitkeep placeholder), so with arm64 not built
this release the step failed: "missing npm-dist/agent-vm-linux-arm64/bin/
agent-vm". Skip any platform dir without a built binary, mirroring the
publish loop's guard. The x64 binary is still verified + chmod'd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
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
Dropping the microsandbox SDK `prebuilt` feature means agent-vm compiles the
guest agentd from the pinned source instead of downloading it, so the
filesystem crate's build.rs now requires vendor/microsandbox/build/agentd to
exist (else it panics "agentd binary not found … run just build-deps"). The CI
build-and-test job ran `cargo build -p agent-vm` with no agentd, so it failed.
Add a step that builds the static musl agentd into build/agentd first (mirrors
the release-npm workflow) + musl-tools.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
Code-review follow-up to the deterministic-manifest change. SOURCE_DATE_EPOCH
only clamps the image CONFIG (created + history); layer file mtimes still
defaulted to build wall-clock, so the manifest digest was reproducible only
while the gha layer cache held. On a cache miss (eviction / size cap /
base-layer invalidation) the apt/curl RUN steps re-ran and emitted layers
with fresh mtimes → new diff_ids → new manifest digest for an unchanged
commit, re-introducing the ~700 MiB consumer re-pull.
- outputs: add `rewrite-timestamp=true` so buildkit clamps every in-layer
file mtime to SOURCE_DATE_EPOCH (the commit time). Layer digests — and
thus the manifest digest — are now reproducible independent of cache
state; the digest moves only on a real content change.
- sbom: false — pin it (defaults off today) so a future buildx default flip
can't re-wrap the single-platform build in an attestation index whose
build-time digest would churn :latest hourly. Complements provenance:false.
- "Show pushed digest" now asserts the pushed `:sha-…` is a single image
manifest, not an index — fails the build if an attestation ever leaks back
in and silently restores the churn.
Follow-up to #15 / part of #17.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gtxsKVWPrR8K1M1Te57MG
PR #15 stopped the agent layers from rebuilding when no agent released,
but consumers still re-downloaded the whole image (~700 MiB) on every
hourly :latest. Root cause: the image config carried a fresh build
timestamp each hour (the `org.opencontainers.image.created=<hour>`
label + buildkit's own config/history timestamps), so the linux/amd64
manifest digest changed every cron build despite byte-identical layers.
microsandbox keys its fsmeta/VMDK materialization on the manifest
digest (registry/client.rs: `layer_force = force || !fsmeta_valid`,
fsmeta keyed by manifest_digest), and `agent-vm pull` uses
PullPolicy::Always — so a new manifest digest forces re-download +
re-materialize of every layer. Measured: pulling a fresh hourly tag
with byte-identical layers re-fetched 723 MiB; pulling the SAME
manifest again fetched 0.
Make the manifest deterministic so it only moves on real content
change:
- SOURCE_DATE_EPOCH = HEAD commit time, fed to buildkit, clamps the
config `created` field and history timestamps to a per-commit
constant. Same commit + same agent versions + same layers => identical
config => identical manifest digest.
- Drop the per-hour `created` label (it lived in the config and was the
main churn source). `revision` (commit sha) is stable across a
commit's hourly builds.
- provenance: false — the attestation manifest embeds build time and
would churn the multi-arch index digest (and the update banner) even
with a stable image manifest; we don't consume provenance.
Result: an unchanged hourly :latest keeps the same digest, so
`agent-vm pull`/`setup` is a true no-op (0 bytes) instead of
re-downloading ~700 MiB. A new commit or a changed agent layer moves
the digest as it should.
Follow-up to #15.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gtxsKVWPrR8K1M1Te57MG
Code review of the per-agent cache keys found two sources didn't track
what the installer actually installs — so the key could miss a real
update (stale image) or rebuild for nothing:
- claude: keyed on npm @anthropic-ai/claude-code `latest`, but
claude.ai/install.sh installs a NATIVE binary from
downloads.claude.ai/claude-code-releases/latest. npm dist-tags move
on a separate cadence (currently stable=2.1.170, latest=next=2.1.181),
so npm `latest` can diverge from the native channel. Switch to the
native `/latest` endpoint (a plain version string) that install.sh
itself reads.
- opencode: keyed on github sst/opencode, but opencode.ai/install
downloads from anomalyco/opencode (the repo was renamed). sst/* only
resolves today via GitHub's rename redirect. Pin the real repo.
codex was already consistent (install.sh resolves from the same
openai/codex releases/latest endpoint) — unchanged.
Also harden the step: per-source `fail()` messages (the old generic
`::error::` was unreachable under `set -e`, which aborts at the failing
assignment before the validation loop), and a plausibility check that
rejects an HTTP-200-but-garbage body (empty / null / HTML) becoming a
cache key. Dry-run resolves codex=rust-v0.141.0 opencode=v1.17.8
claude=2.1.181.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gtxsKVWPrR8K1M1Te57MG
The old AGENT_INSTALL_CACHEBUST=<hourly-timestamp> build-arg re-ran
all three agent-installer layers (+ the sanity check) on every hourly
cron build. Because `curl install.sh | bash` isn't reproducible, each
rebuild emitted fresh layer blobs -> a new ~200 MiB-of-agent-layers
image every hour even when no agent version changed, so :latest
consumers re-downloaded those layers hourly for nothing. (Confirmed
on published images: consecutive hourly builds share layers 1-9 but
get fresh digests for every agent layer, incl. the no-fs-change
sanity layer.)
Replace it with a per-agent version cache key: the workflow resolves
each agent's current upstream version (codex -> openai/codex
releases/latest, opencode -> sst/opencode releases/latest, claude ->
npm @anthropic-ai/claude-code latest) and passes AGENT_VERSION_{CODEX,
OPENCODE,CLAUDE}. Each installer RUN references its arg so BuildKit
rebuilds that layer ONLY when the version string changes; an unchanged
hourly build is now a pure cache hit (identical layer digests ->
nothing to re-pull). Resolution failure fails the build rather than
shipping a stale key and skipping a real update.
Also correct the layer-order rationale. #12 ordered the installers
"smallest first"; the real published sizes are codex 95 / opencode 50
/ claude 68 MiB compressed, and codex is also the most frequently
released (multiple stable cuts/day). Since a lower-layer change
cascades up through every layer above it, the topmost agent layer
re-emits on essentially every change -> the biggest/most-frequent
agent (codex) belongs at the BOTTOM. The existing order
(codex -> opencode -> claude) is already optimal across the plausible
frequency range; only the (wrong) comment is fixed, no reorder.
Follow-up to #12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gtxsKVWPrR8K1M1Te57MG
Why: the v0.1.22 publish failed — first npm E404 (NPM_TOKEN expired/
rejected), then ENEEDAUTH (token empty). Move off the long-lived token to
OIDC trusted publishing: npm mints short-lived credentials from the GitHub
Actions id-token (already `id-token: write` at the workflow level), and
provenance is generated automatically.
How (publish job):
- Drop NODE_AUTH_TOKEN / secrets.NPM_TOKEN from both publish steps.
- Drop setup-node's `registry-url:` — that input writes an empty
`//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}` into .npmrc, which
makes npm error ENEEDAUTH instead of using OIDC. The default registry is
registry.npmjs.org regardless.
- `npm install -g npm@latest` so the runner has npm >= 11.5.1 (node 22
ships npm 10.x, which has no OIDC trusted-publishing support).
Requires a one-time npm-side setup: a trusted publisher configured for BOTH
@wirenboard/agent-vm and @wirenboard/agent-vm-linux-x64 (repo
wirenboard/agent-vm, workflow release-npm.yml). The NPM_TOKEN secret is no
longer used and can be deleted.
Also restores the "Build agentd from source (musl)" step + the msb
`--no-default-features` build that a prior edit had dropped. agentd is
embedded in msb and must ship at the same revision as the producer: the
boot-params side channel (v0.1.22) needs both halves in lockstep. Without
this, msb downloads the stale 0.4.6 prebuilt agentd (no boot-params reader)
and silently drops every mount — for all users, not just non-ASCII paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
agent-vm mirrors the host cwd into the guest at the same absolute path,
but a Cyrillic-named directory crashed the VMM at boot: libkrun packs the
guest workdir + env into a printable-ASCII-only kernel command line and
.unwrap()s a non-ASCII byte into a SIGABRT. Three changes make the real
path work instead of falling back to /workspace:
1. Mount specs travel off the cmdline. The bumped submodule moves
MSB_DIR_MOUNTS/FILE/DISK to a boot-params file on the runtime virtiofs
share (agentd reads it at init). run.rs hands libkrun a '/' workdir
placeholder when the real path isn't cmdline-safe and pins the agent's
real cwd via the byte-safe vsock exec request (attach_with/.cwd on both
the TTY and streaming paths). A mandatory post-boot fs().exists(real)
probe fails loud if the bind didn't materialize (chdir failure is
otherwise silent). resolve_project_guest_path now mirrors the real path
for non-ASCII/whitespace; only a tmpfs-overlap or a control-char path
(unframable for boot-params) still falls back to /workspace.
2. UTF-8 locale. The base image ships with no locale (C/POSIX), which
renders a Cyrillic cwd as `M-P…` meta-escapes in bash/readline AND
makes filesystem encodings default to ASCII so Python/Node/ripgrep
mis-handle the path even when it mounted. Pin LANG=C.UTF-8 for every
guest (GUEST_ALWAYS_ENV in run.rs) and in images/Dockerfile.
3. Ship producer and consumer in lockstep. agentd is embedded in msb and
was downloaded as a release prebuilt; the new producer needs the new
consumer, so release-npm.yml's build-msb now builds agentd from the
pinned source (musl) and links msb with --no-default-features (drops
the `prebuilt` download). A stale prebuilt would have dropped every
mount for every user.
Verified: bash/readline renders the real Cyrillic path as glyphs under
C.UTF-8 (pty capture) and as the reported M-P escapes without it; the
boot-params channel carries the path byte-intact and a normal launch
mounts correctly. Full nested-guest agent runs hit this dev box's
overlay-on-virtiofs limit (the probe reports it); validated on a real
host via the path mirror.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Why
---
PLAN.md item "B2 — Cross-arch binaries" (section B, Distribution /
release) calls for cross-arch builds and per-platform npm packaging,
noting that "the package currently bundles a linux-x86_64 binary". As a
result Apple Silicon and ARM Linux users get no native artifact and fall
back to slow x86 emulation (Rosetta / qemu) — a poor fit for a tool whose
job is to launch microVMs quickly. This change delivers the linux/aarch64
half of B2: a genuine aarch64 binary, built, packaged, and selected at
install time, so ARM hosts run native.
How
---
Release workflow (.github/workflows/release-npm.yml): every build job
(build-agent-vm, build-msb, build-libkrunfw) and the package job gain a
linux-arm64 matrix leg. Because GitHub's hosted arm64 Linux runners
aren't on the free tier, the arm64 leg cross-compiles on the x64 runner:
a `cross: true` matrix flag drives a `CROSS` env switch that (a) enables
the arm64 multiarch apt repo and installs the cross linker
(gcc-aarch64-linux-gnu) plus the :arm64 dev libs agent-vm / msb link
against (libcap-ng, libdbus, libsqlite3), and (b) exports
PKG_CONFIG_ALLOW_CROSS / PKG_CONFIG_PATH / PKG_CONFIG_SYSROOT_DIR so the
`pkg-config` crate resolves the target libs. The msb job also sets
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER directly, since it builds
from vendor/microsandbox (a separate workspace not on the superproject's
.cargo config search path). All of these guards are no-ops on the native
x64 leg, so the existing x86_64 path is unchanged. The libkrunfw arm64
leg cross-builds the guest kernel with ARCH=arm64 / CROSS_COMPILE and
fails fast with a clear message if its arm64 kbuild .config seed
(libkrunfw-overrides/config-libkrunfw_aarch64.patch) hasn't been ported
yet, rather than silently shipping a mis-configured kernel. The publish
job downloads the new agent-vm-linux-arm64 artifact alongside the x64 one.
Cross toolchain config (.cargo/config.toml, new): pins the
aarch64-unknown-linux-gnu linker to aarch64-linux-gnu-gcc so a local
cross build reproduces CI; the file documents the matching apt packages.
npm launcher (npm-dist/agent-vm/bin/agent-vm.js,
npm-dist/agent-vm/package.json): un-comment the linux-arm64 entry in the
launcher's PLATFORM_PACKAGES map and add @wirenboard/agent-vm-linux-arm64
to the main package's optionalDependencies, so `npm install` pulls the
arm64 subpackage on ARM hosts and the launcher dispatches to it.
arm64 subpackage scaffold (npm-dist/agent-vm-linux-arm64/, new): mirrors
the x64 subpackage layout — package.json (os linux / cpu arm64),
README.md, and bin/.gitkeep + lib/.gitkeep placeholders — so the
directory exists in the tree for the release workflow to drop the
cross-built binary, msb, and libkrunfw into.
Dispatch test (npm-dist/agent-vm/bin/agent-vm.dispatch.test.js, new):
arm64 dispatch can't be exercised on an x86_64 CI host because node
reports the host's real process.arch. The test re-derives the launcher's
PLATFORM_PACKAGES map + bin-path logic from the source and asserts the
linux-arm64 key resolves to the arm64 subpackage with the same bin/
layout as x64, so a future edit that forgets arm64 fails here instead of
silently falling through to "no prebuilt binary" on ARM hardware.
README (npm-dist/README.md): document that the package now ships both the
linux-x64 and linux-arm64 per-platform subpackages.
macOS / darwin and win32 cross builds remain out of scope for this change
(still commented placeholders in the launcher) and are tracked under the
rest of B2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Why
---
Until now the only GitHub Actions we ran were the heavyweight, release-
oriented pipelines: build-image.yml (which bakes and pushes the guest OCI
template to GHCR) and release-npm.yml (which cross-compiles the binaries
and publishes the npm packages). Both are gated on tags / specific refs
and take many minutes, so day-to-day pushes and pull requests on the
rewrite branch got no automated feedback at all. A regression that broke
`cargo build` or a unit test could sit undetected on a feature branch
until release time, which is exactly when it is most expensive to find.
PLAN.md item B1 ("CI smoke") calls for a lightweight, always-on check
that gives every push and PR a fast pass/fail signal on the core Rust
workspace, independent of the slow image/release machinery.
How
---
This adds .github/workflows/ci.yml, a dedicated smoke workflow that runs
on push and pull_request so contributors get feedback on the change set
before it is merged, not at release time. It checks out the repository
(with the vendor/microsandbox submodule, since the workspace depends on
the vendored msb crates), sets up the Rust toolchain, and exercises the
agent-vm workspace with the standard cargo checks (build + test) so a
broken compile or a failing unit test fails CI immediately.
It is intentionally scoped as a smoke test rather than a full matrix:
the goal is a quick, reliable gate that catches the common breakages, so
it stays cheap enough to run on every commit and leaves the expensive
cross-compilation and image-publish flows to their existing dedicated
workflows. The two release pipelines are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Companion to #8. The hourly cron checks out rewrite-microsandbox's
Dockerfile (see the `ref:` override that lives on main's workflow
file), so the `ARG AGENT_INSTALL_CACHEBUST` placeholder right before
the three installer RUNs has to land here for the bust to bite. The
workflow-file edit is also bundled so workflow_dispatch from this
branch and any future merge-back to main behave consistently.
See PR #8 on main for the longer rationale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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.
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>
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>
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>
Top fixes from the max-effort review:
**Correctness:**
1. setup.rs::verify_image now calls image_api_version::check() and
runs each agent --version separately. Old code did
`cat ... && claude --version && ...` so a non-zero exit could
mean any of four causes; error message blamed only the API file.
New code names which check failed.
2. main.rs runs `point_at_msb` BEFORE constructing the tokio
runtime. setenv() is not thread-safe; the previous code ran
`unsafe { set_var }` after `#[tokio::main]` had already spawned
workers, technically UB if a worker read env concurrently
(reqwest / sea-orm do this on first use). Switched to plain
`fn main` + manual Runtime::new() so the env mutation happens
single-threaded.
3. release-npm.yml cache key now includes a hash of `rustc -V`.
Without this, an ubuntu-latest rustc rollover (we hit this
exact bug locally this session) reused cached rlibs from the
prior toolchain → undefined-symbol link errors.
4. release-npm.yml's subpackage publish loop is now idempotent:
skips already-published versions instead of erroring. Avoids
the split-state where a transient failure leaves subpackages
at v but the main package never published.
5. build-image.yml's retention switched from
`actions/delete-package-versions@v5`'s `min-versions-to-keep:
350` (which breaks if cron is paused for days) to an explicit
age-based prune via actions/github-script — deletes versions
older than 14 days regardless of build cadence.
**UX:**
6. pull.rs adds `AGENT_VM_INSECURE_REGISTRY=1` env-var escape hatch
so airgapped/intranet plain-HTTP registries
(`registry.corp.example:5000`) can opt in. The hostname
heuristic stays narrow (localhost/127.0.0.1/0.0.0.0/*.local/
*.localhost) for safety.
7. msb_install.rs: MSB_PATH set-but-file-missing now falls back to
the sibling-of-current_exe() if one exists (with a warn) and
gives a much clearer error otherwise. The vanilla-msb rejection
message now detects MSB_PATH presence and tells the user to
unset it instead of generically suggesting "set MSB_PATH
explicitly" (the LAST thing they need to hear when they just set
it).
8. setup.rs warns when running from a source checkout (manifest_dir
has images/Dockerfile) but the submodule isn't initialised —
instead of silently pulling ghcr.io and confusing the dev who
just edited images/Dockerfile.
9. npm-dist/agent-vm/bin/agent-vm.js' missing-optional error lists
network-flake as the top cause (it is) plus --no-optional and
inconsistent lockfile pinning. Previously blamed --no-optional
as the most likely cause, which it usually isn't.
10. README files for both npm packages so npmjs.com renders
something useful.
11. release-npm.yml's libkrunfw symlink step replaced the
`( cd && ln && ln )` subshell with absolute-path `ln -sf`s.
Subshell `set -e` propagation is bash-version-sensitive; the
new form is unconditional.
84/84 → 85/85 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminates the runtime dependency on superradcompany/microsandbox's
release tarball — we already vendor libkrunfw as a sub-submodule,
so building it ourselves is straightforward and removes:
- a foot-gun if upstream prunes / renames a release asset,
- the inability to patch libkrunfw if we ever need to,
- an asymmetry where we own the patched msb but borrow the .so.
Trade-off: a kernel build is ~10-15 min cold on the GH runner. Cache
keyed on the libkrunfw sub-submodule SHA makes that a one-time cost
per submodule bump — every subsequent release-npm run is a ~5 s
restore.
Steps:
1. Read `LIBKRUNFW_VERSION` / `LIBKRUNFW_ABI` from `vendor/
microsandbox/crates/utils/lib/lib.rs` (single source of truth).
2. Restore `vendor/microsandbox/vendor/libkrunfw/libkrunfw.so.<ver>`
from cache if present.
3. On miss: install kernel build deps (bc/bison/flex/libelf-dev/
libssl-dev/kmod/cpio/python3-pyelftools/xz-utils) and run
`make -j$(nproc)` in the libkrunfw tree. Cross-compile for arm64
when matrix target ≠ host.
4. Copy the .so + recreate `libkrunfw.so.<ABI>` and `libkrunfw.so`
symlinks into `npm-dist/agent-vm-$platform/lib/`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the placeholder libkrunfw step with a concrete fetch of
the upstream microsandbox release tarball at:
https://github.com/superradcompany/microsandbox/releases/download/v$MSB_VERSION/microsandbox-$os-$arch.tar.gz
(public release; verified 17 MiB, contains msb + libkrunfw.so.5.2.1).
The tarball's msb is discarded — we ship our own patched build — and
just libkrunfw* lands in `npm-dist/agent-vm-$platform/lib/`.
Versions come from the vendored source of truth (no duplication):
- `msb_version` ← `vendor/microsandbox/Cargo.toml` (workspace root,
since the cli crate uses `version.workspace = true`).
- `libkrunfw_version` / `libkrunfw_abi` ← `vendor/microsandbox/
crates/utils/lib/lib.rs` constants.
After extraction the workflow recreates the ABI symlinks that the
SDK's `setup::install` would normally make
(`libkrunfw.so.<ABI> -> libkrunfw.so.<VERSION>`,
`libkrunfw.so -> libkrunfw.so.<ABI>`), mirroring
`vendor/microsandbox/crates/microsandbox/lib/setup/download.rs::libkrunfw_symlinks`.
Smoke-tested locally: `curl -fsSL` follows GitHub's S3 redirect,
tarball extracts cleanly, glob finds libkrunfw.so.5.2.1 at the top
level (no nested prefix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>