Skip to content

Commit 3c21c07

Browse files
committed
feat(agentenv): support preserved runner sandboxes
1 parent 41414b4 commit 3c21c07

57 files changed

Lines changed: 6903 additions & 255 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitattributes‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
.runner-watch/golden/** filter=lfs diff=lfs merge=lfs -text
2+
3+
# Internal benchmark material: host-specific infrastructure detail and raw
4+
# samples. Useful in-repo, never shipped in a release archive.
5+
benchmarks/substrates/REPORT.md export-ignore
6+
benchmarks/substrates/results-*/** export-ignore

‎AGENTS.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,12 @@ just dogfood # E2E with real runner
3535
- **State**: in-memory behind `Arc<Mutex<…>>` + `Notify`/broadcast. Secrets use `SecretString` — call `expose()` only at protocol boundaries.
3636
- **Wire compatibility**: `/_apis/…` is the source of truth. Validate protocol changes against the **official runner**, not only unit tests.
3737
- **Broker path only**: all work targets the modern broker + Twirp results-service protocol (v2.329.0+).
38-
- **ARM64 local target**: smolvm on Apple Silicon.
38+
- **VM substrates**: two backends behind `preloop_vm::VmProvider` — SmolVM
39+
(libkrun; the only option on macOS/Apple Silicon) and AgentENV (Firecracker
40+
over `/dev/kvm`; the default on a Linux host that has `aenv`). Selected by
41+
`PRELOOP_VM_BACKEND`; capability differences are declared by
42+
`VmProvider::capabilities()` and the orchestrator branches on them. See
43+
`docs/vm-substrates.md` before touching pool or provider code.
3944
- **Store backends**: the `Store` trait (`store.rs`, async, object-safe) is the only surface the server sees; backends are SQLite (`store.rs`, default, `<state_dir>/preloop.db`) and Postgres (`store_pg.rs`), selected via `PRELOOP_STORE_URL` (`sqlite://<path>` / bare path / `postgres://…`). Both are single-writer: one connection behind a mutex. Two servers on the same SQLite file (or same PG database) still diverge in-memory — the DB is a restart source, not a shared bus.
4045
- **Store is best-effort**: in-memory state is the source of truth, the database is a restart source. Store failures are logged; the affected event is still broadcast (see `state.rs::emit`). Per-backend `MIGRATIONS` is the schema source of truth (SQLite: `PRAGMA user_version`; PG: `schema_migrations` version table).
4146
- **Encryption-at-rest is obfuscation, not security**: `<state_dir>/hmac-key.bin` and `preloop.db` sit in the same directory; the store key is HKDF-derived from the JWT HMAC key with domain separation. It stops a stolen DB file, not a compromised state dir. Key loss = unbootable state. The envelope is backend-independent (sealed blobs), so it applies to Postgres rows too — for remote PG, rely on TLS + DB auth instead.

‎CHANGELOG.md‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,59 @@ Releases before v0.27.0 predate the changelog.
99

1010
## [Unreleased]
1111

