Cereal docs
Sandboxes

Sandboxes

Give an agent a managed workspace for commands, files, and application previews.

An agent version may declare a sandbox: a machine of its own, with a workspace and a shell.

export const agent = defineAgent({
  agent: {
    slug: "repo-bot",
    model: { provider: "gateway", model: "anthropic/claude-sonnet-4.6" },
    system: "You work in the checked-out repository at /workspace.",
    sandbox: {
      provider: "modal",
      image: {
        base: "ghcr.io/acme/agent-base:1.4",
        steps: [
          "RUN apt-get update && apt-get install -y git",
          "RUN git clone --depth 1 https://github.com/acme/handbook /workspace"
        ]
      },
      resources: { cpu: 2, memoryMb: 4096 },
      network: { egress: { mode: "allow", allow: ["github.com"] } },
      lifecycle: { idleAfterMs: 15 * 60 * 1000, onIdle: "snapshot", keepSnapshots: 2 }
    }
  }
})

Cereal provisions and manages the sandbox. Set provider to modal; no sandbox host account or infrastructure setup is required.

The thirteen tools

bashRun a command.
read, write, edit, multi_editFiles.
glob, grepFind files, find text.
process_list, process_logs, process_controlWhat is running, its output, start/stop.
port_exposePublish a port as a preview URL.
publishPut a file into blob storage and hand back a reference.
fetchDownload a URL, or a blob reference, into the workspace.

The catalogue is static: Cereal presents all thirteen without starting a machine, which is what makes just-in-time provisioning possible. The sandbox is created on the first sandbox tool call and released when the session goes quiet.

Narrowing what may be called

sandbox: {
  // …
  tools: { preset: "read-only", deny: ["fetch"] }
}

preset and allow intersect; deny removes tools from that result. Use these settings to restrict a sub-agent as well:

subagents: [{
  kind: "derived", name: "review", extends: "parent",
  description: "Reads the diff and comments. Changes nothing.",
  overrides: { sandbox: { tools: { preset: "read-only" } } }
}]

read-only is read, glob, grep, process_list, process_logs and fetch. The preset excludes bash. It includes fetch, which can write downloaded files; add deny: ["fetch"] when that is not appropriate. Denied tools are hidden from the model and rejected if called.

One machine per session tree

A sandbox belongs to a session tree, keyed on the root session. A sub-agent child attaches to the machine its root already has, same workspace and same processes, and its sandbox.attached event records sharedWith.

Two dials: sandbox.scope: "session" puts a session on its own machine, and sandbox.serializeTools runs the tree's sandbox tool calls through one semaphore. Parallel children share a filesystem. Coordinate writes or enable serialization to avoid conflicting edits.

Lifecycle

lifecycle: {
  idleAfterMs: 900_000,
  onIdle: "snapshot",       // or "release"
  snapshotEveryMs: 0,       // off
  keepSnapshots: 1,
  maxSnapshotAgeMs: 86_400_000
}

With onIdle: "snapshot", Cereal saves the workspace when it idles and restores it on the next sandbox tool call. A snapshot restores files, not running processes.

sandbox.resumed says which of three things happened:

fromMeaning
attachNothing was lost, running processes included.
snapshotThe filesystem came back. Processes did not.
freshIt did not, and detail says why.

The sandbox id never changes across a resume: it is also the channel id and a preview's audience.

The image

The image is part of the version. Changing a build step appends an agent version exactly as changing the prompt does, so sync stays idempotent and a step you did not edit does not rebuild.

Building it is not. A bake takes minutes, so sync returns as soon as the version is written and Cereal builds behind it. Until then, sessions.create throws AgentNotReady carrying baking or failed.

let { status } = await client.agents.imageStatus({ id: agentId })   // a read; never builds
while (status === "baking") { await sleep(5_000); ({ status } = await client.agents.imageStatus({ id: agentId })) }

Use RUN, ENV and WORKDIR, one instruction per line. To include files, fetch them in a RUN step or include them in your base image. A local build context is not uploaded, so COPY is not supported. Anything else is refused at bake time, terminally, including an ENTRYPOINT that could take the tools offline and a FROM that could swap the base out.

Set image.base to a registry image Cereal can pull, or omit it for debian:bookworm-slim. Your base must provide a shell and sha256sum. Cereal installs its sandbox tools after your image steps.

The default base is minimal, so it has no git, no curl, no python3 and no ripgrep (grep falls back to POSIX grep and says so). Install what your agent needs in a step, or name a base that already has it:

steps: [
  "RUN apt-get update && apt-get install -y --no-install-recommends ripgrep git curl python3 && rm -rf /var/lib/apt/lists/*"
]

agents.rebake({ id }) rebuilds the latest version's image without appending a version. That is for when the image's content moves while its spec does not: the clone above bakes whatever main was that morning. Run it on a schedule, the way you would a deploy, not on every request.

Operating one

POST /v1/sandboxes/list       { "sessionId": "ses_…" }
POST /v1/sandboxes/get        { "id": "sbx_…" }
POST /v1/sandboxes/snapshot   { "id": "sbx_…" }
POST /v1/sandboxes/resume     { "id": "sbx_…" }
POST /v1/sandboxes/release    { "id": "sbx_…" }     also revokes its preview grants

The control plane's session view has the same four buttons, with the state and the snapshot's age. The typed client wraps sandboxes.get, plus the two verbs that act inside the machine, exec and env. Those are the sandbox API, along with the hook that prepares a machine before the agent's first command.

On this page