From 0468a2649ace684e93e9b47050efe89f2f6df4a6 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sun, 31 May 2026 18:57:07 +0000 Subject: [PATCH] B2: ship a native aarch64-linux release binary via npm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .cargo/config.toml | 15 +++ .github/workflows/release-npm.yml | 122 +++++++++++++++++- npm-dist/README.md | 17 ++- npm-dist/agent-vm-linux-arm64/README.md | 9 ++ npm-dist/agent-vm-linux-arm64/bin/.gitkeep | 0 npm-dist/agent-vm-linux-arm64/lib/.gitkeep | 0 npm-dist/agent-vm-linux-arm64/package.json | 22 ++++ .../agent-vm/bin/agent-vm.dispatch.test.js | 90 +++++++++++++ npm-dist/agent-vm/bin/agent-vm.js | 2 +- npm-dist/agent-vm/package.json | 3 +- 10 files changed, 262 insertions(+), 18 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 npm-dist/agent-vm-linux-arm64/README.md create mode 100644 npm-dist/agent-vm-linux-arm64/bin/.gitkeep create mode 100644 npm-dist/agent-vm-linux-arm64/lib/.gitkeep create mode 100644 npm-dist/agent-vm-linux-arm64/package.json create mode 100644 npm-dist/agent-vm/bin/agent-vm.dispatch.test.js diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ce486c6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,15 @@ +# Cross-compilation linkers. +# +# The `agent-vm` binary and the patched `msb` link a handful of C +# libraries (libcap-ng, libdbus, libsqlite3) plus the usual libc, so a +# cross build needs the matching cross linker — the default `cc` only +# knows how to emit host (x86_64) objects. +# +# CI (release-npm.yml, build-agent-vm/build-msb aarch64 matrix legs) +# installs `gcc-aarch64-linux-gnu` for the linker and the arm64 dev +# libraries (`libcap-ng-dev:arm64 libdbus-1-dev:arm64 +# libsqlite3-dev:arm64`, enabled via `dpkg --add-architecture arm64`) +# so the final link resolves. Locally, install the same Debian/Ubuntu +# packages to reproduce. +[target.aarch64-unknown-linux-gnu] +linker = "aarch64-linux-gnu-gcc" diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index cd33bbd..dc519ec 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -62,6 +62,16 @@ jobs: - platform: linux-x64 runner: ubuntu-latest cargo_target: x86_64-unknown-linux-gnu + # linux-arm64 is cross-compiled on the x64 runner (GitHub's + # hosted arm64 Linux runners aren't on the free tier). + # `gcc-aarch64-linux-gnu` provides the cross linker (wired + # up by the repo .cargo/config.toml); the arm64 dev libs the + # final link needs come in via `dpkg --add-architecture + # arm64` (see the install step). + - platform: linux-arm64 + runner: ubuntu-latest + cargo_target: aarch64-unknown-linux-gnu + cross: true runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 @@ -75,12 +85,28 @@ jobs: submodules: 'recursive' - name: Install Rust toolchain + linux deps + env: + CROSS: ${{ matrix.cross && '1' || '' }} run: | + set -euo pipefail rustup toolchain install stable --profile minimal --no-self-update rustup target add ${{ matrix.cargo_target }} - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - libcap-ng-dev libdbus-1-dev libsqlite3-dev pkg-config + if [[ -n "$CROSS" ]]; then + # Enable the arm64 multiarch repo, then pull the cross + # linker + arm64 builds of the C libs agent-vm / + # microsandbox link (libcap-ng, libdbus, libsqlite3). + # Without the `:arm64` dev libs the final link can't + # resolve -lcap-ng / -ldbus-1 / -lsqlite3 for the target. + sudo dpkg --add-architecture arm64 + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + gcc-aarch64-linux-gnu pkg-config \ + libcap-ng-dev:arm64 libdbus-1-dev:arm64 libsqlite3-dev:arm64 + else + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libcap-ng-dev libdbus-1-dev libsqlite3-dev pkg-config + fi - name: Cargo cache (rust-cache) uses: Swatinem/rust-cache@v2 @@ -110,7 +136,20 @@ jobs: cache-workspace-crates: "true" - name: cargo build --release -p agent-vm - run: cargo build --release --target ${{ matrix.cargo_target }} -p agent-vm + env: + # Cross pkg-config: point at the arm64 .pc files and let + # pkg-config run host→target (the `pkg-config` crate refuses + # cross queries unless PKG_CONFIG_ALLOW_CROSS=1). No-ops on + # the native x64 leg (CROSS unset). + CROSS: ${{ matrix.cross && '1' || '' }} + run: | + set -euo pipefail + if [[ -n "$CROSS" ]]; then + export PKG_CONFIG_ALLOW_CROSS=1 + export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig + export PKG_CONFIG_SYSROOT_DIR=/ + fi + cargo build --release --target ${{ matrix.cargo_target }} -p agent-vm - name: Upload agent-vm binary uses: actions/upload-artifact@v7 @@ -129,6 +168,12 @@ jobs: - platform: linux-x64 runner: ubuntu-latest cargo_target: x86_64-unknown-linux-gnu + # linux-arm64 cross-build — see build-agent-vm for the + # rationale (no free hosted arm64 Linux runner). + - platform: linux-arm64 + runner: ubuntu-latest + cargo_target: aarch64-unknown-linux-gnu + cross: true runs-on: ${{ matrix.runner }} env: # Override vendor/microsandbox's `lto=true, codegen-units=1` @@ -138,18 +183,34 @@ jobs: # runtime cost on a TLS-proxy workload. CARGO_PROFILE_RELEASE_LTO: "thin" CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "16" + # The superproject's .cargo/config.toml sets the aarch64 linker, + # but it is NOT on the config search path when building from + # vendor/microsandbox (a separate workspace), so set the linker + # via env here too. No-op on the native x64 leg. + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc steps: - uses: actions/checkout@v5 with: submodules: 'recursive' - name: Install Rust toolchain + linux deps + env: + CROSS: ${{ matrix.cross && '1' || '' }} run: | + set -euo pipefail rustup toolchain install stable --profile minimal --no-self-update rustup target add ${{ matrix.cargo_target }} - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - libcap-ng-dev libdbus-1-dev libsqlite3-dev pkg-config + if [[ -n "$CROSS" ]]; then + sudo dpkg --add-architecture arm64 + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + gcc-aarch64-linux-gnu pkg-config \ + libcap-ng-dev:arm64 libdbus-1-dev:arm64 libsqlite3-dev:arm64 + else + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libcap-ng-dev libdbus-1-dev libsqlite3-dev pkg-config + fi - name: Cargo cache (rust-cache) uses: Swatinem/rust-cache@v2 @@ -171,7 +232,15 @@ jobs: cache-workspace-crates: "true" - name: cargo build --release -p microsandbox-cli --bin msb + env: + CROSS: ${{ matrix.cross && '1' || '' }} run: | + set -euo pipefail + if [[ -n "$CROSS" ]]; then + export PKG_CONFIG_ALLOW_CROSS=1 + export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig + export PKG_CONFIG_SYSROOT_DIR=/ + fi cargo build --release --target ${{ matrix.cargo_target }} \ --manifest-path vendor/microsandbox/Cargo.toml \ -p microsandbox-cli --bin msb @@ -199,6 +268,15 @@ jobs: - platform: linux-x64 runner: ubuntu-latest arch: x86_64 + # linux-arm64: cross-build the kernel with ARCH=arm64 + + # CROSS_COMPILE=aarch64-linux-gnu-. Needs the arm64 kbuild + # .config seed patch (libkrunfw-overrides/ + # config-libkrunfw_aarch64.patch); the build step fails fast + # with a clear message if that seed hasn't been ported yet. + - platform: linux-arm64 + runner: ubuntu-latest + arch: aarch64 + cross: true runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 @@ -231,12 +309,20 @@ jobs: - name: Install kernel build deps if: steps.lk-cache.outputs.cache-hit != 'true' + env: + CROSS: ${{ matrix.cross && '1' || '' }} run: | + set -euo pipefail sudo apt-get update sudo apt-get install -y --no-install-recommends \ build-essential bc flex bison libelf-dev libssl-dev \ kmod cpio bzip2 xz-utils python3 python3-pyelftools \ curl ca-certificates patch + if [[ -n "$CROSS" ]]; then + # Cross toolchain for the arm64 kernel build. + sudo apt-get install -y --no-install-recommends \ + gcc-aarch64-linux-gnu + fi - name: Clone + patch + build libkrunfw if: steps.lk-cache.outputs.cache-hit != 'true' @@ -244,11 +330,24 @@ jobs: env: LK_V: ${{ steps.lkv.outputs.libkrunfw_version }} ARCH: ${{ matrix.arch }} + CROSS: ${{ matrix.cross && '1' || '' }} run: | set -euo pipefail + # The arm64 leg cross-compiles the kernel. It needs an arm64 + # kbuild .config seed; bail early with a clear message rather + # than silently emit a kernel built with the wrong config. + if [[ -n "$CROSS" && ! -f "libkrunfw-overrides/config-libkrunfw_${ARCH}.patch" ]]; then + echo "::error::libkrunfw-overrides/config-libkrunfw_${ARCH}.patch is missing — the arm64 libkrunfw kernel config seed has not been ported yet (see B2 notes)." + exit 1 + fi git clone --branch "v${LK_V}" --depth 1 \ https://github.com/containers/libkrunfw.git libkrunfw-src cd libkrunfw-src + # Cross build env for arm64: make picks up ARCH/CROSS_COMPILE. + if [[ -n "$CROSS" ]]; then + export ARCH=arm64 + export CROSS_COMPILE=aarch64-linux-gnu- + fi # 1) kbuild .config seed (CONFIG_KVM=y + Intel/AMD backends). patch -p1 < ../libkrunfw-overrides/config-libkrunfw_${ARCH}.patch # 2) Kernel source patches: libkrunfw's Makefile runs @@ -303,6 +402,9 @@ jobs: - platform: linux-x64 runner: ubuntu-latest libkrunfw_arch: x86_64 + - platform: linux-arm64 + runner: ubuntu-latest + libkrunfw_arch: aarch64 runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 @@ -419,6 +521,12 @@ jobs: name: agent-vm-linux-x64 path: npm-dist/agent-vm-linux-x64/ + - name: Download linux-arm64 artifact + uses: actions/download-artifact@v8 + with: + name: agent-vm-linux-arm64 + path: npm-dist/agent-vm-linux-arm64/ + - name: Verify artifact layout + restore executable bits # `actions/upload-artifact@v7` packs files into a zip that # doesn't preserve POSIX permissions. After download, the diff --git a/npm-dist/README.md b/npm-dist/README.md index d23c39f..a4d8f46 100644 --- a/npm-dist/README.md +++ b/npm-dist/README.md @@ -9,15 +9,14 @@ Templates and tooling for distributing `agent-vm` via npm. and `execve`s the prebuilt native binary from the matching per-platform subpackage. Declares per-platform subpackages as `optionalDependencies` so npm installs only the right one. -- `agent-vm-linux-x64/` — per-platform subpackage. Ships the - prebuilt `bin/agent-vm`, the patched `bin/msb`, and - `lib/libkrunfw.so.5.2.1`. agent-vm finds `msb` and `libkrunfw` via - `current_exe()`-relative paths so a user's separate microsandbox - install never shadows them. -- Future per-platform subpackages: `-linux-arm64`, `-darwin-arm64`, - `-darwin-x64`, `-win32-x64`. Add to the launcher's - `PLATFORM_PACKAGES` map and to the main package's - `optionalDependencies`. +- `agent-vm-linux-x64/`, `agent-vm-linux-arm64/` — per-platform + subpackages. Each ships the prebuilt `bin/agent-vm`, the patched + `bin/msb`, and `lib/libkrunfw.so.5.2.1`. agent-vm finds `msb` and + `libkrunfw` via `current_exe()`-relative paths so a user's separate + microsandbox install never shadows them. +- Future per-platform subpackages: `-darwin-arm64`, `-darwin-x64`, + `-win32-x64`. Add to the launcher's `PLATFORM_PACKAGES` map and to + the main package's `optionalDependencies`. ## How releases happen diff --git a/npm-dist/agent-vm-linux-arm64/README.md b/npm-dist/agent-vm-linux-arm64/README.md new file mode 100644 index 0000000..d3a6b1a --- /dev/null +++ b/npm-dist/agent-vm-linux-arm64/README.md @@ -0,0 +1,9 @@ +# @wirenboard/agent-vm-linux-arm64 + +Prebuilt `agent-vm` binary + patched `msb` + libkrunfw for linux/arm64. + +You don't install this directly. The user-facing package is +[`@wirenboard/agent-vm`](https://www.npmjs.com/package/@wirenboard/agent-vm), +which pulls this in as an `optionalDependency` on linux/arm64 hosts. + +Source: . diff --git a/npm-dist/agent-vm-linux-arm64/bin/.gitkeep b/npm-dist/agent-vm-linux-arm64/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/npm-dist/agent-vm-linux-arm64/lib/.gitkeep b/npm-dist/agent-vm-linux-arm64/lib/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/npm-dist/agent-vm-linux-arm64/package.json b/npm-dist/agent-vm-linux-arm64/package.json new file mode 100644 index 0000000..39bec8e --- /dev/null +++ b/npm-dist/agent-vm-linux-arm64/package.json @@ -0,0 +1,22 @@ +{ + "name": "@wirenboard/agent-vm-linux-arm64", + "version": "0.0.0", + "description": "agent-vm prebuilt binary + patched msb + libkrunfw for linux-arm64.", + "homepage": "https://github.com/wirenboard/agent-vm", + "repository": { + "type": "git", + "url": "git+https://github.com/wirenboard/agent-vm.git" + }, + "license": "MIT", + "os": ["linux"], + "cpu": ["arm64"], + "files": [ + "bin/agent-vm", + "bin/msb", + "lib/libkrunfw.so.*", + "README.md" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/npm-dist/agent-vm/bin/agent-vm.dispatch.test.js b/npm-dist/agent-vm/bin/agent-vm.dispatch.test.js new file mode 100644 index 0000000..3451741 --- /dev/null +++ b/npm-dist/agent-vm/bin/agent-vm.dispatch.test.js @@ -0,0 +1,90 @@ +#!/usr/bin/env node +// Deterministic dispatch test for the npm launcher (bin/agent-vm.js). +// +// Why this exists: the launcher's job is to map the running +// platform/arch to a per-platform subpackage and then build the +// `bin/agent-vm` path inside it. arm64 was added to that mapping in +// B2, but arm64 dispatch cannot be exercised on an x86_64 CI host — +// `node` reports the host's real process.arch, so simply running the +// launcher there only ever exercises the linux-x64 leg. This test +// closes that gap without a real arm64 runtime by re-deriving the +// mapping + path logic from the launcher source and asserting the +// linux-arm64 key resolves to the expected package and to the same +// bin/ layout as linux-x64. +// +// Run standalone: `node bin/agent-vm.dispatch.test.js` (no test +// harness / deps — matches the repo convention of sanity-checking the +// launcher with plain `node`/`node --check`; there is no Node test +// runner or root package.json in this repo). +// +// It must stay in lockstep with the real launcher: it extracts the +// live PLATFORM_PACKAGES object out of agent-vm.js (rather than +// hard-coding a copy) so a future edit to the mapping that forgets +// arm64 — or renames the package — fails here instead of silently +// shipping a launcher that falls through to "unsupported platform" +// on arm64 hardware. + +"use strict"; + +const assert = require("node:assert"); +const path = require("node:path"); +const fs = require("node:fs"); +const vm = require("node:vm"); + +const launcherPath = path.join(__dirname, "agent-vm.js"); +const src = fs.readFileSync(launcherPath, "utf8"); + +// Pull the `PLATFORM_PACKAGES = { ... };` object-literal out of the +// launcher source and evaluate just that literal in an isolated VM +// context. We deliberately do NOT `require()` the launcher: it runs +// its dispatch + process.exit() at module load, so requiring it would +// terminate this test process. +const m = src.match(/const\s+PLATFORM_PACKAGES\s*=\s*(\{[\s\S]*?\});/); +assert.ok(m, "could not locate PLATFORM_PACKAGES object literal in agent-vm.js"); +const PLATFORM_PACKAGES = vm.runInNewContext(`(${m[1]})`); + +// Mirror the launcher's binPath construction (the +// `path.join(dir, "bin", "agent-vm"+ext)` line) so we assert the SAME +// layout the launcher actually uses. +function binPathFor(pkgDir, platform) { + const ext = platform === "win32" ? ".exe" : ""; + return path.join(pkgDir, "bin", `agent-vm${ext}`); +} + +// 1) The new arm64 key must resolve to the arm64 subpackage. +assert.strictEqual( + PLATFORM_PACKAGES["linux-arm64"], + "@wirenboard/agent-vm-linux-arm64", + "linux-arm64 must map to @wirenboard/agent-vm-linux-arm64", +); + +// 2) x64 must still resolve (guards against an accidental clobber). +assert.strictEqual( + PLATFORM_PACKAGES["linux-x64"], + "@wirenboard/agent-vm-linux-x64", + "linux-x64 must map to @wirenboard/agent-vm-linux-x64", +); + +// 3) Both linux platforms must produce the identical bin/ layout +// (only the package dir differs) — the arm64 subpackage ships its +// binary at bin/agent-vm exactly like x64 (see its package.json +// `files` list + the bin/.gitkeep placeholder). +const x64Bin = binPathFor("/pkg/agent-vm-linux-x64", "linux"); +const arm64Bin = binPathFor("/pkg/agent-vm-linux-arm64", "linux"); +assert.strictEqual(path.basename(x64Bin), "agent-vm"); +assert.strictEqual(path.basename(arm64Bin), "agent-vm"); +assert.strictEqual( + path.relative("/pkg/agent-vm-linux-x64", x64Bin), + path.relative("/pkg/agent-vm-linux-arm64", arm64Bin), + "arm64 and x64 must use the same bin/ layout", +); + +// 4) A genuinely unsupported platform key must be absent so the +// launcher hits its "no prebuilt binary" error path cleanly. +assert.strictEqual( + PLATFORM_PACKAGES["sunos-sparc"], + undefined, + "unsupported platform keys must be absent (no fall-through entry)", +); + +console.log("agent-vm dispatch test: OK (linux-x64, linux-arm64)"); diff --git a/npm-dist/agent-vm/bin/agent-vm.js b/npm-dist/agent-vm/bin/agent-vm.js index 84608d0..dcad421 100755 --- a/npm-dist/agent-vm/bin/agent-vm.js +++ b/npm-dist/agent-vm/bin/agent-vm.js @@ -22,7 +22,7 @@ const fs = require("node:fs"); const PLATFORM_PACKAGES = { "linux-x64": "@wirenboard/agent-vm-linux-x64", - // "linux-arm64": "@wirenboard/agent-vm-linux-arm64", + "linux-arm64": "@wirenboard/agent-vm-linux-arm64", // "darwin-arm64": "@wirenboard/agent-vm-darwin-arm64", // "darwin-x64": "@wirenboard/agent-vm-darwin-x64", // "win32-x64": "@wirenboard/agent-vm-win32-x64", diff --git a/npm-dist/agent-vm/package.json b/npm-dist/agent-vm/package.json index 131f3f8..c6f9631 100644 --- a/npm-dist/agent-vm/package.json +++ b/npm-dist/agent-vm/package.json @@ -22,7 +22,8 @@ "node": ">=18" }, "optionalDependencies": { - "@wirenboard/agent-vm-linux-x64": "0.0.0" + "@wirenboard/agent-vm-linux-x64": "0.0.0", + "@wirenboard/agent-vm-linux-arm64": "0.0.0" }, "publishConfig": { "access": "public"