diff --git a/README.md b/README.md index fabe0ccb..59f685ce 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ design holds to. That model lives under - [No human clicks](./docs/concepts/no-human-clicks.md) and [read-only inspection](./docs/concepts/read-only-inspection.md) — agents stand up the org through tools; the human holds only the security boundary. +- [Durable agents, disposable compute](./docs/concepts/durable-agents-disposable-compute.md) + and [isolation and egress](./docs/concepts/isolation-and-egress.md) — the agent + and its session are durable and Server-owned; the container/microVM it runs in + is disposable and contained. ## Toolchain diff --git a/docs/concepts/README.md b/docs/concepts/README.md index 8d246a44..9ee2b188 100644 --- a/docs/concepts/README.md +++ b/docs/concepts/README.md @@ -31,6 +31,16 @@ concept; the tool and prompt material carry the detail, these carry the model. load-bearing premise any external-session integration maps onto, never replaces. +## The runtime + +- **[Durable agents, disposable compute](./durable-agents-disposable-compute.md)** + — the agent and its session are durable and Server-owned (Postgres + S3); + the container/microVM it runs in is disposable and dies with the session. + Resume rebuilds the compute and reconstructs the transcript into it. +- **[Isolation and egress](./isolation-and-egress.md)** — model-written code is + contained, not trusted: a per-agent sandbox (container today, microVM in the + end state) with default-deny egress and no server credential. + ## The tools - **[The agent tool set](./tools.md)** — the native tools an agent drives diff --git a/docs/concepts/durable-agents-disposable-compute.md b/docs/concepts/durable-agents-disposable-compute.md new file mode 100644 index 00000000..a5301b9b --- /dev/null +++ b/docs/concepts/durable-agents-disposable-compute.md @@ -0,0 +1,127 @@ +# Durable agents, disposable compute + +A Compass agent is **durable**; the machine it runs on is **disposable**. The +agent — its identity, its place in the tree, and the full history of its work — +outlives any particular container or microVM it happens to be executing in. The +compute is cattle; the agent and its session are the thing of record. Getting +this split right is what lets Compass tear down, move, suspend, and rebuild the +expensive part (the sandbox) without ever losing the valuable part (the agent's +state). + +## The two things that persist, and the one that does not + +- **The agent is a stable, long-lived tree node.** Managers and their peers are + minted once and live as durable accounts — each with a home channel it is + always subscribed to — not spun up one-shot per task. An agent's identity, + its owner, its persona, and its position in the [agent + tree](./handle-vs-account.md) are model facts in the Server's store, not + properties of a running process. See [the comms model](./comms-model.md) for + why agents are stable nodes with home channels rather than ephemeral runs. +- **The session is durable and Server-owned.** Everything the agent has thought + and done — its streamed reasoning, tool calls, and edits — is persisted by the + Server as a durable transcript. The Server owns this store; the agent does not + custody it. (ledger DL-093) +- **The compute is disposable.** The container or microVM the agent executes in + holds only an *ephemeral working copy* of the session file, and it **dies with + the container**. It custodies nothing of record: no durable state, and — by + the [isolation and egress](./isolation-and-egress.md) posture — no storage + credentials at all. (ledger DL-089) + +The test for which bucket something falls in: **would it survive the sandbox +being destroyed right now?** The agent, its identity, and its transcript survive. +The process, the local session file, the checkout, and any in-memory state do +not — and nothing is allowed to depend on them surviving. + +## How the session stays durable without the agent holding storage + +The agent persists nothing durable itself. As a session runs, each committed +session entry is **tee'd upstream** to the Server as a frame: the agent writes +its container-local ephemeral file normally (so the agent runtime's own loader, +compaction, and rewrites work), and the same committed write is streamed up to +the Server, which owns durability. (ledger DL-089) + +The Server's durable store is **two-tier**: + +- a **Postgres hot tail** holding the recent, resume-relevant slice of the + transcript, and +- an **S3-compatible cold archive** that older, superseded segments flush out + to as verbatim log segments. + +So "the session streams out to durable object storage" is exactly right — the +durable home of a session's history is Server-owned Postgres plus S3-compatible +storage, never the sandbox's disk. (ledger DL-093) + +The durable transcript and the live trace you watch in the session log are the +**same artifact seen two ways** — the agent emits committed entries once, and +the Server both persists them and projects the live block-level trace from that +one store. (ledger DL-088) + +## Resume: rebuild the compute, reattach the agent + +Because the durable state lives in the Server and the compute is disposable, +bringing an agent back is a **reconstruct-into-a-fresh-sandbox** operation, not a +"find the old machine" one: + +1. The old container/microVM is gone (torn down, evicted, crashed, or migrated). +2. On resume the Server **reconstructs the session** from its durable transcript + store and hands that reconstructed session body to the Runner. +3. The Runner materializes it into a **new** container/microVM, and the agent + picks up exactly where it left off. + +The agent never notices it is running on different hardware. The identity was +never in the box; it was always in the Server. (ledger DL-087) + +## The compute substrate is moving: container → microVM + +Today an agent's sandbox is a **rootless-podman container**, one per agent, for +blast-radius isolation (ledger DL-024). The end-state substrate is a +**hardware-virtualized microVM** — the same disposable-compute role, hardened to +a VM-class boundary around model-written code. The migration path is: container +through Dogfood and trusted-tenant Beta, microVM in the end state; a host without +KVM degrades to the container runtime with an explicit capability log, never +silently. + +This move **does not change the durability contract on this page** — that is the +whole point of the split. Whether the sandbox is a container or a microVM, it is +still disposable, still custodies no durable state, and the agent still resumes +by the Server reconstructing its transcript into a fresh one. The substrate +change is an isolation upgrade (see [isolation and +egress](./isolation-and-egress.md)), not a change to what persists. + +The direction is already load-bearing in shipped decisions — the microVM's KVM +floor is what retired local agent execution and made the native app a thin client +against a headless, KVM-capable stack (ledger DL-235), and the self-host stack is +a host-level bring-up on a KVM-capable Linux machine (ledger DL-259). The microVM +Runner backend itself is designed in +[`microvm-runner.md`](../designs/infra/runtime/compass-elastic-session-runtime/microvm-runner.md) +(RIG-2394), under the hosted-platform end-state record +[`compass-elastic-session-runtime`](../designs/infra/runtime/compass-elastic-session-runtime/design.md) +(RIG-1717). + +## Why this split is the design, not an accident + +- **Disposable compute is what makes hosted scale possible.** If the agent's + state lived in the box, you could never evict an idle session, pack many + sessions onto a host, migrate one to another machine, or reclaim a crashed + sandbox without data loss. Because durable state is Server-owned, an idle + session's environment can be torn down entirely and rebuilt on activity — the + density play the hosted platform is built on. +- **Durable agents are what make supervision coherent.** A supervisor reasons + about a stable fleet of named agents with continuous histories, not a churn of + one-shot runs. The tree, the ownership trail, and the conversation history all + assume the agent outlives its current process. +- **The boundary is a security boundary too.** Keeping durable state and + credentials out of the sandbox means model-written code runs in a box that + holds nothing of record and cannot exfiltrate what it never had — see + [isolation and egress](./isolation-and-egress.md). + +## Quick reference + +| Thing | Durable? | Lives where | +| --- | --- | --- | +| Agent identity, owner, persona, tree position | yes | Server store (model fact) | +| Session transcript (reasoning, tools, edits) | yes | Server-owned: Postgres hot tail + S3-compatible cold archive | +| Home channel + its conversation history | yes | Server store | +| The container / microVM (the sandbox) | no | Runner host — disposable, rebuilt on resume | +| The container-local session file | no | dies with the sandbox (ephemeral working copy) | +| Storage / durability credentials | n/a | never in the sandbox — the agent holds none | diff --git a/docs/concepts/isolation-and-egress.md b/docs/concepts/isolation-and-egress.md new file mode 100644 index 00000000..c35cf921 --- /dev/null +++ b/docs/concepts/isolation-and-egress.md @@ -0,0 +1,97 @@ +# Isolation and egress: the sandbox around model-written code + +Compass runs code an AI model wrote. The load-bearing safety assumption is +therefore the opposite of trust: **treat every agent as potentially compromised +and contain it**, rather than trusting it to behave. Two boundaries do that +containment — an **execution boundary** (each agent in its own sandbox) and a +**network boundary** (default-deny egress from that sandbox). This page is the +model behind both. + +## Each agent runs in its own sandbox + +Every agent executes in a **per-agent sandbox** on the Runner, isolated from the +host and from every other agent. The reason is **blast radius, not credential +avoidance**: the point is that a misbehaving or compromised agent can damage only +its own sandbox, not the host and not its neighbors. (ledger DL-024) + +The sandbox is [disposable compute](./durable-agents-disposable-compute.md) — it +custodies no durable state and no storage credentials, so destroying it loses +nothing of record. That is what makes aggressive containment cheap: the strong +move (tear the whole thing down) has no data cost. + +The substrate is on a path from **rootless-podman container** (today, through +Dogfood and trusted-tenant Beta) to a **hardware-virtualized microVM** (the +end state). Both fill the same role; the microVM raises the execution boundary +from a shared-kernel container to a VM-class boundary with its own guest kernel — +the right isolation for running untrusted model-written code, and for putting +multiple tenants on one host. A box without KVM degrades to the container +runtime with an explicit capability log, never silently. The microVM Runner +backend is designed in +[`microvm-runner.md`](../designs/infra/runtime/compass-elastic-session-runtime/microvm-runner.md) +(RIG-2394); its KVM floor is already baked into shipped decisions — it is what +made the native app a thin client against a headless KVM-capable stack (ledger +DL-235) and the self-host stack a KVM-capable host bring-up (ledger DL-259). + +## Default-deny egress: the agent reaches only what it is allowed to + +Isolating the process is not enough — a compromised agent with open network +access could still exfiltrate. So the sandbox's network is **firewalled +default-deny**: an agent can reach nothing on the network unless a host is +explicitly allowlisted. (`go/internal/runtime/egress.go`) + +- **Empty policy = pure default-deny.** With no allowlist, only loopback, + already-established flows, and DNS to the container's own resolver work — + nothing else. Reaching an LLM provider, a forge, or object storage requires + that host to be added to the allowlist. (`egress.go:29-31`) +- **The agent cannot widen its own firewall.** The sandbox is granted the + capability to *arm* nftables only so a root entrypoint can install the ruleset + at launch; the agent itself then runs as a **non-root user with an empty + capability set**, so it can neither flush nor edit the rules even though the + sandbox nominally holds the capability. The ruleset is armed *before* the agent + process starts. (`egress.go:6-10`) +- **The rules are built to resist evasion.** Both IPv4 and IPv6 addresses of an + allowlisted host are resolved and allowed, so a dual-stack host can't be + reached over the family you forgot to list; and DNS is allowlisted first, then + names are resolved from inside the sandbox, so resolution can't be the thing + that widens the allowlist. (`egress.go:12-19`) + +## The agent holds no server credential + +Containment extends to what the agent is *given*, not just what it can *reach*. +The agent is **egress-sealed and holds no Compass server token** — it cannot call +privileged server RPCs directly. Privileged operations an agent appears to +perform (spawning a peer, a forge write) are **forwarded by the Runner to the +Server** — the Runner asserts no identity and is a pure relay; the Server +resolves the caller's account from the session binding at the edge and executes +the call under that account's authority — rather than handed to the agent as a +credential it could misuse. (ledger DL-076) + +This is the same shape as the [handle/account split](./handle-vs-account.md) +(the agent never holds a per-agent forge seat) and the [no-human-clicks +security boundary](./no-human-clicks.md) (the human provides secret *values*; the +agent only declares needs): capability is kept out of the model's hands and +mediated by the Server. + +## Why this is a hard principle + +- **The threat model is "the agent is compromised."** Model-written code is + untrusted by construction. Every boundary here assumes the agent may try to + reach a host it shouldn't or damage what it can touch, and contains the damage + structurally instead of relying on the agent's good behavior. +- **Containment is cheap because compute is disposable.** Since the sandbox holds + nothing durable, the strongest response — destroy it — costs nothing. Isolation + and disposability reinforce each other: the box is safe to nuke *because* it is + empty, and it can be empty *because* durability lives in the Server. +- **It scales to multi-tenant.** A VM-class boundary per agent plus default-deny + egress is what lets many tenants share a host without one reaching another — + the isolation guarantee the hosted platform is sold on. + +## Quick reference + +| Boundary | Mechanism | The agent cannot | +| --- | --- | --- | +| Execution | per-agent sandbox (container today → microVM end state) | touch the host or another agent's sandbox | +| Network | default-deny nftables egress, allowlist-only | reach any host not explicitly allowed | +| Firewall control | rules armed by root at launch; agent runs non-root, empty caps | flush or edit its own egress ruleset | +| Server authority | egress-sealed, no server token; privileged calls forwarded by the Runner, resolved + executed by the Server | call privileged server RPCs directly | +| Durable state / secrets | Server-owned; none in the sandbox | exfiltrate durable data or credentials it never held |