Documentation menu

Sandboxes

Every run of a runbook gets its own sandbox: an isolated Linux machine that Jetty creates for that run, runs the agent in, and deletes when the run ends. This page covers what is in a sandbox, what Jetty does to it from start to finish, its limits, and how to run on a Docker image of your own.

The short version:

  • One sandbox per run. Nothing carries over between runs, and concurrent runs never share a machine.
  • The default is the python312-uv snapshot: Python 3.12, Node 22, Chromium and the common agent CLIs, ready in seconds.
  • The agent works in /app, reads uploaded files from /app/assets and must write its deliverables to /app/results.
  • Default size and time: 4 CPUs, 8 GB of memory, 10 GB of disk, and up to 30 minutes for the agent.
  • You can bring your own environment: another named snapshot, a public Docker image, or a base image plus packages.

What a sandbox is

A sandbox is an isolated machine with its own filesystem, processes and network access, created from an image. The agent has a real shell in it: it can install packages, run a browser, start servers and write files, and none of that touches another run or Jetty itself. In most sandboxes the agent runs as root.

The sandbox is where the agent runs. The parts of a run that surround the agent run elsewhere: Jetty's worker resolves the run's parameters and secrets, creates the sandbox, streams the agent's log, collects the outputs and records the run.

Choosing the environment

A sandbox is created from one of three sources. Jetty takes the first that applies:

SourceSet withStart-upUse it when
1. Snapshotsnapshot, in the runbook's frontmatter or a run's init_paramsSeconds: the image is already builtAlmost always. This is the default
2. Docker imageimage, with snapshot left emptyThe image is pulled and prepared before the sandbox startsYou need a toolchain, a runtime or system libraries the snapshots don't have
3. Generated Dockerfilebase_image, apt_packages, pip_packages, with no snapshot and no imageMinutes: the image is built for the runYou need a few extra packages and have no image of your own

A named snapshot wins whenever it exists. If the name doesn't match an available snapshot, Jetty falls through to the image, then to the Dockerfile. apt_packages and pip_packages apply only on the Dockerfile path: a snapshot or an image already contains its packages.

The default snapshot

A runbook that names no snapshot runs on python312-uv. It contains:

  • Python 3.12 on Debian (bookworm), with pip, uv and a C and Fortran build toolchain, so most Python packages install without extra system work
  • Node 22 and npm
  • git and curl
  • Chromium, the Playwright Python library and the Playwright MCP server, for browser automation
  • The claude-code, codex, opencode and hermes agent CLIs, pre-installed

prism-playwright is the other named snapshot, built around Playwright and Chromium for browser-heavy work. A runbook picks its snapshot in frontmatter, and a run can override it:

RUNBOOK.md frontmatter
---
agent: claude-code
snapshot: prism-playwright   # python312-uv (default) | prism-playwright
---

An agent whose CLI isn't in the image is installed when the run starts, which adds a few seconds. The agent can also install what it needs itself, with pip, uv, npm or apt-get: a runbook step such as “install pdfplumber” works, and costs that install time on every run.

The life of a sandbox

  1. Prepare. Jetty resolves the run's parameters, fills the runbook's {{variables}}, and downloads the run's attached files.
  2. Create. A new sandbox is created from the snapshot, image or generated Dockerfile. No secret or user variable is part of the image.
  3. Set up. Jetty creates the working directories, places the attached files in /app/assets, installs or pins the agent CLI, and writes the MCP server configuration if the run has one.
  4. Run the agent. The agent starts in /app with the runbook as its instruction and its environment applied. Its log is streamed to the run while it works.
  5. Clean up credentials. When the agent exits, the credential files Jetty wrote are deleted and, if Jetty has checks to run, the processes the agent left behind are stopped.
  6. Collect. Jetty downloads the agent's log, /app/results and /app/assets.
  7. Check. Jetty runs the runbook's Jetty checks in the same sandbox, in a clean shell.
  8. Save and delete. The outputs are saved to the collection's storage and the sandbox is deleted. A run that fails or times out deletes its sandbox too.

As a backstop, a periodic sweep deletes any sandbox that is still running but has gone idle, so a sandbox whose run was interrupted does not linger.

Where things are

PathWhat it holdsKept after the run
/appThe agent's working directoryNo, apart from the two directories below
/app/resultsThe run's deliverables, written by the agentYes. The frontmatter's primary_outputs are surfaced first
/app/assetsFiles attached to the run; zips are extractedYes
/app/checks/<name>The git repositories a runbook's code checks declare as sourcesNo
/logs/agentThe agent CLI's log, session and transcriptYes, as the run's agent log

A runbook can write these paths as {{results_dir}}, {{assets_dir}} and {{checks_dir}}. Jetty fills them in; a run cannot move them.

Important

Results are a contract

A run whose agent finishes with nothing in /app/results fails. Anything the agent leaves elsewhere in the sandbox is gone when the sandbox is deleted.

Getting files in. Attach files to a run directly, or pre-stage them through the upload API and pass the returned storage paths. The exact calls are in the machine instructions.

Resources and limits

LimitDefaultChange it with
CPUs4cpus on the task's step, for an image or Dockerfile sandbox. A snapshot has its own fixed size
Memory8 GBmemory on the task's step (for example "16G"), under the same rule
Disk10 GBFixed
Agent timeUp to 30 minutestimeout_sec on the task's step, in seconds, to set a shorter budget. For longer jobs, contact [email protected]
Agent install10 minutesFixed. Pre-install the CLI in the image to avoid it
Sandbox creation20 minutes, retried onceFixed. Covers pulling or building an image

