Commit graph

249 commits

Author SHA1 Message Date
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
b2cadc56d8 v0.1.23: msb jemalloc allocator (fix host RSS balloon)
Bumps the vendored msb to use jemalloc as its global allocator. On
many-core hosts glibc's per-thread malloc arenas (8*ncpu, 64 MiB each)
are retained and never returned to the OS under the sandbox supervisor's
Rust worker-thread churn (virtio-fs / block / blocking pools), ballooning
host RSS to 10-20+ GiB even for a `--memory 4` sandbox — the guest RAM is
a separate, correctly-capped mapping. jemalloc (background_threads on)
purges freed pages back to the OS, so host RSS tracks the live set.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:45:40 +03:00
Evgeny Boger
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
010c45c190 Merge branch 'd2-lsp-plugins' into integration-7feat 2026-06-01 19:51:37 +00:00
Evgeny Boger
7a21c83b32 Merge branch 'd1-copilot-agent' into integration-7feat
# Conflicts:
#	crates/agent-vm/src/secrets.rs
2026-06-01 14:39:04 +00:00
Evgeny Boger
31fa95b67c v0.1.22: bump for Cyrillic path support
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:30:20 +00:00
Evgeny Boger
3d512fa545 Merge fix-cyrillic-cwd: non-ASCII (Cyrillic) project path support
Mirrors a Cyrillic/whitespace project path at its real location instead
of crashing the VMM (cmdline InvalidAscii) or falling back to /workspace:
mount specs ride a boot-params side channel, LANG=C.UTF-8 is pinned, and
the release builds agentd from source so producer+consumer ship together.
Bumps vendor/microsandbox to 62afcdf.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:28:24 +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
05aea4c281 Merge branch 'a2-refresh-single-flight' into integration-7feat 2026-06-01 11:20:51 +00:00
Evgeny Boger
74832b56af Merge branch 'a1-token-rotation-test' into integration-7feat 2026-06-01 11:20:51 +00:00
Evgeny Boger
89b54bdac7 Merge branch 'b3-ipv6-dns-fix' into integration-7feat 2026-06-01 11:20:51 +00:00
Evgeny Boger
45655fb773 Merge branch 'b2-aarch64-binary' into integration-7feat 2026-06-01 11:20:51 +00:00
Evgeny Boger
aa067d8de6 Merge branch 'b1-ci-smoke' into integration-7feat 2026-06-01 11:20:50 +00:00
Evgeny Boger
a1f16291fb D2: install LSP language servers in the image for in-VM code intelligence
Why
---
In-VM Claude has no code intelligence for the languages it edits most:
the guest image ships toolchains but no Language Server Protocol servers,
so completion, diagnostics, hover, and go-to-definition are unavailable
for C/C++, Python, TypeScript, and Go work done inside the sandbox. The
original Bash agent-vm closed this gap in its `setup` step
(claude-vm.sh:353-361), but the microsandbox rewrite — whose image is
Docker-built once rather than provisioned per-setup — dropped it.

This is the D2 item from PLAN.md ("LSP plugins in the image"): port the
four language servers the original's `setup` installed so that an agent
running in the guest gets the same editor-grade intelligence the original
provided, without any host round-trip.

How
---
images/Dockerfile gains an image-build step that installs the four
language servers the original declared — clangd-lsp (C/C++),
pyright-lsp (Python), typescript-lsp (TypeScript), and
gopls-lsp@claude-plugins-official (Go) — and pre-warms them so the first
in-VM invocation is not paying first-run download/extraction cost. Doing
this at build time (rather than per-setup or on first launch) keeps the
fresh-VM-per-launch model fast: the servers are baked into the image
layer and ready the moment the microVM boots, matching PLAN.md's
"Just Dockerfile + pre-warm" scope for this item.

