Commit graph

287 commits

Author SHA1 Message Date
Evgeny Boger
05e8969215 v0.1.25: route guest egress through host HTTP proxy (CONNECT)
Some checks failed
CI / build-and-test (push) Has been cancelled
Headline changes since 0.1.24:
- network: all guest egress (HTTPS/HTTP/image pulls) is re-originated through
  the configured host HTTP proxy via CONNECT, honouring HTTPS_PROXY/HTTP_PROXY/
  ALL_PROXY and NO_PROXY, while preserving the TLS-intercept egress policy.
- fails closed: a configured-but-unreachable proxy errors instead of silently
  dialing direct.
- security: reject control chars/whitespace in the CONNECT target host and in
  parsed SNI, closing a guest-controlled CRLF/header-injection vector.
- run: verbose startup banner reporting the proxy route (and NO_PROXY bypass).
- vendor/microsandbox bumped to wb merge of microsandbox#4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:32:16 +03:00
Evgeny Boger
920bff82d7
Merge pull request #19 from wirenboard/host-proxy-support-v2
Honour host HTTP proxy for guest egress (CONNECT) + verbose startup logging
2026-06-27 17:31:17 +02:00
Evgeny Boger
885aa026d1 vendor: point submodule at merged wb (microsandbox#4)
Update the gitlink from the PR branch head to the wb merge commit
d6b9b11 now that microsandbox#4 is merged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 18:26:25 +03:00
Evgeny Boger
958af6ddc7 proxy: honour host HTTP proxy for guest egress + log it at startup
Behind a corporate/host HTTP proxy, agent-vm could pull its image (the
host-side reqwest clients auto-detect HTTPS_PROXY/HTTP_PROXY) but the
agent *inside* the VM could not reach the internet: the microsandbox
network stack re-originated guest connections directly, bypassing the
proxy. This made agent-vm unusable in proxy-only networks.

Bump the vendor/microsandbox gitlink to the network-crate change that
tunnels guest egress through the host proxy via HTTP CONNECT (preserving
the TLS-intercept MITM and egress policy — see the submodule commit).

On the host side, also surface the detected proxy verbosely at launch:

    ==> Proxy: routing guest egress + image pulls through http://proxy:3128 (HTTP CONNECT)
    ==> Proxy: bypassing (no_proxy) localhost,127.0.0.1,.internal

so the operator can see at a glance that all traffic is routed through
their proxy. The banner reads the same HTTPS_PROXY/HTTP_PROXY/ALL_PROXY/
NO_PROXY the network stack consumes, via the re-exported
microsandbox_network::http_proxy::ProxyConfig::from_env().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:36:18 +03:00
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
281d82ddb3 v0.1.24: microsandbox 0.5.7 (wb) + 7-feature integration + agentd clean teardown
Headline changes since 0.1.23:
- vendor/microsandbox bumped to the canonical wb line (0.5.7), agentd source
  built from the fork (no upstream prebuilt); SDK prebuilt download dropped.
- agentd exits its init/workload on shutdown instead of reboot(RB_POWER_OFF) —
  teardown ~8.4s -> ~0.45s, data-safe.
- 7-feature integration: GitHub Copilot agent (D1), baked LSP plugins (D2,
  seeded past the persistence symlink), token rotation + single-flight (A1/A2),
  CI smoke (B1), aarch64 binary (B2), DNS family ordering (B3).
- image: reproducible :latest manifest, version-keyed agent layers, WB diag
  CLIs + codestyle-pinned Python linters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-24 01:25:06 +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
ea73a39add agent-vm: drop the stale --mount IRQ-pool advisory note
The note warned that extra `--mount`s could trip
`RegisterNetDevice(IrqsExhausted)` because libkrun's virtio IRQ pool was
tight. That ceiling was the 16-pin IOAPIC MP-table bug, now fixed upstream
(msb_krun 0.1.17 maps the full IOAPIC pin range — verified 22 mounts / 34
virtio devices boot cleanly). The warning now fires far below the real limit
and is just noise, so remove it along with the stale rationale comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-23 22:04:32 +03:00
Evgeny Boger
7d69877c35 agent-vm: repoint vendor/microsandbox to the wb-clean line
wb-clean is now the canonical microsandbox branch (wb is being retired). It
carries the same downstream features as wb but with clean per-feature history,
plus the agentd clean-teardown + fork-artifact-URL fixes (cherry-picked).
Pointer 1a7e803 (old wb) -> a5c830d. No Cargo.lock change (same crate versions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-23 20:44:43 +03:00
Evgeny Boger
0d9d86e19a agent-vm: drop the microsandbox SDK prebuilt feature
The SDK's default `prebuilt` feature made `cargo build -p agent-vm` download the
upstream msb+libkrunfw bundle from superradcompany. agent-vm ships its own
from-source msb, so disable it (default-features = false, keep net + keyring).
Builds now require a local build/agentd (`just build-agentd`), matching the
release workflow; Cargo.lock loses the download-only deps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-23 19:03:55 +03:00
Evgeny Boger
8be5bc0720 agent-vm: bump vendor/microsandbox — agentd clean teardown + fork artifact URLs
Pulls in fix/agentd-clean-vm-teardown:
- agentd exits its init/workload on shutdown instead of reboot(RB_POWER_OFF),
  which halts on the no-poweroff libkrunfw kernel — teardown ~8.4s → ~0.45s.
- release-artifact URLs resolve from wirenboard/microsandbox, not upstream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
2026-06-23 19:03:55 +03:00
Evgeny Boger
ef76b22733 D2: seed baked LSP plugins into persistent state on first boot
The image installs the LSP plugins under /root/.claude, but the launcher
symlinks /root/.claude -> /agent-vm-state/claude for persistence, which
shadows the baked tree — the booted guest's `claude plugin list` was empty.

Stash the plugins + settings to /opt/agent-vm/claude-seed (not shadowed) and
ship seed-claude-plugins.sh; the launch prelude runs it before exec'ing the
agent, copying the stash into the state dir once and merging enabledPlugins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:24:42 +03:00
Evgeny Boger
d2de958638 Merge branch 'integration-7feat' into integration-on-bump
# Conflicts:
#	vendor/microsandbox
2026-06-22 21:21:21 +03:00
Evgeny Boger
cd7d81fdec agent-vm: set SecretEntry.on_violation for msb 0.5.7 SDK
The 0.5.7 secrets config gained a per-entry on_violation override
(Option<ViolationAction>). The token-isolation test constructs a
SecretEntry literally, so it must set the new field. None = inherit
the collection-level default. Release build was unaffected (only test
code constructs SecretEntry); cargo test -p agent-vm now compiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:18:06 +03:00
Evgeny Boger
ea5821594e agent-vm: bump vendor/microsandbox to wb (rebased onto upstream)
Bumps the vendor/microsandbox submodule from the old wirenboard fork
(702f52a) to the wb branch — all downstream agent-vm features re-ported
onto current upstream microsandbox (sdk/rust @ upstream/main 69803ab3).
See wirenboard/microsandbox @ wb.

Two adaptations forced by upstream API/layout changes:
- Cargo.toml: the SDK crate moved crates/microsandbox -> sdk/rust.
- run.rs: Image::get now takes a LocalBackend handle (seed_pulled_marker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:58:07 +00:00
Evgeny Boger
a5c0d7ff79
Merge pull request #17 from wirenboard/image-reproducible-manifest
image: make :latest manifest deterministic (stop ~700 MiB hourly re-pull)
2026-06-19 17:56:13 +02: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
ac20c8e746
Merge pull request #15 from wirenboard/image-version-cachebust
image: key agent layers on upstream version (stop hourly ~200 MiB :latest churn)
2026-06-18 19:17:15 +02: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
5c7aea9460
Merge pull request #12 from wirenboard/image-tools-layers
image: WB tooling + diag CLIs, layer reorder for cron-cache wins
2026-06-18 11:06:41 +02:00
Evgeny Boger
d5209f73fe image: WB tooling + diag CLIs, layer reorder for cron-cache wins
- network/diag tools (ssh, ping, ifconfig, sshpass, mtr, dig, nc, tcpdump,
  net-tools, dnsutils, …) merged into the base apt layer (~+38 MiB)
- Python lint pinned to codestyle/python/config/requirements.txt:
  black 24.2.0, isort 5.13.2, pylint 3.3.9, pytest-cov 2.10.1, attrs
  23.1.0, j2cli (~5 MiB)
- wirenboard/codestyle cloned at build to /opt/wirenboard/codestyle
  (--depth=1, ref pinnable via `--build-arg CODESTYLE_REF=…` for
  reproducible builds; default `master`) so the per-project
  `bash ../codestyle/.../linux-devenv.sh` idiom works in-VM without
  re-cloning (<1 MiB)
- agent installers split into three separate RUNs (codex → opencode →
  claude, smallest to largest) so a single release only invalidates one
  layer (~10–70 MiB) instead of three
- agent installers HARD-FAIL by default (production CI must not silently
  ship an image missing an agent). `AGENT_INSTALL_SOFT_FAIL=1` build-arg
  + matching `AGENT_VM_BUILD_SOFT_FAIL_AGENTS` env-var on build.sh
  decouple soft-fail from the CA-detection path — auto-enabled on
  TLS-intercept dev hosts, overridable in either direction. The
  codestyle clone follows the same policy
- shared agent-vm-install helper (curl + run + soft-fail policy in one
  place) replaces three near-identical RUN blocks; the per-installer
  RUNs become one-line invocations
- sanity check exercises every binary directly (`tool --version
  >/dev/null`) instead of `command -v` + echo-substitution which masked
  broken-but-on-PATH binaries; pipe-into-head removed so failures
  propagate
- build-time host CA shim (Dockerfile + images/build.sh): when
  /usr/local/share/ca-certificates/microsandbox-ca.crt exists on the
  host, build.sh passes it as a buildx --secret (content NOT baked into
  the image) plus a CA_SHIM_CACHEBUST=mtime build-arg so the
  conditional Dockerfile shim invalidates correctly; CI builds without
  the host CA file are unchanged (the shim no-ops)

DEFERRED (commented out in Dockerfile for the initial roll-out;
uncomment + restore the corresponding sanity-check probes when WB
engineers actually need the in-VM cross-compile path):

- WB build essentials: build-essential, debhelper/devscripts/equivs/
  dpkg-cross, pkg-config, clang-format/tidy, libgtest/gmock/curl/
  modbus/systemd-dev, gcovr, gdb-multiarch, cmake/ninja-build,
  python3-{cups,libgpiod,pycurl}. ~208 MiB compressed.
- WB cross toolchains: crossbuild-essential-{armhf,arm64},
  qemu-user-static, debootstrap/schroot/sbuild. ~258 MiB compressed.

Smoke-tested in a real microVM (locally-built image, soft-fail path,
image-API contract v1 intact): in-build sanity check passes (`==
sanity OK ==`), all enabled tools present and functional. Final image:
593 MiB compressed, +~38 MiB vs baseline (the diag CLIs added to the
base apt layer; codestyle clone <1 MiB; python pins ~5 MiB; everything
else byte-identical to original).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 14:49:58 +00:00
Evgeny Boger
687ebcc6ed
Merge pull request #11 from wirenboard/rewrite-microsandbox
Microsandbox rewrite: libkrun-based microVM runtime
2026-06-10 19:34:56 +02:00
Evgeny Boger
1509b0fb49 Merge remote-tracking branch 'origin/main' into rewrite-microsandbox
# Conflicts:
#	.github/workflows/build-image.yml
#	.github/workflows/release-npm.yml
#	images/Dockerfile
2026-06-10 20:34:06 +03: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
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