Skip to content

What agent harnesses require of this platform

This document maps the sandbox contracts of three agent harnesses onto this platform’s current surface, and ranks the gaps. The harnesses are Harbor (agent evaluation), Omnigent (server-managed agent sessions), and the Vercel Sandbox / eve shape (agent-tool sandboxes). None of them becomes a dependency: their contracts were read from their source and public docs, and what this platform ships stays generic: capabilities any workload can use, never integrations that track a harness’s release cadence.

Sources: Harbor read at harbor-framework/harbor (local checkout, feat/lambda-microvm-environment); Omnigent read at PR #2217’s branch; Vercel Sandbox from vercel.com/docs/sandbox (retrieved 2026-08-14, SDK v2.9.2) and eve.dev/docs/sandbox; this repo read on main at d80150a.

Harbor drives a sandbox through one abstract class: exec (shell command, cwd, per-exec env dict, run-as-user with HOME/groups fixup, timeout with process-group kill and code 124, full stdout/stderr capture), file transfer (single files with mode, directory trees with tar fidelity: modes, symlinks, empty dirs), lifecycle (build-from-Dockerfile with content-addressed reuse, readiness signal, terminate/suspend), and per-instance credential bootstrap that never bakes a secret into a shared image. Commands must be startable idempotently under retry and outlive any auth-token ceiling, which is why Harbor’s own MicroVM provider hand-rolled a start/poll/ack daemon. Optional tiers Harbor degrades around: multi-container, GPUs, Windows, dynamic network policy, live output streaming.

Omnigent needs lifecycle more than exec. Its host dials out to the server over a WebSocket tunnel, so the sandbox needs no inbound API at all; what it needs is: create (or reserve-an-id-then-create), idempotent terminate, suspend-to-snapshot that preserves the whole process tree, idempotent resume under the same identity, cheap liveness, a per-launch env/secret channel delivered before the workload starts, and a published lifetime cap so launch-token TTLs can be derived above it. Its provider plus a deploy shim hand-rolled a lifecycle-hooks HTTP server, a 16 KB runHookPayload env channel, idle-policy derivation math, and NotFound/Conflict-to-success mappings, all of which become deletable if the platform supplies those natively. An exec daemon also lets Omnigent use its simpler exec-model launcher, where repo clone and config injection are shared framework code instead of in-image shell scripts.

Vercel Sandbox / eve is an API shape rather than a consumer: named, persistent-by-default sandboxes (stop auto-snapshots; the next call resumes), blocking and detached exec with a durable command id, replayable buffered output, live log streaming, kill with signal, sudo, cwd/env, batch file writes as gzipped tarballs, a node:fs/promises-shaped metadata surface, declared ports mapping to public URLs, SNI-domain and CIDR egress policy applied to the live session, and snapshot/fork. Agent frameworks layered on it need far less: eve’s backend adapter is about ten session methods (run, spawn with byte streams and kill, read/write file, remove, resolve path, set network policy, stop), and the AI SDK’s canonical sandbox tool needs only create, blocking exec with captured output, and teardown.

The daemon and client cover more of these contracts than any of the three providers’ hand-rolled daemons did, with proofs behind each behavior:

  • Idempotent detached exec: caller-minted exec id, retry-safe start, read-only poll, explicit ack, TTL only after ack, so unread output is never destroyed (agentd/src/exec.rs). This is precisely the start/poll/ack model Harbor’s provider built, plus SSE streaming with byte-cursor resume and explicit gap events, which Harbor’s daemon lacks.
  • Per-exec env, cwd, user/group, timeout_sec, stdin, and shell vs argv mode are all in the wire protocol and applied by the daemon (protocol/src/exec.rs:116, agentd/src/exec.rs:999-1019), and exposed by the Rust, Python, and Node clients.
  • Kill with SIGTERM-grace-SIGKILL to the process group; per-command timeout enforced daemon-side when requested.
  • File transfer: streamed single-file read/write with mode-at-open, tar upload/download with a confined extraction path (lexical resolution, symlink and bomb defenses, member/size caps), disk-pressure refusal with the real numbers (agentd/src/fs.rs).
  • Per-VM secret bootstrap through runHookPayload with one-shot semantics and traffic ordering guaranteed by the platform; the token never enters a child’s environment.
  • Suspend/resume that preserves memory, filesystem, token, running processes, and exec records (measured, docs/PLATFORM.md); local refusal of illegal transitions with zero billable calls.
  • Teardown that never raises, reports leaked identifiers, and a local ledger (microvm ls) that records leaks before attempting deletes.
  • Image builds from a Dockerfile with local pre-flight of the two platform traps (FROM/base agreement, WORKDIR requirement) and clientToken replay protection.
  • A machine-readable manifest, one JSON envelope per invocation, stable error codes, and append-only exit codes: the agent-friendly CLI surface none of the three harnesses’ providers had to build against before.

