AI agent sandbox

A disposable machine your AI agent can break

Autonomous agents need somewhere to work: a filesystem that remembers what the last step did, a shell, a package manager, and a network. They should not need your laptop. Every sandbox here is a dedicated virtual machine, created with one API call and destroyed when the run is over.

An agent needs a machine, not a function

An agent that writes code does not run it once. It writes a script, runs it, reads the traceback, installs the module it forgot, edits a file and runs it again. Each of those steps depends on the previous one having left something behind — a file on disk, a package in site-packages, a cloned repository, a half-built virtualenv.

That rules out the stateless-function model, where every invocation starts from the same frozen image and forgets everything the moment it returns. You can fake continuity by replaying the whole history on each call, but the cost grows with the length of the run and it breaks the first time a step is not idempotent.

It also rules out running the code in your own process. "The model decides what to execute" and "the process holds our database credentials" cannot both be true and still be safe. Prompt injection turns any document the agent reads into a potential instruction, and the blast radius of a bad instruction is exactly the set of things the executing process can reach.

What is left is a machine. The only real questions are whose machine it is, how long it lives, and what it can reach while it is alive.

The loop, end to end

Create once per run, execute per step, destroy at the end. The sandbox keeps its state between calls, so the agent can build on what it just did.

import os, requests

API = "https://sandbox-as-a-service.com/v1"
H = {"Authorization": f"Bearer {os.environ['AAS_API_KEY']}"}

class Workspace:
    """One sandbox for the lifetime of one agent run."""

    def __enter__(self):
        self.id = requests.post(f"{API}/sandboxes", headers=H, json={
            "size": "small",
            "timeout_minutes": 30,
        }).json()["id"]
        return self

    def run(self, command, timeout_ms=120_000, cwd="/home/sandbox"):
        r = requests.post(f"{API}/sandboxes/{self.id}/exec", headers=H, json={
            "command": command,
            "timeout_ms": timeout_ms,
            "cwd": cwd,
        }).json()
        # Feed stderr back to the model — that is where the repair signal is.
        return r["stdout"], r["stderr"], r["exit_code"]

    def __exit__(self, *_):
        requests.delete(f"{API}/sandboxes/{self.id}", headers=H)


with Workspace() as ws:
    ws.run("git clone --depth 1 https://github.com/psf/requests repo")
    ws.run("python3 -m venv .venv && .venv/bin/pip install -q -e ./repo", timeout_ms=300_000)

    for attempt in range(5):
        code = agent.write_code(context)          # your model call
        out, err, status = ws.run(
            f"cat > /home/sandbox/task.py <<'PY'\n{code}\nPY\n"
            ".venv/bin/python /home/sandbox/task.py"
        )
        if status == 0:
            break
        context = agent.observe(stdout=out, stderr=err, exit_code=status)
const API = "https://sandbox-as-a-service.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.AAS_API_KEY}`,
  "Content-Type": "application/json",
};

async function withWorkspace(fn) {
  const { id } = await fetch(`${API}/sandboxes`, {
    method: "POST",
    headers,
    body: JSON.stringify({ size: "small", timeout_minutes: 30 }),
  }).then((r) => r.json());

  const exec = (command, timeoutMs = 120000) =>
    fetch(`${API}/sandboxes/${id}/exec`, {
      method: "POST",
      headers,
      body: JSON.stringify({ command, timeout_ms: timeoutMs }),
    }).then((r) => r.json());

  try {
    return await fn(exec);
  } finally {
    // Always tear down; the timeout is a backstop, not the plan.
    await fetch(`${API}/sandboxes/${id}`, { method: "DELETE", headers });
  }
}

await withWorkspace(async (exec) => {
  await exec("npm init -y && npm install --silent zod", 300000);
  const r = await exec("node /home/sandbox/task.js");
  console.log(r.exit_code === 0 ? r.stdout : r.stderr);
});
# Create the workspace for this run
SBX=$(curl -sX POST https://sandbox-as-a-service.com/v1/sandboxes \
  -H "Authorization: Bearer $AAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"size":"small","timeout_minutes":30}' | jq -r .id)

# Step 1 — set up
curl -sX POST https://sandbox-as-a-service.com/v1/sandboxes/$SBX/exec \
  -H "Authorization: Bearer $AAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"pip install --user -q httpx","timeout_ms":180000}'

# Step 2 — run what the model wrote, with its own env
curl -sX POST https://sandbox-as-a-service.com/v1/sandboxes/$SBX/exec \
  -H "Authorization: Bearer $AAS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command":"python3 task.py","cwd":"/home/sandbox","env":{"TASK_ID":"42"}}'