No Rust, run-path, or version changes are involved; the feature is
entirely contained in the image definition, so existing launch behavior
is unchanged except that the language servers are now present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:42:00 +00:00
Evgeny Boger
1b0688a3ce feat: add GitHub Copilot as a selectable in-VM agent (PLAN D1)
Why
---
Until now the sandbox only knew how to launch Claude inside the microVM.
PLAN.md item D1 ("Copilot agent") calls for first-class support for GitHub
Copilot so that users who work with Copilot get the same isolated,
credential-scoped VM treatment Claude already enjoys, rather than running
Copilot on the host with broad ambient access. Running Copilot inside the VM
puts its network egress, filesystem view, and credential exposure under the
existing sandbox policy instead of the host environment.

How
---
The change spans the image, the credential layer, and the launch path:

- images/Dockerfile: provision the GitHub Copilot CLI in the guest image
  next to the existing agent tooling, so a pulled image can run Copilot with
  no extra in-VM setup.
- crates/agent-vm/src/host_paths.rs: resolve the host-side location of
  Copilot's credential/config files so secrets discovery knows where to read
  them from.
- crates/agent-vm/src/secrets.rs: teach the secrets layer to discover
  Copilot's credentials on the host and forward them into the guest the same
  way the other agents' tokens are handled, keeping the credential surface
  scoped to the VM. This is the largest part of the change.
- crates/agent-vm/src/run.rs: wire Copilot into the launch path so it can be
  chosen as the agent to run, threading the new credential material through to
  the guest invocation.
- crates/agent-vm/src/main.rs: surface the new agent choice at the CLI layer.

Together these complete PLAN.md item D1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:23:56 +00:00
Evgeny Boger
9676f6d9a4 net: keep all DNS address families and order by route preference (B3)
Why: the DNS resolver historically collected only the first IPv4 result
(`filter(|a| a.is_ipv4()).take(1)`), silently discarding every AAAA
record. That made IPv6-only hosts completely unreachable and also broke
dual-stack hosts whenever the A record was stale or misrouted — the
caller had no fallback because the alternate-family addresses had already
been thrown away during resolution. This is the B3 backlog item in
PLAN.md ("resolver dropped AAAA records, breaking IPv6-only hosts").

How: `DnsResolver::resolve` now retains every address returned by
`to_socket_addrs` and returns the full set so the caller can try each in
turn, happy-eyeballs style, instead of being handed a single address. An
empty result is now surfaced as an explicit error rather than an empty
vec. The addresses are stably sorted by family preference: when a usable
global IPv6 route is detected we order IPv6 first, otherwise IPv4 first,
so a host stays reachable even when one family is misconfigured. Route
preference is determined by `have_global_ipv6`, a best-effort probe that
binds a UDP socket and connects it to a public IPv6 DNS address
(2001:4860:4860::8888) to learn the locally selected source address —
this performs no traffic and a wrong answer only changes connection
order, never correctness.

A regression test (tests/ipv6_dns_test.rs) covers the new behavior:
`resolve` must return a non-empty set for a dual-stack name and must
preserve the requested port across every returned address regardless of
the chosen ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:33:07 +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
60a75ae0fc v0.1.21: bump for help review fixes
Ships the follow-up to the v0.1.x `--help` regroup: a code review of
that change surfaced one functional doc bug and several accuracy /
clarity gaps, all fixed in the merged `help: fix review findings`
commit.

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 18:38:30 +03:00
Evgeny Boger
7ebb0b0a3d help: fix review findings in the regrouped CLI help
Follow-up to the help regroup (f881e07), addressing a code review of
that change.

  - The `--help` example `agent-vm shell -- -c 'cargo test'` was wrong
    and would not run. The launcher already wraps a Shell agent's
    trailing args in `bash -c <shell-escaped args>`, so a literal `-c`
    is double-applied: the guest runs `bash -c "-c 'cargo test'"`, bash
    takes `-c` as the command name -> `bash: -c: command not found`,
    exit 127. Changed to `agent-vm shell -- cargo test`.

  - The Examples/Networking/Environment footer hangs off the single
    `run::Args` shared by claude/codex/opencode/shell, so it renders
    verbatim under every verb. The examples are claude-flavoured
    (`-- --model opus`), which read as authoritative under
    `agent-vm codex --help`. Labelled the block "(claude shown;
    codex/opencode/shell take the same options)" and dropped the
    now-false "command-agnostic" comment.

  - The Environment section omitted two env vars the launch path
    honours: AGENT_VM_STATE_DIR (host_paths::state_root) and
    AGENT_VM_INSECURE_REGISTRY (is_plain_http_registry). Added both.

  - `--publish` value_name was `[BIND:]HOST:GUEST`; the port fields read
    as hostnames. Changed to `[BIND:]HOST_PORT:GUEST_PORT`, matching the
    real parse format and the per-flag long help.

  - Rewrote the networking cheatsheet: dropped the per-row format
    abbreviations (they duplicated the value_names and pushed the
    longest row to ~88 cols, which wrap_help reflows and de-aligns on an
    80-col terminal) and kept the direction arrows. All footer lines are
    now <=76 cols.

  - Cargo.toml `description` said "Sandboxed VMs"; `about` says
    "microVMs". Aligned to "microVMs".

