Leap0

Sandboxes

What a sandbox is, how it behaves, and which endpoints you use to manage and invoke it.

A sandbox is an isolated runtime environment. Create one from a template, invoke it over HTTP or WebSocket, then delete it when you are done.

Why use sandboxes?

Sandboxes exist for security. They let agents execute arbitrary code, access files, and use the network without compromising your credentials, local files, or host system. This isolation is essential when agents run autonomously.

Isolation and security

Every sandbox runs in its own Firecracker microVM:

  • Firecracker is launched with the Jailer for additional process and filesystem isolation.
  • Sandboxes run on the Linux 6.1 LTS kernel.
  • Each sandbox gets dedicated network boundaries with egress policy enforcement. Control outbound access with network_policy.

See Firewall for egress configuration details.

Lifecycle

A sandbox can have several different states. Each state reflects the current status of your sandbox.

  • starting: The sandbox is booting and preparing its runtime environment before it can accept requests.
  • running: The sandbox is active and ready to handle HTTP, WebSocket, filesystem, process, and other tool requests.
  • paused: The sandbox's memory and disk state have been saved, and compute is no longer running. It resumes automatically when it receives a request.
  • unpausing: The sandbox is being restored from its paused state so it can return to running.
  • stopping: The sandbox is shutting down while preserving disk changes, but not in-memory execution state.
  • stopped: The sandbox is fully off, but its disk changes are preserved. Call start to boot it again.
  • deleting: The sandbox is being cleaned up and permanently removed.
  • deleted: The sandbox has been permanently removed and is no longer available.

Create a sandbox

Set auto_pause to true if you want Leap0 to pause the sandbox when its timeout is reached.

from leap0 import Leap0Client, NetworkPolicyMode

client = Leap0Client()
sandbox = client.sandboxes.create(
    vcpu=2,
    memory=2048,
    timeout=30,
    auto_pause=True,
    env_vars={
        "NODE_ENV": "production",
    },
    network_policy={
        "mode": NetworkPolicyMode.CUSTOM,
        "allow_cidrs": ["1.2.3.4/32"],
    },
)

print(sandbox.id, sandbox.state)
import { Leap0Client, NetworkPolicyMode } from "leap0";

const client = new Leap0Client();
const sandbox = await client.sandboxes.create({
  vcpu: 2,
  memory: 2048,
  timeout: 30,
  autoPause: true,
  envVars: {
    NODE_ENV: "production",
  },
  networkPolicy: {
    mode: NetworkPolicyMode.CUSTOM,
    allowCidrs: ["1.2.3.4/32"],
  },
});

console.log(sandbox.id, sandbox.state);
await client.close();

Get a sandbox

Returns the sandbox status, resource configuration, and current state.

from leap0 import Leap0Client

client = Leap0Client()
sandbox = client.sandboxes.create()
print(sandbox.id, sandbox.state, sandbox.vcpu, sandbox.memory)
import { Leap0Client } from "leap0";

const client = new Leap0Client();
const sandbox = await client.sandboxes.get("<sandbox_id>");

console.log(sandbox.id, sandbox.state, sandbox.vcpu, sandbox.memory);
await client.close();

Invoke a sandbox

Send any HTTP or WebSocket request directly to the sandbox host. Whatever your app serves inside the sandbox is reachable.

By default, traffic is routed to the port defined by the template image's PORT environment variable. If your app serves on a different port, append -<port> to the sandbox subdomain.

Port routing

Only one port can be reached per request. To target a specific port, append -<port> to the sandbox subdomain (e.g. sbx-abc123-3000). If no port suffix is set, traffic routes to the port from the template's PORT environment variable.

Examples:

  • Default port from image metadata: https://sbx-abc123.sandbox.leap0.dev/health
  • Explicit port routing: https://sbx-abc123-3000.sandbox.leap0.dev/health

HTTP

import os

import httpx
from leap0 import Leap0Client

API_KEY = os.environ["LEAP0_API_KEY"]

client = Leap0Client()
sandbox = client.sandboxes.create()
response = httpx.get(
    sandbox.invoke_url("/<your-path>"),
    headers={"authorization": API_KEY},
)

response.raise_for_status()
print(response.text)
import { Leap0Client } from "leap0";
import WebSocket from "ws";