When the agent reaches its time limit it is stopped. A long runbook should write its outputs to /app/results as it goes. If a job needs more than 30 minutes, contact [email protected].

Network access

Sandboxes have outbound network access. The agent can fetch URLs, call APIs, clone repositories and install packages, and it reaches its model provider the same way. Jetty adds no allow-list, so what a run can reach is decided by the credentials you give it; see Secrets and Environment Variables. A run can drive Chromium inside the sandbox for sites that need a real browser.

Running on your own Docker image

When the snapshots don't have what the job needs (a JVM, a GPU-free ML stack, a specific compiler, your company's CLI), build an image and point the run at it.

What the image needs

  • Pullable without credentials. Jetty takes an image reference and no registry login, so the image must be public (for example on GHCR or Docker Hub). Don't bake secrets into it; they reach the run through the collection.
  • A normal Linux userland: bash and the core utilities. Jetty runs every command through bash.
  • A way to install the agent. The agent installers expect a Debian or Ubuntu base (apt-get) with curl, and install Node if it is missing. Putting Node and the agent CLI in the image skips that work on every run. If the CLI in the image is not the version Jetty pins, Jetty installs the pinned one.
  • What your checks use. If the runbook has Jetty-run code checks, the image needs python3 for the built-in checks and git for check sources. Jetty verifies this before the agent starts and fails the run early if something is missing.

The image's ENTRYPOINT and CMD don't run your job: Jetty starts the sandbox and executes the agent in it.

Step 1: Build and push the image

Dockerfile
FROM python:3.12-slim-bookworm

# What Jetty and the agent installer rely on: bash, coreutils, curl, git.
RUN apt-get update && apt-get install -y --no-install-recommends \
      curl git ca-certificates gnupg && \
    rm -rf /var/lib/apt/lists/*

# Node, so the agent CLI doesn't have to install it on every run.
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | sh - && \
    apt-get install -y nodejs && rm -rf /var/lib/apt/lists/*

# The agent CLI itself (optional; Jetty installs or re-pins it if needed).
RUN npm install -g @anthropic-ai/claude-code

# Your own toolchain.
RUN pip install --no-cache-dir pandas duckdb

WORKDIR /app
Shell
docker build -t ghcr.io/acme/report-env:1.4.2 .
docker push ghcr.io/acme/report-env:1.4.2

Use a version tag rather than latest, so a run records exactly which environment it used and a new push can't change a runbook's behaviour unnoticed.

Step 2: Point a run at it

An image is a run parameter, not a frontmatter key. Pass image and clear snapshot, because a named snapshot takes priority and every runbook task has python312-uv as its default:

Shell
curl -X POST "https://flows-api.jetty.io/api/v1/run/<your-collection>/<your-task>" \
  -H "Authorization: Bearer $JETTY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "init_params": {
      "snapshot": "",
      "image": "ghcr.io/acme/report-env:1.4.2"
    }
  }'

init_params merge per top-level key, so the task's runbook, agent, model and variables are unchanged. The run log says which source was used: Creating sandbox from registry image: ….

Step 3: Make it the task's default

To use the image on every run, set the same two keys in the task's own init_params. Raise cpus and memory in the step configuration if the job needs more:

Task workflow
{
  "init_params": {
    "instruction": "<contents of RUNBOOK.md>",
    "agent": "claude-code",
    "model": "anthropic/claude-sonnet-5",
    "model_provider": "openrouter",
    "snapshot": "",
    "image": "ghcr.io/acme/report-env:1.4.2",
    "vars": {},
    "file_paths": []
  },
  "steps": ["run"],
  "step_configs": {
    "run": {
      "activity": "runbook",
      "agent_path": "init_params.agent",
      "model_path": "init_params.model",
      "model_provider_path": "init_params.model_provider",
      "snapshot_path": "init_params.snapshot",
      "instruction_path": "init_params.instruction",
      "template_variables_path": "init_params.vars",
      "files_path": "init_params.file_paths",
      "cpus": 4,
      "memory": "8G"
    }
  }
}

Note

A task's parameters are generated from the runbook's frontmatter when the task is built from the runbook text, and the frontmatter has no image key. If a task is rebuilt from its runbook, set image and the empty snapshot again, or pass them with each run.

Extra packages without an image

If all you need is a few packages on top of a stock base, skip the registry and let Jetty generate the Dockerfile. With no snapshot and no image, it builds from base_image (default python:3.13-slim-bookworm), adds git, curl and uv, then installs your apt_packages and pip_packages:

JSON
"init_params": {
  "snapshot": "",
  "base_image": "python:3.12-slim-bookworm",
  "apt_packages": ["ffmpeg", "poppler-utils"],
  "pip_packages": ["pandas", "pdfplumber"]
}

This builds an image for the run, which takes minutes. For a runbook you run often, a pushed image starts faster and is the same on every run.

Troubleshooting

SymptomCauseFix
The run log says Creating sandbox from snapshot although you passed imageThe task's default snapshot still appliesSend "snapshot": "" with the image
Sandbox creation fails or times outThe image is private, the reference is wrong, or the image is very largeCheck that docker pull works without logging in; slim the image
Agent install failedThe image has no Node and no apt-get or curl to install it withAdd Node and the agent CLI to the image
The run fails before the agent starts, naming a missing toolThe runbook's Jetty checks need python3 or gitAdd them to the image
apt_packages or pip_packages had no effectThe run used a snapshot or an imageClear snapshot and image, or install the packages in your image
The run fails with no results filesThe agent wrote outside /app/results, or ran out of timeName /app/results in the runbook and write outputs incrementally