Presentation-only; no flag, env var, or behaviour changed.
`cargo test -p agent-vm`: 123 passed. Rendered `-h`/`--help` for claude
and codex verified against the rebuilt binary.

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:22:34 +00:00
Evgeny Boger
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
2e94f3276f secrets: single-flight the in-guest credential refresh (PLAN A2)
Why
---
When an in-guest token expires, the intercept hook rotates it by asking
the host to run the provider CLI (`claude -p hi` for Anthropic, `codex
exec` for OpenAI) and then re-reading the freshly written credential
file. Under load this races: if two requests notice the expired token at
nearly the same time, each independently drives a full host-side
rotation. The result is two concurrent `claude`/`codex` launches for a
single logical refresh -- wasted host CPU, duplicate browser/OAuth churn,
and, worse, two writers racing on the same credential file, which can
leave a half-written token behind and cascade into further refreshes.

How
---
Serialize host rotations per provider with a `RefreshLock` (added in
`secrets.rs`) keyed by a provider-specific lock name -- `REFRESH_LOCK_
ANTHROPIC` and `REFRESH_LOCK_OPENAI`. Both `refresh_anthropic` and
`refresh_openai` now acquire the matching lock before doing any work:

  let _flight = RefreshLock::acquire(state_dir, secrets::REFRESH_LOCK_ANTHROPIC)?;
  if !token_recently_rotated(&secrets::anthropic_token_path(state_dir)) {
      trigger_host_refresh("claude", &["-p", "hi", "--model", "sonnet"])?;
  }

The first waiter performs the rotation; the second, once it acquires the
lock, sees via `token_recently_rotated` that the credential file was just
rewritten and skips its own host CLI invocation entirely, reusing the
fresh token. The locks are intentionally per-provider so a concurrent
Anthropic refresh never serializes against an OpenAI one (and vice
versa) -- only true duplicates of the same provider's refresh collapse
into a single host launch.

This implements item A2 (refresh single-flight) from PLAN.md: collapse
concurrent credential refreshes for the same provider into one host-side
rotation, with a freshness check so late waiters reuse the just-rotated
token instead of spawning a redundant CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:12:23 +00:00
Evgeny Boger
0f301a1146 intercept-hook: end-to-end mid-session token rotation across an expiry boundary
Why
---
PLAN.md item A1 ("Real mid-session token rotation") was the last open item
of the old Phase 4. The substitution + OAuth-refresh-hook infrastructure
already existed (file-backed `SecretValue::File` + `_intercept-hook`), but no
run had ever crossed a real token-expiry boundary end-to-end: Claude tokens
expire after ~hours, Codex/ChatGPT after ~24h, so a normal CI run never
exercises the refresh path. Without that coverage we could not claim that an
externally-rotated host token is actually picked up on the next request
without a re-attach — the whole point of host-rooted secrets.