const apiKey = process.env.LEAP0_API_KEY ?? "<your-api-key>";
const client = new Leap0Client();
const sandbox = await client.sandboxes.get("<sandbox_id>");

const res = await fetch(sandbox.invokeUrl("/<your-path>"), {
  headers: { authorization: apiKey },
});

console.log(await res.text());
await client.close();

Server-Sent Events

import os

import httpx
from leap0 import Leap0Client

API_KEY = os.environ["LEAP0_API_KEY"]

client = Leap0Client()
sandbox = client.sandboxes.create()
with httpx.stream(
    "GET",
    sandbox.invoke_url("/<your-path>"),
    headers={
        "authorization": API_KEY,
        "Accept": "text/event-stream",
    },
) as response:
    response.raise_for_status()
    for line in response.iter_lines():
        if line:
            print(line)
import { Leap0Client } from "leap0";

const apiKey = process.env.LEAP0_API_KEY ?? "<your-api-key>";
const client = new Leap0Client();
const sandbox = await client.sandboxes.get("<sandbox_id>");

// EventSource does not support custom headers.
// Use fetch + ReadableStream to send an Authorization header.
const res = await fetch(sandbox.invokeUrl("/<your-path>"), {
  headers: {
    authorization: apiKey,
    Accept: "text/event-stream",
  },
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    if (line.startsWith("data: ")) {
      console.log(line.slice(6));
    }
  }
}

await client.close();

WebSocket

import asyncio
import os

import websockets
from leap0 import Leap0Client

API_KEY = os.environ["LEAP0_API_KEY"]

client = Leap0Client()
sandbox = client.sandboxes.create()

async def connect() -> None:
    async with websockets.connect(
        sandbox.websocket_url("/<your-path>"),
        additional_headers={"authorization": API_KEY},
    ) as ws:
        await ws.send("hello")
        response = await ws.recv()
        print(response)

asyncio.run(connect())
import { Leap0Client } from "leap0";

const apiKey = process.env.LEAP0_API_KEY ?? "<your-api-key>";
const client = new Leap0Client();
const sandbox = await client.sandboxes.get("<sandbox_id>");

const ws = new WebSocket(sandbox.websocketUrl("/<your-path>"), {
  headers: { authorization: apiKey },
});

await new Promise((resolve, reject) => {
  ws.on("open", () => ws.send("hello"));
  ws.on("message", (data) => {
    console.log(data.toString());
    ws.close();
  });
  ws.on("close", resolve);
  ws.on("error", reject);
});

await client.close();

Pause a sandbox

Pause a running sandbox. The VM state, memory, and disk are captured and uploaded, the sandbox is marked as paused, and the VM is terminated. The sandbox can later be resumed automatically when it receives a request.

from leap0 import Leap0Client

client = Leap0Client()
sandbox = client.sandboxes.create()
sandbox.pause()
print(sandbox.state)
import { Leap0Client } from "leap0";

const client = new Leap0Client();
const sandbox = await client.sandboxes.get("<sandbox_id>");

await sandbox.pause();
console.log(sandbox.state);
await client.close();

Stop a sandbox

Stop a running sandbox when you want to shut compute down but keep filesystem changes for later. Unlike pause, this does not preserve in-memory execution state.

curl -X POST \
  -H "Authorization: Bearer <api-key>" \
  "https://api.leap0.dev/v1/sandbox/<sandbox_id>/stop"

The sandbox moves through stopping and then stopped.

Start a stopped sandbox

Start a sandbox that is currently stopped. Leap0 boots a fresh VM from the original template and reapplies the persisted writable disk state.

curl -X POST \
  -H "Authorization: Bearer <api-key>" \
  "https://api.leap0.dev/v1/sandbox/<sandbox_id>/start"

The response now includes the full updated sandbox metadata, so SDKs do not need to issue a follow-up get after start.

The sandbox returns to running when the request completes.

start is for stopped sandboxes only. You do not need to call it for paused sandboxes.

Delete a sandbox

Running, paused, and stopped sandboxes can all be deleted.

from leap0 import Leap0Client

client = Leap0Client()
sandbox = client.sandboxes.create()
sandbox.delete()
import { Leap0Client } from "leap0";

const client = new Leap0Client();
await client.sandboxes.delete("<sandbox_id>");
await client.close();

Was this page helpful?

On this page