Compass brain/hands split — separate the agent from on-demand compute
Design record. This designs the brain/hands split for Compass agents — separating the agent loop from on-demand compute on vendor-neutral primitives. Every
go/internal/*,agent-image/*, anddocs/*citation below is a path in theRigelBuild/compassmonorepo at HEAD13a43cec(line numbers drift as the code evolves; resolve them against that commit). It lives in the sealed design corpus (docs/designs/platform/) because that is where the wave’s design records freeze. Frozen on merge; executing agents read this as the contract for RIG-1717.
Status: Draft Tracking: RIG-1717
Problem / Intent
Section titled “Problem / Intent”Today’s architecture fuses brain and hands: one rootless-podman container per
agent, and the agent binary runs inside it. AgentRuntime.Launch
(go/internal/runtime/agent.go:177-198) creates + starts the container, arms
the default-deny egress firewall as root (armEgress,
go/internal/runtime/agent.go:300-309, running EgressPolicy.NftScript(),
go/internal/runtime/egress.go:87), installs scoped credentials, and creates
the checkout dir; the Runner then execs a bare compass-agent argv inside the
container (agentCommand, go/internal/runner/agent_exec.go:32;
AgentEnv.execSpec :77-96; StartAgent :136). That compass-agent is a
bun build --compile standalone bundle (agent-image/entrypoint.nix:5-29,
wrapper :227-229) — an I/O-bound LLM loop that is already portable in
principle, yet today it can only run welded to the container that also serves
as its compute environment.
The fusion means every agent pays for a full container for its whole lifetime,
even though ~90% of what an agent does — read, edit, reason, call the model —
needs no compute environment at all. It also welds two decisions together that
should be independent: where the agent loop runs and where agent-generated
code runs. RIG-1717 separates them: the brain (the agent loop — LLM
calls, reasoning, read/write over a shared virtual FS; lightweight, I/O-bound,
portable) from the hands (compute for running model-written code —
compile, test, dev server; heavy, native, security-sensitive), with hands spun
up on demand and torn down after. The pattern is adopted on
vendor-neutral primitives — never Cloudflare libraries; Workers is at most
one swappable brain-placement backend, not a foundation. Sequencing:
Beta-phase, explicitly after the Dogfood substrate
(docs/designs/platform/compass-dogfood-loop/design.md), landing when hosted
Compass makes infra cost first-order. Nothing in Dogfood blocks on this.
Approach
Section titled “Approach”The split
Section titled “The split”- Brain = the agent loop: LLM calls, reasoning, tool dispatch, reads and
writes against a shared virtual FS. A Bun process today (the same
OMP-derived loop that ships in
agent-image/entrypoint.nix), a Go goroutine later (SEA-1719), or optionally a Worker isolate at a managed edge. Because the brain is portable, “where the brain runs” and “where the hands run” are independent decisions — that independence is what defeats lock-in. - Hands = an on-demand compute environment for agent-generated code. Booted only when a task needs it, FUSE-mounting the same virtual FS the brain has been editing (edits already present — no copy in, no copy out), torn down after.
Tiered execution — reach for compute only when needed
Section titled “Tiered execution — reach for compute only when needed”Target: <10% of operations hit a microVM.
- Brain over virtual FS — write/edit/read/reason. No compute environment at all. ~90% of a PR’s operations.
- Light in-isolate exec — grep, small deterministic shell ops, run as
shell-to-JS over the virtual FS (the
just-bashtrick) inside the brain’s own process. No VM, no container. - Heavy/native exec in a microVM — compile, test, dev server. Only here does a Firecracker microVM boot (~125ms), FUSE-mounts the shared virtual FS, runs the command, and is reclaimed.
Vendor-neutral primitives (the self-hostable stack)
Section titled “Vendor-neutral primitives (the self-hostable stack)”- Shared virtual FS → Turso AgentFS (MIT,
github.com/tursodatabase/agentfs): a POSIX filesystem inside one SQLite
.db, FUSE/NFS-mountable, copy-on-write per-agent branching, SQL-queryable audit trail. Cloudflare’sdofsminus the Durable Object — runs on one Linux box, open format. Its SQL-auditable property directly serves SEA-1718’s “human-auditable, not a black box” constraint. - On-demand isolation → Firecracker (Apache-2.0): ~125ms boot, ~5MB overhead, thousands of microVMs per box, hardware/KVM isolation. With bubblewrap/namespaces as a lighter tier for trusted worker tasks; untrusted (model-written) code escalates to a microVM.
- Brain → our own OMP-derived loop (JS today; Go later, SEA-1719 — note Go→WASM runs single-threaded in a Worker isolate, so goroutine density is inert on Workers; Go pays off only as a real OS process on boxes we run, which is why the portable path stays JS).
How this maps onto the existing seam
Section titled “How this maps onto the existing seam”The package layering (go/internal/runtime/podman.go:10-20) already isolates
the engine behind the ContainerRuntime interface
(go/internal/runtime/podman.go:286-324) — “everything above depends on the
interface, so a libpod-REST backend can replace it without touching a caller.”
The split reuses that discipline rather than replacing it:
ContainerRuntimestays untouched. It remains the engine seam for the container-shaped things that survive the split (the gVisor/container fallback tier, and the fused path during migration).- A new sibling seam,
HandsRuntime, sits beside it (not a rewrite ofAgentRuntime): a backend-selecting exec API in the Cloudflare Computerexec(cmd, {backend})shape —Exec(ctx, HandsSpec) (ExecOutput, error)whereHandsSpeccarries the command, cwd, env, and the branch id. The isolationBackend(isolate|sandbox|microvm|container) is assigned Runner-side by policy, never authored by the brain — a model-driven caller may pass at most an upgrade-only hint policy can raise but never lower (Global Constraint 9); “the Runner routes each exec to a tier, callers never name Firecracker or podman.”Execis completion-shaped; a long-lived variant (streaming stdio + kill/wait handle, in theContainerRuntime.ExecStreamingshape,go/internal/runtime/podman.go:299-307) is reserved in H1 for SEA-1720’s agent-launched dev servers (OQ7). AgentRuntimenarrows from “the agent’s world” to “one brain-placement backend.” ItsLaunch(go/internal/runtime/agent.go:173-198) — container + in-container agent — becomes the fused/container placement, kept working throughout the migration. A new host-process placement runs the brain as a supervised bun process against the AgentFS mount, no container. TheAgentSpecfields (go/internal/runtime/agent.go:32-57) split along the same line:Persona/Role/AgentAccountID/Modelare brain concerns;Image/Mountsare hands concerns;Workspaceis replaced by a virtual-FS branch;Egresssplits in two (next bullet).- Egress splits. Today one
EgressPolicy(go/internal/runtime/egress.go:32-34) covers the whole container. Post- split the brain needs LLM/API endpoints and the hands need default-deny plus build-time hosts — different allowlists, sameNftScript()-style fail-closed mechanism (go/internal/runtime/egress.go:87) applied per placement (nft in the microVM / sandbox netns for hands; the brain’s policy enforced by its own placement). - The virtual FS is a new infra seam,
VirtualFS:Mount(ctx, branch BranchID) (mountpoint string, unmount func() error, err error),Branch(ctx, parent BranchID) (BranchID, error), andAudit(ctx, branch BranchID, since time.Time) ([]AuditEntry, error)over an AgentFS.db. The brain’s placement and every hands backend consume the same mount, which is what makes tier 3’s “edits already present, no copy” true — as a measured default, not an assumption: heavy execs over a FUSE→virtio-fs mount are a known worst case (many-small-filego test/bun install), so V1/H2 benchmark it and the design keeps a materialize-the-branch-into-guest-storage-then-write-back-the-diff escape hatch (OQ5) for the case where no-copy loses to the copy it saves. - The Runner’s substrate stays runtime-agnostic.
SpecBuilder(go/internal/runner/host.go:46-48) keeps assembling specs from operator defaults + the provision request; the dogfood loop moves under the split unchanged, per the sequencing constraint.
HandsRuntime is the one genuinely new abstraction, and it is justified by a
capability no existing seam carries: ContainerRuntime models a container’s
lifecycle (create/start/exec/stop against a ContainerID,
go/internal/runtime/podman.go:286-324), while hands need an exec routed to
a tier that may not be a container at all (in-process isolate, bubblewrap,
microVM). Forcing microVMs to impersonate ContainerIDs would corrupt the
existing seam’s meaning; a sibling interface keeps both honest.
Alternatives considered
Section titled “Alternatives considered”Cloudflare Computer / Workers as the foundation — rejected
Section titled “Cloudflare Computer / Workers as the foundation — rejected”@cloudflare/computer (github.com/cloudflare/computer) is the closest
blueprint — its dofs/computerd FUSE-sync and backend-selecting
exec(cmd, {backend}) are exactly the shapes this record adopts as a
pattern. But as a foundation it fails twice: it is Durable-Object-bound and
preview-only (a lock-in plus a stability risk), and Workers/V8 isolates cannot
be hands at all — no POSIX, no subprocess, no go test. Treating a Worker as
a compute environment is a category error, not a tradeoff. Cloudflare Computer
stays a reading reference only; Workers remains at most an optional,
swappable brain-placement backend.
Container-only hands (podman/gVisor, no microVM) — fallback, not default
Section titled “Container-only hands (podman/gVisor, no microVM) — fallback, not default”Keeping hands as containers would reuse ContainerRuntime
(go/internal/runtime/podman.go:286-324) wholesale. But hands run
model-written code: we want a hardware boundary, not a shared kernel.
gVisor-hardened containers narrow the syscall surface but still interpose a
userspace kernel on the host kernel. Kept as the fallback tier where KVM is
unavailable (nested-virt-less VMs, some CI); Firecracker is the default
where KVM exists.
e2b open infra (Nomad/Consul orchestration) — too heavy
Section titled “e2b open infra (Nomad/Consul orchestration) — too heavy”e2b (Apache-2.0, github.com/e2b-dev/E2B) is the reference for the Firecracker-orchestration glue — spawn/mount/run/teardown, warm pool — but its Nomad/Consul cluster machinery is sized for multi-tenant fleets. Single-box first: we take the glue patterns, not the cluster weight.
The microVM / container / Worker tradeoff (why Firecracker for hands)
Section titled “The microVM / container / Worker tradeoff (why Firecracker for hands)”| Firecracker microVM | Container (podman / gVisor) | Worker / V8 isolate | |
|---|---|---|---|
| Isolation boundary | Hardware/KVM — own kernel | Shared host kernel (gVisor: userspace kernel shim) | V8 sandbox — no OS at all |
Can run go test, compilers, dev servers | Yes — full Linux | Yes | No — no POSIX, no subprocess (category error) |
| Cold start | ~125ms | ~100ms–1s | ~5ms |
| Per-instance overhead | ~5MB | ~10–100MB | ~KBs |
| Density | Thousands/box | Hundreds/box | Tens of thousands |
| Trust level for model-written code | Strong (hardware boundary) | Weaker (kernel attack surface; gVisor mitigates) | N/A as hands |
| Lock-in | None (Apache-2.0, any Linux/KVM box) | None | Cloudflare-shaped |
A ~125ms boot is noise against a seconds-to-minutes compile/test run, and the hardware boundary is the right posture for untrusted code — so microVM wins for hands, containers are the KVM-less fallback, and isolates are relevant only as an optional brain placement.
Global Constraints
Section titled “Global Constraints”- Vendor neutrality (hard rule). No Cloudflare foundation dependency
anywhere in the stack — no
@cloudflare/computer, no Durable Objects, no Workers-required path. Cloudflare Computer is a reading reference only. Every primitive must be self-hostable on one Linux box: AgentFS (MIT), Firecracker (Apache-2.0), bubblewrap, our own brain loop. - Linux/KVM for the microVM tier. Firecracker requires KVM
(
/dev/kvm). Hosts without it (or without nested virt) get the gVisor-hardened-container fallback tier; theHandsRuntimebackend selector must degrade explicitly, never silently run untrusted code in a plain container. - Single-box first. All orchestration targets one Linux box (the dogfood/self-host shape). No Nomad, no Consul, no cluster scheduler. Multi-box is a later record.
- The substrate stays runtime-agnostic. The Runner’s provision flow —
the
SpecBuilderseam (go/internal/runner/host.go:46-48), the dispatch/session plumbing (go/internal/runner/agent_exec.go,dispatch.go,host.go) — keeps its interfaces; the split slots in as new implementations behind existing seams plus the three new ones (HandsRuntime,VirtualFS,BrainPlacement). The dogfood loop (docs/designs/platform/compass-dogfood-loop/design.md) must keep passing unchanged at every increment. - Sequencing: Beta-phase, after Dogfood. Nothing in the Dogfood milestone blocks on this record; no task here may become a Dogfood dependency. Work starts when hosted/managed Compass is being stood up.
- The fused path stays green during migration.
AgentRuntime.Launch(go/internal/runtime/agent.go:173-198) remains a working brain placement until the split path carries a full session end to end; increments are additive behind seams, never a big-bang cutover. - Security floors carry over. Default-deny egress (fail-closed,
go/internal/runtime/egress.go:71-107semantics) applies to every placement of both brain and hands; model-written code never executes outside a hardware boundary (microVM) or the explicitly-degraded gVisor fallback; secrets keep the 0600/stdin-not-argv discipline (go/internal/runtime/agent.go:238-246). - Version floors. Firecracker ≥ 1.7, AgentFS pinned to a vetted release
(see OQ2), Linux ≥ 5.10 with FUSE3, existing podman ≥ 4.3 floor unchanged
(
go/internal/runtime/podman.go:405-413). - Tier selection fails closed; the agent can never lower isolation. The
isolation tier for any hands exec is chosen by Runner policy, not by the
model-driven brain: a command not on the tier-2 in-isolate allowlist
escalates to tier 3 (an unknown/unclassified command is tier 3 by default),
and the
ExecHandsRPC carries no authoritativeBackendfrom the agent — at most an upgrade-only hint policy may raise. This is the invariant the whole hardware-boundary posture rests on: a classifier that fails open would put model-chosen argv inside the brain’s own process, which holds the LLM credentials, the scoped-$HOMEsecrets, and the brain’s (wider) egress — strictly weaker than today, where every agent exec runs in the default-deny container (go/internal/runtime/egress.go:71-107).
Lanes: virtual FS = infra, hands = runner/infra, brain =
compass-agent. Dependency order is the task numbering within each lane plus
the explicit Depends: lines; V1 → H1 is the critical path, brain tasks
parallelize once V1 and H1 freeze their interfaces.
M0 — baseline the tier mix from real sessions (lane: compass-agent, parallel pre-task)
Section titled “M0 — baseline the tier mix from real sessions (lane: compass-agent, parallel pre-task)”Before three backends get built, ground the ~90%-brain / <10%-microVM figures: classify the tool-calls in existing dogfood-loop session logs (the substrate already emits tool-call telemetry) into tier 1 / 2 / 3 and report the real distribution. Cheap, no dependency; informs whether the <10% target is an acceptance bar or needs adjustment before E1 measures it live.
- Interfaces: consumes existing dogfood session tool-call logs; produces a one-off tier-mix report committed in this record’s directory. No seam.
- Depends: nothing (runs in parallel with V1).
- Test cycle: the report itself; a classification pass rerunnable as sessions accrue.
V1 — AgentFS vetting spike + the VirtualFS seam (lane: infra)
Section titled “V1 — AgentFS vetting spike + the VirtualFS seam (lane: infra)”Vet Turso AgentFS against our load: FUSE-mount a .db, run a representative
repo checkout + edit burst + concurrent branch reads, measure latency and
crash behavior, and confirm the SQL audit trail captures every write. Freeze
the Go seam the rest of the design consumes. Outcome includes a written
go/no-go against OQ2 (fallback if AgentFS is not production-ready).
- Interfaces: produces
go/internal/vfs.VirtualFS:Mount(ctx, branch BranchID) (mountpoint string, unmount func() error, err error),Branch(ctx, parent BranchID) (BranchID, error),Audit(ctx, branch BranchID, since time.Time) ([]AuditEntry, error). Consumes: an AgentFS release pin; nothing in-repo. - Depends: nothing (first task).
- Test cycle: spike benchmarks committed as a report in this record’s directory; interface lands with a fake impl + contract tests.
V2 — per-agent branch lifecycle in provision (lane: infra)
Section titled “V2 — per-agent branch lifecycle in provision (lane: infra)”Wire VirtualFS into the Runner’s provision flow: a provision request creates
a copy-on-write branch for the agent; teardown retires it; the audit query is
exposed for SEA-1718. Workspace (go/internal/runtime/agent.go:39-40)
gains a virtual-FS variant without breaking the fused clone-based path.
- Interfaces: consumes
vfs.VirtualFS(V1) and theSpecBuilderseam (go/internal/runner/host.go:46-48); produces aWorkspaceSourcevariant onAgentSpec(clone-dir today | vfs-branch new) the placements read. - Depends: V1.
- Test cycle: provision→branch→teardown round-trip against a real AgentFS
.dbin an integration test; fused path regression suite stays green.
H1 — the HandsRuntime seam + tier policy (lane: runner/infra)
Section titled “H1 — the HandsRuntime seam + tier policy (lane: runner/infra)”Define the backend-selecting exec API and the routing policy. No real backend yet: land the interface, the policy (which commands go to which tier, the fail-closed default of Global Constraint 9, the explicit KVM-absent degradation), and a fake backend for tests.
- Interfaces: produces
go/internal/hands.HandsRuntime:Exec(ctx, HandsSpec) (runtime.ExecOutput, error)withHandsSpec{Command []string, Dir string, Env map[string]string, Branch vfs.BranchID, Timeout time.Duration}— noteHandsSpeccarries no authoritativeBackend: the Runner’s tier policy assigns it (Backend ∈ {BackendIsolate, BackendSandbox, BackendMicroVM, BackendContainer}), a caller hint may only upgrade. Reserve a long-lived variant (ExecStreaming-shaped: live stdio + kill/wait handle, mirroringgo/internal/runtime/podman.go:299-307) in the seam now for SEA-1720 (OQ7), even if unimplemented, so freezing H1 does not force a breaking change later. Reusesruntime.ExecOutput(go/internal/runtime/podman.go:139-146). Sibling ofContainerRuntime, never a replacement for it. - Depends: V1 (for
vfs.BranchIDin the spec type). - Test cycle: unit tests on the tier-routing policy incl. the fail-closed default (unknown command → tier 3) and the KVM-absent degradation being explicit (error or logged fallback per policy, never silent).
H2 — Firecracker backend (lane: runner/infra)
Section titled “H2 — Firecracker backend (lane: runner/infra)”The microVM tier: a minimal kernel + rootfs image carrying the devenv toolchain, spawn/FUSE-mount(branch)/run/teardown glue in the e2b shape (minus Nomad/Consul), and a small warm pool so boot latency hides behind the exec. Hands-side egress: default-deny nft inside the microVM, allowlist from the hands policy (H4).
- Interfaces: implements
hands.HandsRuntimeforBackendMicroVM; consumesvfs.VirtualFS.Mount(V1) exported into the guest over virtio-fs/FUSE; consumes the Firecracker API socket (machine config, drives, vsock). Produces the rootfs image build (nix, sibling ofagent-image/). - Depends: V1, H1.
- Test cycle: integration test on a KVM box — boot, mount a branch with
pre-made edits,
go testa tiny module, assert edits were visible with no copy step, teardown reclaims the VM; boot-latency budget asserted (<500ms p95 warm).
H3 — sandbox + container fallback backends (lane: runner/infra)
Section titled “H3 — sandbox + container fallback backends (lane: runner/infra)”The lighter tiers: bubblewrap/namespaces (BackendSandbox) for trusted
worker tasks, and the gVisor-hardened container (BackendContainer) as the
KVM-absent fallback, reusing ContainerRuntime
(go/internal/runtime/podman.go:286-324) unchanged underneath.
- Interfaces: implements
hands.HandsRuntimeforBackendSandboxandBackendContainer; consumesruntime.ContainerRuntime(container path) and the AgentFS mountpoint (bind-mounted into the sandbox). - Depends: H1; V1 for the mount.
- Test cycle: sandbox denies net + escapes by default (probe tests); container fallback runs the same H2 smoke suite behind gVisor.
H4 — egress split: brain policy vs hands policy (lane: runner/infra)
Section titled “H4 — egress split: brain policy vs hands policy (lane: runner/infra)”Split the single EgressPolicy (go/internal/runtime/egress.go:29-34) into
two derivations in the Runner: the brain allowlist (LLM/API endpoints, git
hosts) and the hands allowlist (default-deny plus build-time hosts), both
fail-closed in the NftScript() shape (go/internal/runtime/egress.go:71-107).
Applied per placement: nft in the microVM/sandbox netns for hands; the brain’s
policy enforced by whichever placement hosts it.
- Interfaces: consumes
runtime.EgressPolicy/NftScript; producesBrainEgress/HandsEgressfields whereAgentSpec.Egress(go/internal/runtime/agent.go:41-42) is today, derived inSpecBuilder. - Depends: H1 (to know where hands policies attach); parallel with H2/H3.
- Test cycle: existing egress probe tests extended per placement (blocked host unreachable from hands VM; LLM endpoint reachable from brain only).
B1 — brain tools over the virtual FS + tier-2 in-isolate exec (lane: compass-agent)
Section titled “B1 — brain tools over the virtual FS + tier-2 in-isolate exec (lane: compass-agent)”Point the agent’s read/write/edit tools at the AgentFS mountpoint (tier 1),
and add the tier-2 light exec: shell-to-JS (just-bash-style) for grep-class
deterministic ops inside the brain’s own bun process — no VM, no container.
Because tier 2 runs inside the credential-holding brain process, its
containment is load-bearing (Global Constraint 9): a fixed builtin command
set (JS implementations, no shell-out), no subprocess/FFI, FS access
confined to the branch mount, no network, and a resource ceiling
(wall-clock + memory) so a misclassified heavy op degrades to a killed tier-2
call, never a wedged brain. Anything outside the builtin set is not tier 2 —
it escalates to tier 3 (B2).
- Interfaces: consumes the mountpoint path handed to the agent via env
(extending
AgentEnv.execSpec’s env contract,go/internal/runner/agent_exec.go:77-96— aCOMPASS_VFS_ROOTvariable in the same style asCOMPASS_WORKDIR); produces the tier-2 exec module inpackages/compass-agent. - Depends: V1 (a mountable branch to develop against); independent of H*.
- Test cycle: agent-side unit tests over a real FUSE mount incl. the containment probes (a subprocess/net attempt from tier 2 is refused; an over-budget op is killed); a golden session transcript showing tier-1/2 ops never leave the process.
B2 — brain→hands escalation (lane: compass-agent)
Section titled “B2 — brain→hands escalation (lane: compass-agent)”The brain’s exec tool classifies a command (tier 2 vs escalate) and sends tier-3 work to the Runner over the existing per-container/per-agent socket RPC, carrying the command + branch id. The brain never names a backend and never talks to Firecracker directly: the Runner assigns the isolation tier by policy (Global Constraint 9); the classifier fails closed (unknown → escalate).
- Interfaces: consumes a new Runner RPC
ExecHands(ExecHandsRequest) → ExecOutputwhere the request carries the command, cwd, env, and branch id but no authoritativeBackend(an optional upgrade-only hint at most); the Go side derives the tier and dispatches tohands.HandsRuntime. Produces the agent-side classification policy + tool wiring. - Depends: H1 (interface), B1; H2/H3 for end-to-end.
- Test cycle: fake-HandsRuntime integration test — a
go testcommand escalates, agrepdoes not, an unknown command escalates (fail-closed); a hint requesting a weaker tier does not lower it; classification table unit-tested.
B3 — host-process brain placement (lane: runner/infra, with compass-agent)
Section titled “B3 — host-process brain placement (lane: runner/infra, with compass-agent)”A second brain placement beside the fused one: the compass-agent bundle
(agent-image/entrypoint.nix:227-229) runs as a supervised host process
against the AgentFS mount — no per-agent container. AgentRuntime.Launch
(go/internal/runtime/agent.go:173-198) becomes one of two placements behind a
BrainPlacement seam; registry, session RPC resolution
(go/internal/runtime/registry.go) and StartAgent
(go/internal/runner/agent_exec.go:136) resolve either. The host placement
must reproduce the two guarantees the container gave structurally, which is
real scope, not a wrapper: (1) egress — today default-deny is armed via the
container’s netns + NET_ADMIN entrypoint running nft
(go/internal/runtime/agent.go:300-309, egress.go:71-107); a host process has
neither, so it gets its own netns (pasta/slirp + nft, the recommended
mechanism — OQ6) or bwrap network confinement, applying H4’s BrainEgress
fail-closed; (2) secrets — every credential materializer is a container
exec today (WriteAgentFile go/internal/runtime/agent.go:239-260,
secrets_materialize.go), so B3 provides a host-side equivalent honoring the
same 0600 / stdin-not-argv floor.
- Interfaces: produces
BrainPlacementwithLaunch(ctx, AgentSpec) (BrainHandle, error)/Teardown, implemented by the existing container path and the new host-process path; consumesvfs.VirtualFS(mount), H4’sBrainEgress, and a host-side secrets materializer (0600, stdin-not-argv). - Depends: V2, B1, H4.
- Test cycle: dogfood-loop session driven end to end on the host-process placement; egress probe (blocked host unreachable, LLM endpoint reachable) and a 0600-perms assertion on the materialized secrets; fused-placement regression suite stays green (Global Constraint 6).
E1 — end-to-end split session + tier-mix telemetry (lane: runner/infra)
Section titled “E1 — end-to-end split session + tier-mix telemetry (lane: runner/infra)”Drive a full real session on the split path: host-process brain, tier-1/2 ops in-process, one compile/test escalation into a Firecracker microVM, teardown reclaims everything. Instrument the tier mix so the <10%-microVM target is measured, not asserted.
- Interfaces: consumes everything above; produces a per-session tier-mix
counter surfaced through the Runner’s session status (the
AgentSessionStatusattribution path,go/internal/runtime/agent.go:52-56comment trail). - Depends: B2, B3, H2.
- Test cycle: the dogfood-loop e2e extended with a split-path variant; tier-mix assertion in the harness.
G1 — orphan reclamation / crash reconciliation (lane: runner/infra)
Section titled “G1 — orphan reclamation / crash reconciliation (lane: runner/infra)”AgentRuntime is deliberately stateless about container existence — the
container engine is the source of truth and Exists reconciles after a Runner
crash (go/internal/runtime/agent.go:1-11). Firecracker VMs, virtiofsd/FUSE
mounts, and vfs CoW branches have no such engine, so a Runner crash leaks
them. Preserve the statelessness property: a persisted inventory of live
VMs/mounts/branches + a startup reconciliation pass that reaps orphans.
- Interfaces: consumes
hands.HandsRuntime(VM handles),vfs.VirtualFS(branch inventory); produces a reconciliation pass invoked on Runner startup, beside the existing container reconcile. - Depends: H2, V2.
- Test cycle: kill the Runner mid-exec, restart, assert no leaked VM / virtiofsd / mount / branch remains.
Checklist mirroring the plan (owning lane in brackets; order = dependency order):
- M0 [compass-agent] — baseline the tier mix from real dogfood session logs (parallel pre-task, no deps).
- V1 [infra] — AgentFS vetting spike + freeze
vfs.VirtualFS(Mount/Branch/Audit); go/no-go vs OQ2 fallback. - H1 [runner/infra] —
hands.HandsRuntimeseam + tier-routing policy + explicit KVM-absent degradation (depends: V1). - V2 [infra] — per-agent CoW branch lifecycle in provision;
WorkspaceSourcevariant onAgentSpec(depends: V1). - B1 [compass-agent] — brain tools over the AgentFS mount + tier-2 in-isolate shell-to-JS exec (depends: V1).
- H2 [runner/infra] — Firecracker backend: rootfs image, warm pool, mount/run/teardown glue (depends: V1, H1).
- H3 [runner/infra] — bubblewrap sandbox + gVisor container fallback backends (depends: H1, V1).
- H4 [runner/infra] — egress split into
BrainEgress/HandsEgressderivations, per-placement fail-closed arming (depends: H1). - B2 [compass-agent] — fail-closed tier classification +
ExecHandsescalation RPC (Runner-assigned tier, no agent-authored backend) from brain to Runner (depends: H1, B1; e2e needs H2/H3). - B3 [runner/infra + compass-agent] —
BrainPlacementseam + host-process brain placement beside the fused container placement (depends: V2, B1, H4). - G1 [runner/infra] — orphan reclamation for microVMs / virtiofsd mounts / vfs branches after a Runner crash (depends: H2, V2).
- E1 [runner/infra] — full split-path session end to end + tier-mix telemetry proving the <10%-microVM target (depends: B2, B3, H2).
Open Questions
Section titled “Open Questions”Each tagged load-bearing (an executor hits real ambiguity — blocks merge; the caller asks Matt) or non-load-bearing (deferred with rationale). Each carries a recommendation; the record is drafted against the recommendation as a stated assumption.
- [load-bearing] Firecracker vs gVisor as the default tier-3 boundary for the single-box dogfood milestone. Firecracker is the designed default (hardware boundary for model-written code), but the first self-host boxes may lack KVM/nested-virt, making gVisor the de facto first tier shipped. Recommendation: Firecracker is the contract default; H3’s gVisor fallback may land and even ship first where KVM is absent, but H2 remains the acceptance bar for E1 — E1 does not close on a gVisor-only box.
- [load-bearing] AgentFS production-maturity risk + fallback. AgentFS is
young (MIT, Turso); the tier-1/3 “no copy” story leans on it. V1 is the
vetting spike; the fallback if it fails needs a ruling, and the honest fork
is between two imperfect options: (a) plain host directory + overlayfs CoW
branches with the audit trail reconstructed by writing back the branch
diff on teardown (complete, but not real-time), or (b) an fsnotify/inotify
journal (real-time, but lossy — queue overflow drops events, no ordering
or content capture). Either way the SQL-auditable property that serves
SEA-1718’s “human-auditable, not a black box” constraint degrades — this
is an explicit audit downgrade needing a SEA-1718 ruling, not a shim that
preserves it. Recommendation: (a) overlayfs + branch-diff write-back;
keep
vfs.VirtualFSnarrow enough that the swap is impl-only. Flag the downgrade to SEA-1718 if V1 goes no-go. - [load-bearing] Does the brain↔hands seam extend
AgentRuntimeor sit as a new tier below/beside it? This record designs it as a sibling:hands.HandsRuntimebesideruntime.ContainerRuntime, withAgentRuntimenarrowing to the fused brain placement behind a newBrainPlacementseam. The alternative — growingAgentRuntimeinto the split façade — keeps one entry point but muddles its container-lifecycle contract (go/internal/runtime/agent.go:150-158;ContainerRuntimeis keyed onContainerID,podman.go:286-325). Recommendation: sibling seams as designed; the existing seams’ signatures stay honest. - [load-bearing] Is increment #1 a full split, or hands-on-demand behind
the current fused path? The earlier “bind-mount the existing clone into
the microVM” idea is not executable as-is: the clone is created inside
the container (
go/internal/runtime/workspace.go:3-5,podman.go:1-8), there is no host path to bind into a Firecracker guest, and reaching into rootless-podman overlay upperdirs of a live container is the shared-mutable hack this design refuses. So the real fork is: (a) increment #1 includes V1 — the fused container mounts an AgentFS branch as its checkout (V1 → H1 → H2 → B2), hands-on-demand lands on the vfs from day one; or (b) add a host-volume-workspace prerequisite (a newW0moving the clone to a host-backed volume, with userns uid-mapping + credential-scoping consequences — itself a nontrivial change to the fused path) so hands can bind the workspace without vfs. Recommendation: (a) — accept the vfs dependency in increment #1; it is smaller thanW0and avoids a throwaway host-volume detour. Only E1’s host-process-brain acceptance belongs to increment #2 (V2/B1/B3). - [load-bearing] FUSE→virtio-fs throughput for heavy builds — does the
no-copy pillar hold? Tier 3 mounts the AgentFS branch (host FUSE) and
re-exports it into the guest over virtio-fs; many-small-file workloads
(
go test,bun install, git) are the classic FUSE worst case and the compile could slow by more than the copy it saves. V1 benchmarks a tier-1 workload only; nothing benchmarks a compile inside a microVM over the mounted branch until E1 (last task, after every backend is built). Recommendation: add a compile-workload benchmark to V1/H2 (go test over FUSE→virtio-fs vs a materialized tmpfs/reflink copy), and name the escape hatch — for heavy execs, materialize the branch into guest-local storage and write back the diff — so “no copy” is a measured default with a fallback, decided before three backends are built, not a pillar that can’t bend. - [load-bearing] Host-process brain egress + secrets mechanism (blocks
B3). Global Constraint 7 promises default-deny for every placement, but
today’s enforcement is structurally container-shaped (netns +
NET_ADMINentrypoint arming nft,agent.go:300-309,egress.go:71-107; secrets via container-execWriteAgentFileagent.go:239-260). A supervised host bun process has neither. Recommendation: give the host placement its own netns via pasta/slirp + nft (closest to today’s mechanism, keepsNftScript()reusable) with bwrap network confinement as the alternative, plus a host-side 0600 / stdin-not-argv secrets materializer — named in B3’s scope. Matt to confirm the netns primitive. - [load-bearing]
HandsRuntimelong-lived / streaming exec shape (SEA-1720).Execis completion-shaped; SEA-1720’s agent-launched dev servers need a long-lived process with a live handle/port.ContainerRuntimeitself splitsExecvsExecStreamingfor this (podman.go:299-307). If H1 freezes a completion-only seam, SEA-1720 forces a breaking change post-freeze. Recommendation: reserve a streaming variant in the H1 interface now (even unimplemented); defer the dev-server port-exposure wiring itself to SEA-1720. - [non-load-bearing] Tier-2 command classification table. The safety invariant (unknown → tier 3; the agent can never lower the tier) now lives in Global Constraint 9, so the table contents are a policy detail iterated behind B1/B2’s unit-tested allowlist — a wrong classification escalates to a slower tier, never opens a hole. Deferred.
- [non-load-bearing] Warm-pool sizing and reclaim policy for H2. Pure tuning; measurable once E1’s telemetry exists. Deferred — start with a pool of 2 and a 60s idle reclaim.
- [non-load-bearing] Concurrent-writer coherence on a shared branch (the brain edits while hands compile on the same mount). Assumed fine: the single FUSE daemon serializes writes; stated here so an executor does not assume independent caches. Revisit only if a backend bypasses the daemon.
- [non-load-bearing] Worker-isolate brain placement. Explicitly optional
edge; nothing in this plan builds it. Deferred until a managed-edge
requirement exists — the
BrainPlacementseam (B3) is where it would plug in. - [non-load-bearing] Go-brain density work (SEA-1719). Composes with but is not part of this record; the split makes it possible (an idle goroutine brain is near-zero cost, hands in Firecracker regardless), tracked on SEA-1719.