Commit graph

38 commits

Author SHA1 Message Date
Evgeny Boger
5cf95e20ef release-npm: skip unbuilt platforms in publish's artifact-layout verify
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
2026-06-24 01:53:29 +03:00
Evgeny Boger
7e8ad37589 release-npm: build guest agentd in build-agent-vm; make arm64 legs non-blocking
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
2026-06-24 01:43:34 +03:00
Evgeny Boger
a8559ed5d4 ci: build guest agentd (musl) before cargo build
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
2026-06-23 23:16:50 +03:00
Evgeny Boger
d3194bf831 Merge origin/main into bump-microsandbox-wb
Brings in main's image work (reproducible :latest manifest, version-keyed
agent layers, WB diag CLIs + codestyle-pinned Python linters) alongside the
0.5.7 bump + 7-feature integration + agentd-teardown/fork-artifact fixes.

Conflicts resolved in images/Dockerfile:
- header/LABEL: describe the union (4 agents incl. Copilot + diag + codestyle).
- agent installs reordered so Claude precedes the LSP-plugin block (the merge
  had stranded the LSP install ahead of `agent-vm-install claude`, so
  /root/.local/bin/claude was absent and the unguarded seed-stash hard-failed).
- seed-stash made best-effort (skip when no plugins) to match the LSP block's
  soft-fail policy.
- sanity check: keep main's hard-requirements + agent-CLI loop, add copilot.

Verified: image builds (LSP ×4 installed + stashed, Copilot 1.0.64, sanity OK);
boots with D1 copilot + D2 plugins √ enabled; teardown fix intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-23 23:13:20 +03:00
Evgeny Boger
aba431cfc8 image: clamp layer mtimes (rewrite-timestamp) + pin sbom off + assert single manifest
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
2026-06-19 15:48:27 +00:00
Evgeny Boger
aaf88cfa46 image: make :latest manifest deterministic so unchanged pulls are no-ops
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
2026-06-19 10:41:32 +00:00
Evgeny Boger
620db380b9 image: fix agent version sources to match each installer's real channel
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
2026-06-18 10:44:22 +00:00
Evgeny Boger
2943cd940a image: key agent layers on upstream version, not hourly timestamp
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
2026-06-18 09:57:47 +00:00
Evgeny Boger
82474aed75 Merge remote-tracking branch 'origin/rewrite-microsandbox' into integration-7feat
# Conflicts:
#	.github/workflows/release-npm.yml
#	crates/agent-vm/src/run.rs
2026-06-02 20:31:42 +00:00
Evgeny Boger
c3fda73ab1 release-npm: publish via npm OIDC trusted publishing; restore agentd-from-source
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>
2026-06-01 20:26:08 +00:00
Evgeny Boger
0d5e576e47 Support non-ASCII (Cyrillic) project paths end to end
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>
2026-06-01 11:28:03 +00:00
Evgeny Boger
45655fb773 Merge branch 'b2-aarch64-binary' into integration-7feat 2026-06-01 11:20:51 +00:00
Evgeny Boger
0468a2649a B2: ship a native aarch64-linux release binary via npm
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>
2026-05-31 18:57:07 +00:00
Evgeny Boger
9c97658e6f ci: add a smoke-test workflow that builds and checks the workspace (B1)
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>
2026-05-31 14:21:22 +00:00
Evgeny Boger
e556ad3ad6 ci(build-image): bust cache for the three agent installers each run
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>
2026-05-29 13:17:34 +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
01f7b4b3bd ci(build-image): fix retain job — correct GHCR package name
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>
2026-05-25 20:14:08 +00:00
agent-vm
8cc5dbaf13 Merge branch 'images-zstd-layers': zstd-compress OCI layers
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).
2026-05-25 20:03:33 +00:00
agent-vm
484f967404 ci(build-image): also allow rewrite-microsandbox for moving tags
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>
2026-05-25 20:02:34 +00:00
agent-vm
db55962bea Merge branch 'nested-virt': ship libkrunfw with CONFIG_KVM=y
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>
2026-05-25 19:49:23 +00:00
agent-vm
ce6b9cacca ci(build-image): zstd-compress layers + gate moving tags to main
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>
2026-05-25 19:49:10 +00:00
agent-vm
6b2fdea71a feat: ship libkrunfw with CONFIG_KVM=y; reroute msb state off ~/.microsandbox
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>
2026-05-25 19:49:00 +00:00
Evgeny Boger
3163a5961a Revert "ci(release-npm): restore source mtimes post-checkout so cargo cache works"
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.
2026-05-25 16:47:44 +03:00
Evgeny Boger
90376ecb6c ci(release-npm): restore source mtimes post-checkout so cargo cache works
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).
2026-05-25 16:35:50 +03:00
Evgeny Boger
ff9d3bee28 ci(release-npm): cache path-dep crates so warm runs actually skip work
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.
2026-05-25 15:38:52 +03:00
Evgeny Boger
1d5a8c4b42 ci(release-npm): force rust-cache save on non-default refs
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>
2026-05-25 15:29:15 +03:00
Evgeny Boger
94646ceead ci(release-npm): fix chmod line-continuation (YAML scalar folding) 2026-05-25 15:11:34 +03:00
Evgeny Boger
f7dc485472 ci(release-npm): parallel build + rust-cache + relax msb LTO
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>
2026-05-25 15:03:18 +03:00
Evgeny Boger
1d5f36c451 rename ghcr image: agent-vm → agent-vm-template + clarify it's the guest
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>
2026-05-25 14:39:57 +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
6bee1f5626 v0.1.1: bump + ci chmod fix
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>
2026-05-25 13:46:52 +03:00
Evgeny Boger
953fb4aa47 ci(release-npm): fix node require path in idempotent-skip helper
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>
2026-05-25 13:12:13 +03:00
Evgeny Boger
643b24953b ci(release-npm): download each platform artifact by exact name
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>
2026-05-25 13:05:15 +03:00
Evgeny Boger
494b8dd0aa distribution: address code-review findings before v0.1.0
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>
2026-05-25 11:43:08 +03:00
Evgeny Boger
3df904c632 Revert "ci(npm-release): build libkrunfw from source instead of pulling upstream binary"
This reverts commit 631da16a41.
2026-05-25 00:38:06 +03:00
Evgeny Boger
631da16a41 ci(npm-release): build libkrunfw from source instead of pulling upstream binary
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>
2026-05-25 00:25:30 +03:00
Evgeny Boger
42df2b4ff5 ci(npm-release): wire libkrunfw bundling from upstream microsandbox
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>
2026-05-25 00:18:42 +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