How
---
This extends `crates/agent-vm/src/intercept_hook.rs` to drive and assert the
rotation flow directly, so the expiry boundary is reproduced deterministically
instead of waiting hours for a live token to lapse:

- Synthesize a near-expired bearer in the file-backed secret, issue a request
  through the intercept hook, and assert the hook detects expiry and triggers
  the host-side refresh rather than forwarding the stale token.
- After the simulated host rotation rewrites the credential file, assert the
  next request observes the freshly rotated bearer with no relaunch and no
  re-attach, confirming the file-backed `SecretValue::File` re-read path.
- Cover both the Claude and Codex/ChatGPT shapes so the ~hours and ~24h expiry
  boundaries are both exercised by the same harness.

This closes A1 by making the previously-untested rotation path a repeatable
check, decoupled from real-world token lifetimes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 13:52:08 +00:00
Evgeny Boger
94ae08e7c1 PLAN: replace phase roadmap with remaining-work plan vs main
The old Phase 0-9 roadmap is essentially all shipped, so it no longer
answers the only live question: what's left for the microsandbox rewrite
to fully replace — and then beat — the original Bash agent-vm on main.

Rewrote PLAN.md around that question. Per-phase history is dropped (it
lives in git log + ARCHITECTURE.md); the new structure is:

- "Where the rewrite stands today" — the working/verified surface,
  including the per-launch egress controls that already exceed main.
- A. In-scope work to finish: A1 real mid-session token rotation
  (built, never exercised e2e), A2 refresh single-flight flock
  (confirmed absent in intercept_hook.rs), A3 project-integrity
  snapshot (rewrite fingerprints only the 3 cred files; main also
  covers .git/hooks, .git/config, CLAUDE.md, Makefile), A4 git
  push --dry-run push-access probe (confirmed absent).
- B. Distribution: CI smoke, cross-arch binaries, IPv6 DNS upstream fix.
- C. Beyond main: detached/persistent-VM fast launch.
- D. Per-feature decisions made with the user: port back copilot
  agent+token and LSP plugins in the image; won't-do GitHub App
  per-repo token minting, USB passthrough, dynamic-memory balloon.

Verified candidate gaps against the actual rewrite source rather than
the docs: dropped onboarding-config and .agent-vm.runtime.sh-hook from
the list because secrets.rs / run.rs:891-962 already implement them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 20:31:06 +00:00
Evgeny Boger
13850a81aa Merge branch 'improve-help-output' into rewrite-microsandbox
Regroup and tighten the agent-vm CLI usage output: terse short help,
help_heading groups, wrap_help, value names, examples/networking/env
footers, and a working -V/--version.
2026-05-30 19:08:55 +00:00
Evgeny Boger
f881e073c1 help: regroup and tighten the CLI usage output
The launch verbs (claude/codex/opencode/shell) share one `run::Args`,
and every flag carried its full multi-sentence explanation as the
*short* help. clap renders the first doc paragraph as `-h` text, so
`-h` was a wall: 13 flags, each 3-8 lines of run-on prose. Worse, the
crate built clap without `wrap_help`, so `--help` printed each long
paragraph as one unwrapped line — the terminal soft-wrapped it, but
copy/paste and narrow widths showed a single giant line per flag.

Split short vs long help, and group the flags:

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 19:08:11 +00:00
Evgeny Boger
63cab08d82 README: document --publish / --auto-publish / --allow-egress / --allow-lan / --allow-host
New "Ports & egress" section between Project hook and Troubleshooting.
Lays out the default-deny baseline (`public_only` policy: only DNS +
Public group), then a one-row-per-flag table covering all five
network surface flags merged this iteration:

- --publish (host → guest port forward)
- --auto-publish (Lima-style guest → host mirror)
- --allow-egress IP|CIDR (per-IP egress)
- --allow-lan (whole Private RFC1918 + CGNAT + ULA)
- --allow-host (gateway IP → host 127.0.0.1, dial via
  host.microsandbox.internal)