Ranked by how many harnesses need it, times how much hand-rolled code it deletes, over the cost of building it here.

1. Expose per-exec env (and user) through the CLI. The daemon applies env per request and the bindings expose it; the CLI hardcodes env: HashMap::new() (microvms-cli/src/commands/lifecycle.rs:677-707). Every harness passes env per exec (Harbor merges three layers of it on every call), and the PATH failure the coding-agents example documents is this gap biting a real workload. exec --env KEY=VALUE (repeatable) plus --user/--group makes the CLI equal to the bindings. Smallest change, highest reach.

2. Ship the platform daemon as the reusable answer to “no exec API”. Harbor and Omnigent each carry a several-hundred-line stdlib Python daemon baked into task images. agentd already does everything those daemons do, better tested. What is missing is packaging: a documented recipe (and a Dockerfile stanza helper) for appending agentd to an arbitrary task image, so a harness provider is a thin client over the published wire protocol instead of a daemon author. The coding-agents example is the seed; this is its generalization.

3. Image name resolution and content-addressed reuse in the CLI. run --image passes the identifier verbatim to the service, which rejects bare names (“Malformed ARN”); both Harbor’s provider and our example resolve ARNs by listing, and both key image names to content hashes to avoid the stale-snapshot-on-name-reuse hazard. Resolve names client-side and offer build --reuse keyed on artifact content hash.

4. A per-launch environment channel. Shipped. Omnigent’s whole hooks-server shim existed because the platform offers no per-launch env vars; the runHookPayload is the only per-VM secret channel and it carried exactly one token. The run hook now accepts an optional env map in the same payload and the daemon applies it as the base environment of every later exec, with the per-request env winning on a shared key. RunRequest::with_launch_env, run --launch-env KEY=VALUE, and both bindings expose it. Two things the design pinned rather than left open: the token never becomes part of that base environment, proven by a test that asserts a child’s whole environment equals the launch map; and only the first successful bootstrap sets it, so a caller who cannot win the token cannot rewrite the environment either. The payload budget is 4096 bytes and not 4 KB of headroom — it is shared with the token, and microvms-core refuses an over-budget payload locally, naming the env’s share of it, since AWS’s own answer arrives after the call and botocore does not check. Credential-scale material still belongs on the file path or a role.

5. Session-lifetime alignment for long execs. Shipped. Harbor’s daemon exists partly because commands must outlive the 60-minute proxy-token ceiling. Our detached exec already survived it by design — state lives in the daemon; a re-minted token reattaches — and the design is now the tested contract this item asked for. The live suite’s reattach after token rotation section (conformance/run_rs.py, drive_token_rotation) starts a detached exec, reattaches from nothing but the three identifiers a harness would have persisted — each attach minting a fresh proxy token, which is what a rotation is — and asserts under the check name no output produced before the reattach was lost that bytes buffered under one token are read whole under the next. What it deliberately does not do is wait the real hour: an expired token being refused is the platform’s property, not this contract, and the mechanism the survival rests on is fully exercised without it. The doc half is docs/EMBEDDING.md, “The proxy-token reality”, which names start, rotate, poll, ack as a normal sequence rather than a recovery path.

6. Idle-signal correctness for outbound-tunnel workloads. Shipped, with the naive half ruled out. The platform measures idleness only by inbound endpoint traffic. Omnigent’s host holds an outbound tunnel and receives none, so auto-suspend can freeze a VM mid-turn; multi-hour agent runs past 400 minutes are the case that hurts.