12+
### Added
13+
14+
- **AgentENV is now a first-class VM substrate, and the default on KVM hosts.**
15+
`crates/preloop-vm/src/agentenv.rs` implements `VmProvider` over the `aenv`
16+
CLI (Firecracker + overlaybd + ublk), so any host with Linux ≥ 6.8,
17+
`/dev/kvm`, and `aenv` on `PATH` selects it automatically;
18+
`PRELOOP_VM_BACKEND=smolvm|agentenv` overrides, and an unrecognized value is
19+
a hard error rather than a silent fallback. macOS is unchanged (SmolVM is the
20+
only libkrun-capable runtime there). AgentENV is faster at every VM lifecycle
21+
operation the pool performs (boot, fork, pause, resume); guest CPU is
22+
identical and guest I/O trades in both directions. Rationale, mapping, and
23+
operational caveats in `docs/vm-substrates.md`; harnesses in
24+
`benchmarks/substrates/`.
25+
- `VmProvider::capabilities()` reports what a backend can express
26+
(`live_host_volumes`, `socket_mounts`, `file_packs`), defaulting to SmolVM's
27+
behaviour so existing providers need no change. The orchestrator branches on
28+
it instead of assuming: a backend whose packs are not host files no longer
29+
builds, downloads, or relocates an artifact — it prepares its golden directly
30+
from the base image and forks per job.
31+
32+
### Changed
33+
34+
- `preloop shell` and `preloop debug` reach guests through the resolved backend
35+
(`guest_exec_command` / `guest_upload_command` / `guest_shell_command`)
36+
instead of spawning `smolvm` directly. On AgentENV they resolve the
37+
server-assigned sandbox id from the engine's registry
38+
(`$PRELOOP_HOME/agentenv-machines.json`) and report a clear error when it is
39+
missing, rather than failing obscurely.
40+
- On the AgentENV backend `preloop serve` forces the TCP control transport
41+
(`control_socket = None`): AgentENV cannot forward a host Unix socket into a
42+
guest. `PRELOOP_RUNNER_URL` must therefore be guest-reachable.
43+
- **Debugging is explicit on AgentENV, and unattached debug VMs suspend.**
44+
`preloop run --debug` is the opt-in for a live debug session (wire field
45+
`preloopDebugOnFailure`); with no flags an AgentENV failed job is cleaned up
46+
instead of parking a paid sandbox — SmolVM keeps its terminal-attached
47+
default for compatibility, and `--preserve-on-failure` remains the
48+
shell-only hold. The orchestrator suspends an unattached AgentENV debug
49+
sandbox after 15 s (`aenv pause`, measured 0.19 s) and resumes it when
50+
`preloop debug`/`preloop shell` attaches (measured 0.09 s), gated on the new
51+
`VmProvider::capabilities().preserves_runtime_state_on_suspend` so backends
52+
whose stop is a real shutdown are never parked mid-session. `preloop shell`
53+
exit now demotes its marker instead of deleting it, holding the VM for the
54+
remaining 10-minute idle window; the engine renews the AgentENV TTL
55+
keepalive on attach and re-suspends on detach.
56+
Verified live on a KVM host: real Firecracker debug sessions suspend after
57+
15 s unattached and resume for attach/verdict (same-VM retry passed
58+
end-to-end). Hardening that fell out of the live run: the guest
59+
provisioning wrapper now survives base images without `sudo` (missing
60+
`/etc/sudoers.d`, `useradd` off the exec PATH — the bare `ubuntu:24.04`
61+
sandbox broke configure), and `preloop debug`/`preloop shell` poll for
62+
envd readiness after `aenv resume` instead of racing its proxy ("410
63+
Gone: sandbox is not proxyable").
64+
1265
### Fixed
1366

1467
- Runner teardown now releases stale job bindings immediately, status snapshots
@@ -24,6 +77,30 @@ Releases before v0.27.0 predate the changelog.
2477
left `in_progress` with nothing executing it raises a
2578
`run_in_progress_without_execution` condition instead of vanishing from the
2679
operator's view.
80+
- `preloop run` declared its change set as known even when path derivation had
81+
not run, so an empty list read as "nothing changed" and every `paths:` filter
82+
rejected the run with a 400. The flag now mirrors whether derivation actually
83+
produced a list.
84+
- Every path that attaches a controller — the REPL, the inline failure prompt,
85+
`preloop debug --verdict`, and `preloop shell` — now holds the same attach
86+
marker for as long as it may act. Previously a one-shot verdict claimed no
87+
marker at all, so the orchestrator could suspend the VM under the controller,
88+
and the marker was demoted before the watcher observed it. Marker release
89+
after a verdict now waits for the worker's session transition, which is what
90+
lets the watcher resume the VM and re-acquire the pool concurrency permit;
91+
without it a resumed job's slot stayed released.
92+
- `AgentEnvProvider::exec_with_secret_env` returned success for a guest command
93+
that exited non-zero, so a failing secret-bearing step reported as passed.
94+
The exit status now propagates while the secret payload is still removed.
95+
- The substrate benchmark harnesses purged sandboxes they did not own: both
96+
`benchmarks/substrates/e2e-bench.sh` and `benchmarks/substrates/project-bench.sh` swept every AgentENV sandbox on the
97+
host instead of the ids in their own `$PRELOOP_HOME` registry. They now read
98+
their registry and tolerate ids that are already gone.
99+
- `benchmarks/substrates/aenv-egress-allow-host.sh` is idempotent and sets its
100+
firewall exception up in an order that survives an `aenv` restart: a second
101+
run no longer appends a duplicate deny-list complement, and the engine-port
102+
`ACCEPT` rules are re-inserted at the head of `INPUT` after the service
103+
re-adds its blanket veth `REJECT`.
27104

28105
## [0.32.7] - 2026-09-10
29106

0 commit comments

Comments
 (0)