Calls out that loopback/link-local/metadata stay denied even
under --allow-lan (disjoint groups by design) and that --allow-host
is the narrowest way to reach a host dev server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 17:54:38 +00:00
Evgeny Boger
65c6d59a40 run: seed the pulled-digest marker on launch so the update banner can fire
Follow-up to the ghcr.io banner auth fix (v0.1.17). The banner compares the
registry against the digest recorded by our last pull (`pulled_marker`), but
that marker was written *only* by `agent-vm pull`. So anyone who got their
image via a launch's `IfMissing` auto-pull — or via an older agent-vm that
never wrote the marker (the pre-v0.1.17 401 bug) — had no baseline, landed in
`UpdateState::NotCached`, and the banner stayed silent forever until they ran
`agent-vm pull` by hand. Which is the exact "I always had to pull manually"
complaint the banner was meant to retire.

Seed the marker on the launch path, just before the probe, from what
microsandbox actually has cached (`Image::get(ref).manifest_digest()`), but
only when no marker exists yet — an existing marker is the authoritative
record of our last pull. A stale cache then trips the banner on *this* launch,
not the next.

Why `Image::get` is safe here but not in pull.rs: pull.rs uses
PullPolicy::Always, where microsandbox's cached manifest digest can lag a
re-pull under the same tag (the reason pulled_marker.rs exists). The launch
path uses IfMissing and never re-pulls, so the cached digest is accurate.
Verified live that `Image::get(...).manifest_digest()` returns the exact
per-platform digest `image_check::fetch_remote_digest` produces, so the
marker-vs-registry comparison is apples-to-apples:

    seeded pulled-digest baseline from cache  digest=sha256:90cfcbdc...
    registry update probe                     digest=sha256:90cfcbdc...   (UpToDate, no banner — cache current)

Fresh installs: image not cached → no seed → no banner on first run (correct,
the imminent pull lands the current image); armed from the next launch on.

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

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

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

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

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

Cargo.lock refreshed by the post-bump build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:24:29 +00:00
Evgeny Boger
92c9da84cf Merge branch 'investigate-image-check' into rewrite-microsandbox 2026-05-30 13:23:21 +00:00
Evgeny Boger
2a45955818 Merge remote-tracking branch 'origin/rewrite-microsandbox' into rewrite-microsandbox 2026-05-30 13:23:00 +00:00
Evgeny Boger
2d4d5978bf image_check: do the ghcr.io Bearer-token handshake so the update banner fires
The launch-time "newer image available" probe did a plain unauthenticated
GET against the registry manifest endpoint. ghcr.io (and Docker Hub) answer
that with 401 + `WWW-Authenticate: Bearer realm=...` even for public images,
so `remote_manifest_digest` hit `!status.is_success()` and returned `Ok(None)`
— the banner never fired for the default `ghcr.io/...:latest` image, and the
same `fetch_remote_digest` in `agent-vm pull` silently skipped writing the
pulled-digest marker. Net effect: the update check never worked; users always
had to `pull` blind.

Add the standard registry token handshake: on 401, parse the Bearer challenge,
fetch an anonymous token from `realm?service=&scope=`, and retry the manifest
GET with `Authorization: Bearer`. Works for ghcr.io and Docker Hub; offline /
private / malformed-challenge cases still fall through to a quiet `Ok(None)`.

Also:
- Cap the launch-path probe with an overall 5s budget (tokio time feature):
  the handshake is up to three sequential round-trips, each with its own 5s
  per-request timeout, awaited inline before boot — a slow registry must not
  stall launch beyond a single request's worth of wait.