The parenthetical above — “a trivial periodic authenticated request” — is the thing that does not work, and the reason is measured rather than argued. The endpoint proxy terminates outside the VM and forwards over loopback (docs/PLATFORM.md), so a request an in-VM process sends to the daemon’s port is generated on the far side of the meter and never passes through it. A keepalive route inside the guest would answer 200 and change nothing, discovered as a suspend during exactly the long run it was added to protect.

So GET /v1/health now carries busy and execs, and the consumer is an orchestrator outside the VM whose own poll is the inbound traffic. That also keeps the assertion repeated and explicitly the caller’s, which is what rules out the daemon self-keepaliving: a hung process would otherwise bill silently to the 8-hour ceiling. busy is “producing”, not “unfinished” — an exited exec awaiting an ack reads false — and execs counts every registered entry so a caller can tell a drained VM from one holding output nobody read. The one thing this section left unmeasured — that a poll from outside does in fact reset the timer — has since been measured twice over: by hand in docs/PLATFORM.md (“An outside poll of /v1/health does reset the idle timer”), and on every live run by conformance/run_rs.py’s drive_idle_keepalive, which runs a VM to the edge of a 60-second idle window while polling and watches it survive under the check name a VM polled from outside outlives its idle window — then stops polling and watches the same VM suspend, which is the control that proves the survival was the polling.

7. An eve backend adapter (separate package, later). Ten session methods over the Node binding makes this platform a pinnable eve backend: real VMs where the consolidator today accepts a pure-JS bash interpreter. Worth doing as its own repo once 1-3 land; it depends on eve’s types, so it can never live here.

  • Vercel wire compatibility. The valuable seam is eve’s adapter, not Vercel’s REST surface. Snapshot/fork-on-stop, the heart of Vercel’s persistence model, needs snapshot-to-image, which is the standing AWS platform ask (docs/STRATEGY.md), not something this client can build.
  • Multi-container. ResourcesList.max = 1 is a platform constant. Harbor treats single-container as an accepted tier; Omnigent needs one container; nothing here changes.
  • Harness provider classes. The Harbor BaseEnvironment subclass, the Omnigent launcher, and the eve backend all import their harness’s packages, so they live in those ecosystems (or standalone adapter repos), never here. This repo’s deliverable is the daemon, the clients, and the published behavior they can rely on. docs/AGENT-VMS.md now carries two agent profiles (Claude Code, Codex) as the L3 layer such a class would call: one function that builds, launches, and provisions the VM, one that hands the agent a task. The non-goal still holds for the class itself, because it imports the harness’s packages and this repo does not.
  • GPUs, Windows, dynamic network policy. Not offered by the platform; harnesses that need them reject the environment up front, which is the correct degradation.

Items 1 and 3 are CLI work measured in hours and unblock every harness equally. Item 2 is documentation plus a small helper. Item 7 waits for the first three.

Items 4 and 6 have shipped, and the three design decisions this section predicted they would need were the right three. Payload budget: the token and the env share 4096 bytes, checked locally before the launch because neither AWS nor botocore gives a caller a signal in time. Env precedence: the launch env is the base and the per-request map is overlaid, which leaves the existing per-request contract unchanged for anyone who sends no launch env. Busy semantics: producing rather than unfinished, reported to an orchestrator outside the VM, because the guest-side keepalive this document floated cannot work against a proxy that terminates outside the guest. Item 5 has shipped too, and it cost no code: the reattach-after-token-rotation path always worked by design, and what this document asked for — a conformance check plus a doc section — is exactly what landed. drive_token_rotation and drive_idle_keepalive in conformance/run_rs.py are the checks; docs/EMBEDDING.md carries the operator-facing halves of both, the rotate-mid-run sequence and the outside-poll keepalive ownership.

Reading readTextFile from the AI SDK sandbox contract while item 4 was being built turned up one gap this document had missed. That method takes 1-based inclusive startLine/endLine and returns through EOF when endLine is past the end, and GET /v1/fs/file had no way to express it — a harness implementing it over this daemon would read whole files and slice them client-side, which on a multi-megabyte file is the transfer this route exists to avoid. The route now takes start_line and end_line with exactly those semantics, still streamed, with the un-ranged read byte-identical to what it was.