cmd / sand

sand boots a Firecracker microVM for each agent task, runs the task in it, and destroys the VM. The agent is a language model that calls tools in a loop. Its tools include a shell, so the model writes and runs commands I did not write. Those commands need a boundary between what the model can reach and what I own.

Why not a container

A container shares the host kernel with every other container and with the host itself. A Firecracker guest runs its own Linux kernel behind a virtual machine boundary.

The split

The host holds the model API key and runs the agent loop. The guest runs commands and returns output. When the model calls a tool, the host sends the command to the guest over vsock, a socket between the VM and its host that no network route reaches.

host                          guest
  |  model API key              |  sh -c <command>
  |  agent loop                 |  stdout, stderr
  |  vsock connect  --------->  |  vsock listen :5000
  |  JSON request   --------->  |  run, stream, exit code

The guest has no network by default. A task that needs the internet gets it through a flag.

Two templates exist. The minimal template is Ubuntu with a shell and the exec agent, at 256 MiB of RAM. The Go template adds git, the Go toolchain, and a warm build cache, at 1 GiB.

Isolation

Each guest runs under the Firecracker jailer: a separate uid, its own network namespace, and cgroup limits. One nft table drops guest-to- guest traffic and traffic to private address ranges.

The disk is a reflink clone of a template on btrfs. A clone takes about 3 ms and shares extents with the template until the guest writes to it.

Exec protocol

The host and the guest agent talk over one vsock connection per command. The host sends one JSON line. The agent streams back frames: one byte for the stream, a 4-byte length, and the payload.

type Request struct {
	Argv  []string `json:"argv"`
	Dir   string   `json:"dir,omitempty"`
	Env   []string `json:"env,omitempty"`
	Stdin bool     `json:"stdin,omitempty"`
}

The protocol has no authentication. The only way to reach the guest is through the host's vsock socket, which the jailer owns.

Snapshots

A snapshot captures the guest's full memory and device state. A restore from a local file takes about 560 ms. Snapshots and templates live in Cloudflare R2, compressed with zstd. A 512 MiB memory file compresses to about 33 MB. A snapshot is a checkpoint, not a backup.

What sand does not do

sand runs on one host and keeps its state in Postgres. The job table is built and tested. The worker that claims a job, boots a guest, and runs the task is next.

sand has no live migration, no guest-to-guest communication, and no persistent disk across tasks. A guest is disposable by design.

← All articles