- Drop an empty token instead of sending `Authorization: Bearer ` + re-401.
- Surface the probe outcome via `tracing::debug!` (RUST_LOG=agent_vm=debug).
- Tests for the WWW-Authenticate challenge parser.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:30:20 +00:00
Evgeny Boger
5a731501d5 run: --publish accepts [::1]:p:p, rejects /udp + bump vendor/microsandbox
Two parser fixes plus the vendor bump:

  IPv6 host bind via docker-style brackets: `[::1]:8080:80`
  and `[::]:5000:5000` now work. Previously the parser split
  the whole body on `:` and any IPv6 form exploded into too
  many parts and got rejected with the generic syntax error,
  despite the underlying PortPublisher accepting IpAddr::V6.

  UDP is now REJECTED with a clear error message. Previously
  `--publish 53:53/udp` parsed, the banner said "Publishing
  …/udp", and PortPublisher::spawn_listener_one silently
  returned None for non-TCP — user got a published port that
  in fact had no listener.

Vendor bump (29ab515) carries the round-2 fixes:
  - IPv6 loopback end-to-end (LoopbackForwardReq gains
    loopback_target; runtime sends right bind/target pair)
  - Supervisor owns `active` across reconnects (no listener
    leaks)
  - Stream errors propagate to supervisor for reconnect
  - Exponential backoff on supervisor restart
  - IdCounter clamps away from PORT_EVENT_BROADCAST_ID
  - ForwarderRegistry::spawn awaits prior task before rebind

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 14:42:54 +00:00
Evgeny Boger
374921cb99
Merge pull request #9 from wirenboard/image-cron-cachebust-rms
ci(build-image): bust cache for agent installers each run
2026-05-29 17:12:42 +03:00
Evgeny Boger
45c208ce31 run: clarify --auto-publish help text + bump vendor/microsandbox
The previous help text claimed "Loopback-only guest binds are NOT
forwarded" — but the agentd-side forwarder added in 18d2c75 does
in fact forward them. Operators reading --help reasonably
expected 127.0.0.1-only guest services to stay private; in
practice agent-vm exposes them on host 127.0.0.1 (every other
process on the host's loopback can reach them).

Updated text describes what actually happens and adds an explicit
security note pointing users at the per-port --publish flag when
they DO want loopback to stay private inside the guest.

Vendor bump picks up the upstream fixes (b7cb641):
  - LoopbackForward family-aware bridge (IPv6 loopback works)
  - Mode-flip reconcile (dev-server bind-address changes)
  - Accept-loop exponential backoff (no EMFILE busy-spin)
  - IdCounter clamp (no slot-boundary id wrap)
  - UDS supervisor (auto-publish recovers from transient errors)
  - spawn_listener_one binds before registering the AbortHandle
  - FLAG_SESSION_START removed from LoopbackForward* RPCs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:41:28 +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
6d52474eac Bump vendor/microsandbox: auto-publish unit tests
No behaviour change — picks up the new tests for the loopback
forwarder registry and the listener-collapse precedence rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:19:27 +00:00
Evgeny Boger
18d2c75330 Bump vendor/microsandbox: agentd loopback forwarder
Picks up the agentd-side TCP forwarder that makes 127.0.0.1-only
guest LISTEN sockets reachable from the host under --auto-publish.
No agent-vm-side code change needed — the auto-publish flag now
just works for loopback binds as well as wildcard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:10:29 +00:00
Evgeny Boger
fd696a13ea run: add --auto-publish flag (vendored msb auto-publish)
Bumps vendor/microsandbox to pick up the runtime auto-publish
feature and exposes it as a CLI flag. When set:

  - .network() builder enables auto_publish() so the runtime's
    in-msb poll task spawns once the agent socket is ready.
  - The host process subscribes to sandbox.port_events() and
    prints each Added/Removed mapping to stderr as `==> auto-
    publish: guest :PORT → host 127.0.0.1:PORT`.

All forwarding rides the native smoltcp PortPublisher; no
per-connection python3 tunnel like the previous exec-tunnel
prototype. Loopback-only guest binds are not auto-forwarded
(documented on the flag) because the smoltcp dial target is the
guest's VLAN IP, not loopback — use --publish or run the service
on 0.0.0.0 inside the guest.

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 08:26:28 +00:00
Evgeny Boger
ca516fb807 Revert exec-tunnel auto-publish — moving into msb (vendor/microsandbox)
The exec-tunnel implementation lived in agent-vm because it could be
done without upstream changes. Now superseded by a microsandbox-side
native implementation: dynamic PortPublisher add/remove + in-msb
poll loop reading /proc/net/tcp via the in-process agentd client,
emitting PortEvent over the existing agent.sock relay. Each
forwarded byte stays on the native smoltcp path (no python3 fork,
no extra hops), and the feature becomes a generic microsandbox
primitive any consumer can use via NetworkBuilder.auto_publish(...).

The agent-vm replacement (a thin event-subscription that prints
each guest port → host port mapping) lands in a follow-up commit
after the vendor/microsandbox bump.

Reverts:
- 800b51c auto-publish: forward 127.0.0.1 guest binds too, not only 0.0.0.0
- 913d9e0 run: add --auto-publish for Lima-style host ← guest port mirroring

--publish (static smoltcp publish, commit 4407bc2) is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:14:54 +00:00
Evgeny Boger
800b51c411 auto-publish: forward 127.0.0.1 guest binds too, not only 0.0.0.0
Lima itself defaults to loopback-only (skipping 0.0.0.0) because on
its host-network model 0.0.0.0 in the guest is reachable from the
LAN and silently mirroring that on host loopback would change the
security posture. Our setup is the other way around — smoltcp is
in-process inside agent-vm, so 0.0.0.0 in the guest is reachable
only from the agent-vm process anyway. Forwarding both is the same
trust boundary and saves the user from having to remember which
address their dev server happened to pick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:46:05 +00:00
Evgeny Boger
913d9e0066 run: add --auto-publish for Lima-style host ← guest port mirroring
Polls /proc/net/tcp{,6} inside the guest every ~2s over the existing
agentd FsRead channel and diff-drives a per-guest-port host
listener. Each accepted host connection is bridged through a
per-connection in-guest python3 tunneller (exec over agentd) — no
upstream microsandbox changes needed, since the smoltcp PortPublisher
takes ports statically at boot and lives in the msb child process
behind a JSON-config boundary.

Mirrors Lima's default policy: only 0.0.0.0 / [::] binds are
auto-forwarded (loopback-only services stay private). When the
preferred host port is already taken, falls back to ephemeral.
Trade-off vs --publish: each inbound connection forks a python3
process — fine for dev tunnels, not for high-throughput.

Demo:
  agent-vm shell --auto-publish
  # in guest: python3 -m http.server 8080 --bind 0.0.0.0
  # on host: curl http://127.0.0.1:8080/  → reachable within ~2s

New modules:
  proc_net_tcp.rs   — /proc/net/tcp{,6} LISTEN parser (5 tests)
  exec_tunnel.rs    — per-connection python3 bridge
  auto_publish.rs   — discovery + diff + spawn/abort loop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:15:38 +00:00
Evgeny Boger
4407bc244d run: add --publish HOST:GUEST[/proto] for host → VM port forwarding
The smoltcp networking engine is in-process and never exposes the
guest IP on a host interface, so a `0.0.0.0` listener inside the VM
is unreachable from the host out of the box. microsandbox already
ships a published-port primitive (host listener that forwards
inbound connections into the guest via smoltcp); this just plumbs
it through the agent-vm CLI in docker-compatible syntax:

  agent-vm shell -p 18080:8080         # 127.0.0.1:18080 → guest :8080
  agent-vm shell -p 0.0.0.0:5000:5000  # all host ifaces
  agent-vm shell -p 53:53/udp          # UDP

Guest must listen on 0.0.0.0 / the assigned MSB_NET_IPV4 — a bare
127.0.0.1 bind inside the guest isn't reachable since the smoltcp
dial target is the guest's VLAN address, not loopback. Doc'd on
the flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:50:43 +00:00