# Done
curl -sX DELETE https://sandbox-as-a-service.com/v1/sandboxes/$SBX \
  -H "Authorization: Bearer $AAS_API_KEY"

Failure modes worth designing for

The agent never calls DELETE

It will happen — a crash in your orchestrator, an exception between steps, a model that decides the task is finished and stops emitting tool calls. Every sandbox carries a timeout and is destroyed when it expires, and a reconciler compares recorded state against what is actually running. Treat the timeout as the real guarantee and DELETE as the optimisation.

A command hangs forever

Interactive prompts are the usual cause: a package manager asking for confirmation, a test runner waiting on stdin, an SSH host-key question. Set timeout_ms per command rather than relying on the sandbox lifetime, and prefer non-interactive flags. A command that hits its timeout returns what it produced so far with truncated set.

Output blows the context window

A failing test suite can emit megabytes. Responses report truncated so you know output was cut, but the better fix is at the source: pipe through tail -c, use the quiet flags your tools already have, and write large artefacts to a file the agent can grep instead of returning them wholesale to the model.

The agent tries to escalate

Commands run as an unprivileged user with no sudo. The cloud metadata endpoint is blocked, so credential-stealing patterns that work against container platforms find nothing. Outbound SMTP is blocked, which removes the most common abuse path when an agent is talked into acting on someone else's behalf.

Assume the sandbox is hostile

The useful mental model is not "the agent's computer" but "a computer an attacker may already control". Anything you put inside its environment — an API key, a token, a database URL — should be something you are willing to see published, because a successful prompt injection can exfiltrate it over the sandbox's ordinary outbound network connection.

In practice that means scoping credentials to the task: a read-only deploy key rather than an account token, a short-lived signed URL rather than long-lived storage credentials, a test database rather than production. Pass them through the env field of the specific exec call that needs them instead of baking them into the sandbox at creation time, so their exposure lasts one command instead of the whole run. The isolation boundary protects you from the code; it does not protect the secrets you hand to the code.

What the boundary actually is

Each sandbox is a full virtual machine with its own kernel, filesystem and network stack, not a container sharing a kernel with other tenants. A kernel bug inside the sandbox stops at the hypervisor rather than reaching a neighbour's workload, and no machine is ever handed to a second account — when a sandbox is destroyed, the machine is destroyed. Details are on the security page, and the behaviour under load, including how startup is measured on the warm and cold paths, is documented in limits.

Questions

Should each agent run get its own sandbox, or should one sandbox be shared?

One sandbox per run is the default worth starting from. It makes runs independent: a run that corrupts its own virtualenv, fills its disk or leaves a background process behind cannot affect the next one, and you never have to write cleanup code that undoes an arbitrary sequence of shell commands. Share a sandbox across runs only when the setup cost genuinely dominates — a large repository clone plus a slow dependency install — and accept that you are now responsible for resetting state between runs.

How does the agent get files in and out?

Through the same exec endpoint. Getting files in usually means git clone, a curl of a signed URL, or a heredoc that writes the file from the command string. Getting results out means printing them to stdout, or uploading to your own object storage from inside the sandbox. Anything left on the sandbox disk when it is destroyed is gone, so treat stdout and your own storage as the only durable channels.

What stops a runaway agent from spending my whole balance?

Three things. Sandboxes carry a timeout — 15 minutes by default, 24 hours maximum — and are destroyed when it expires whether or not your agent remembered to call DELETE. Billing is drawn from prepaid credit, so the maximum loss is the credit on the account. And there is a cap of 20 concurrent sandboxes per account, so a loop that creates sandboxes without destroying them fails fast instead of scaling up.

Can the agent install its own dependencies?

Yes. Python 3, Node.js 22, git, curl and build-essential are already there; anything else the agent can install with apt-get or pip as it would on any Linux box. It runs as an unprivileged user without sudo, so system-wide installs need the package manager path your image already permits — in practice agents install into a virtualenv or with pip install --user, which is what you want anyway.

Does the agent need an SDK, or can it call the API itself?

Either. The REST API is a handful of endpoints — create, exec, read and write files, extend, destroy — and works from any HTTP client. If the agent speaks MCP, point it at the MCP server and sandbox creation, command execution and teardown become ordinary tool calls with no glue code on your side. There are no published SDK packages yet.

Give your agent a machine

Sign in, create an API key, and let the run happen somewhere disposable. $${SIGNUP_BONUS} of runtime credit on signup — about ${SIGNUP_BONUS_HOURS} sandbox-hours — and no card required.