..

LangSmith Sandbox

Sandboxed code execution for LangSmith. Run untrusted code safely in isolated containers.

⚠️ Warning: This module is experimental. Features and APIs may change, and breaking changes are expected as we iterate.

Quick Start

from langsmith.sandbox import SandboxClient

# Client uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY from environment
client = SandboxClient()

# First, build a snapshot (defines the container image and root filesystem)
snapshot = client.create_snapshot(
    "python-snapshot",
    docker_image="python:3.12-slim",
    fs_capacity_bytes=4 * 1024**3,  # 4 GB
)

# Now create a sandbox from the snapshot and run code
with client.sandbox(snapshot_id=snapshot.id) as sb:
    result = sb.run("python -c 'print(2 + 2)'")
    print(result.stdout)  # "4\n"
    print(result.success)  # True

# Or create a sandbox to keep
sb = client.create_sandbox(snapshot_id=snapshot.id)
result = sb.run("python -c 'print(2 + 2)'")
client.delete_sandbox(sb.name)  # Don't forget to clean up when done

# Or use an existing sandbox by name
sb = client.get_sandbox(name="your-sandbox")
result = sb.run("python -c 'print(2 + 2)'")

The examples below assume snapshot_id is bound to a snapshot UUID obtained from client.create_snapshot(...) (or client.list_snapshots() / client.get_snapshot(...)). You only need to build a snapshot once; many sandboxes can be created from the same snapshot_id.

Installation

The sandbox module works out of the box for basic command execution (HTTP). For real-time output (streaming, callbacks, and timeout=0), install the optional dependency:

pip install 'langsmith[sandbox]'

This pulls in the websockets package. Without it, sb.run() falls back to HTTP automatically.

Configuration

The client automatically uses LangSmith environment variables:

from langsmith.sandbox import SandboxClient

# Uses LANGSMITH_ENDPOINT and LANGSMITH_API_KEY
client = SandboxClient()

# Or configure explicitly
client = SandboxClient(
    api_endpoint="https://api.smith.langchain.com/v2/sandboxes",
    api_key="your-api-key",
    timeout=30.0,
)

Running Commands

with client.sandbox(snapshot_id=snapshot_id) as sb:
    # Run a command
    result = sb.run("echo 'Hello, World!'")

    print(result.stdout)     # "Hello, World!\n"
    print(result.stderr)     # ""
    print(result.exit_code)  # 0
    print(result.success)    # True

    # Commands that fail return non-zero exit codes
    result = sb.run("exit 1")
    print(result.success)    # False
    print(result.exit_code)  # 1

Streaming Output

For long-running commands, you can stream output in real time. This requires the websockets package (pip install 'langsmith[sandbox]').

Callbacks

The simplest way to get real-time output. Blocks until the command completes.

import sys

with client.sandbox(snapshot_id=snapshot_id) as sb:
    result = sb.run(
        "make build",
        timeout=600,
        on_stdout=lambda s: print(s, end=""),
        on_stderr=lambda s: print(s, end="", file=sys.stderr),
    )
    print(f"\nBuild {'succeeded' if result.success else 'failed'}")

Streaming with CommandHandle

For full control — access to the process handle, stream identity, kill, and reconnection.

with client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = sb.run("make build", timeout=600, wait=False)

    print(f"Command ID: {handle.command_id}")

    for chunk in handle:
        prefix = "OUT" if chunk.stream == "stdout" else "ERR"
        print(f"[{prefix}] {chunk.data}", end="")

    result = handle.result
    print(f"\nExit code: {result.exit_code}")

Killing a Running Command

import threading
import time

with client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = sb.run("sleep 3600", timeout=7200, wait=False)

    # Kill after 10 seconds from another thread
    def kill_after(h, seconds):
        time.sleep(seconds)
        h.kill()

    threading.Thread(target=kill_after, args=(handle, 10)).start()

    for chunk in handle:
        print(chunk.data, end="")

    result = handle.result
    print(f"Exit code: {result.exit_code}")  # non-zero (killed)

Sending Stdin Input

with client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = sb.run(
        "python -c 'name = input(\"Name: \"); print(f\"Hello {name}\")'",
        timeout=30,
        wait=False,
    )

    for chunk in handle:
        if "Name:" in chunk.data:
            handle.send_input("World\n")
        print(chunk.data, end="")

    result = handle.result

Auto-Reconnect

CommandHandle (returned by sb.run(wait=False)) automatically reconnects on transient disconnects — hot-reloads, network blips, etc. No user code needed:

with client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = sb.run("make build", timeout=600, wait=False)

    # Auto-reconnects on transient errors (hot-reload, network blips)
    for chunk in handle:
        print(chunk.data, end="")

    result = handle.result

For manual reconnection across process restarts:

with client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = sb.run("make build", timeout=600, wait=False)
    command_id = handle.command_id

    # ... later, possibly in a different process ...

    handle = sb.reconnect(command_id)
    for chunk in handle:
        print(chunk.data, end="")
    result = handle.result

No Timeout (timeout=0)

With WebSocket enabled, you can set timeout=0 to let a command run indefinitely with no server-side deadline. This works with both wait=False and callbacks. Useful for long-lived processes like dev servers, file watchers, or background tasks that you control via kill().

with client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = sb.run("python server.py", timeout=0, wait=False)

    for chunk in handle:
        print(chunk.data, end="")
        if "Ready" in chunk.data:
            break  # server is up, do other work

    handle.kill()  # stop when done

Note: timeout=0 requires WebSocket support (pip install 'langsmith[sandbox]'). Without WebSocket, run() falls back to HTTP which has its own request-level timeout.

Command Lifecycle & TTL

The sandbox daemon automatically manages command session lifecycles with two timeout mechanisms:

Session TTL (finished commands)

After a command finishes (exits), its session remains in memory for a TTL period. During this window you can still reconnect to retrieve output. After the TTL expires, the session is cleaned up and reconnect() will raise an error.

with client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = sb.run("make build", wait=False)
    command_id = handle.command_id

    # Even after the command finishes, you can reconnect within the TTL window
    handle = sb.reconnect(command_id)
    result = handle.result
    print(result.stdout)

    # After TTL expires, reconnect raises SandboxOperationError

Idle Timeout (running commands)

Running commands with no connected clients are killed after an idle timeout (default: 5 minutes). The idle timer resets each time a client connects. This prevents orphaned long-running processes from consuming resources indefinitely.

You can set a per-command idle timeout via the idle_timeout parameter. Set to -1 for no idle timeout (the command runs indefinitely until explicitly killed or it exits on its own).

with client.sandbox(snapshot_id=snapshot_id) as sb:
    # Start a long-running command with a 30-minute idle timeout
    handle = sb.run(
        "python server.py",
        timeout=0,
        idle_timeout=1800,
        wait=False,
    )

    # As long as a client is connected (iterating), the idle timer is paused
    for chunk in handle:
        print(chunk.data, end="")
        if "Ready" in chunk.data:
            break

    # After disconnecting, the idle timer starts
    # If no client reconnects within idle_timeout seconds, the process is killed

Kill on Disconnect

By default, commands continue running after a client disconnects and can be reconnected to later. Set kill_on_disconnect=True to kill the command immediately when the last client disconnects:

with client.sandbox(snapshot_id=snapshot_id) as sb:
    # Command is killed as soon as the client disconnects
    handle = sb.run(
        "python server.py",
        kill_on_disconnect=True,
        wait=False,
    )

    for chunk in handle:
        print(chunk.data, end="")
        if "Ready" in chunk.data:
            break
    # Command is killed here when iteration stops and the WS disconnects

Combining Lifecycle Options

All lifecycle parameters can be combined:

with client.sandbox(snapshot_id=snapshot_id) as sb:
    # Long-running task: 30-min idle timeout, 1-hour session TTL
    handle = sb.run(
        "python train.py",
        timeout=0,              # No command timeout
        idle_timeout=1800,      # Kill after 30min with no clients
        ttl_seconds=3600,       # Keep session for 1 hour after exit
        wait=False,
    )

    # Fire-and-forget: no idle timeout, infinite TTL
    handle = sb.run(
        "python background_job.py",
        timeout=0,
        idle_timeout=-1,        # Never kill due to idle
        ttl_seconds=-1,         # Keep session forever
        wait=False,
    )

PTY (Pseudo-Terminal)

Set pty=True to allocate a pseudo-terminal for the command. This is useful for interactive programs and commands that detect terminal capabilities:

with client.sandbox(snapshot_id=snapshot_id) as sb:
    # Run an interactive Python REPL with PTY
    handle = sb.run("python", pty=True, wait=False)

    for chunk in handle:
        if ">>>" in chunk.data:
            handle.send_input("print('hello')\n")
            break

    for chunk in handle:
        if ">>>" in chunk.data:
            handle.send_input("exit()\n")
            break

    result = handle.result

    # Commands that require a TTY
    result = sb.run("top -b -n 1", pty=True)

Note: PTY mode merges stdout and stderr into a single stream (stdout). Only use PTY when the command requires it — most commands work fine without it.

File Operations

Read and write files in the sandbox:

with client.sandbox(snapshot_id=snapshot_id) as sb:
    # Write a file
    sb.write("/app/script.py", "print('Hello from file!')")

    # Run the script
    result = sb.run("python /app/script.py")
    print(result.stdout)  # "Hello from file!\n"

    # Read a file (returns bytes)
    content = sb.read("/app/script.py")
    print(content.decode())  # "print('Hello from file!')"

    # Write binary files
    sb.write("/app/data.bin", b"\x00\x01\x02\x03")

TCP Tunnel

Access any TCP service running inside a sandbox (databases, Redis, HTTP servers, etc.) as if it were running on your local machine. The tunnel opens a local TCP port and forwards connections through a multiplexed WebSocket to the target port inside the sandbox.

Requires the websockets package (pip install 'langsmith[sandbox]').

Basic Usage — PostgreSQL

Build a snapshot from the postgres:16 image. The entrypoint initializes and starts Postgres automatically:

import psycopg2

postgres_snapshot = client.create_snapshot(
    "postgres-snapshot",
    docker_image="postgres:16",
    fs_capacity_bytes=4 * 1024**3,
)

sb = client.create_sandbox(snapshot_id=postgres_snapshot.id)
pg_handle = sb.run(
    "POSTGRES_HOST_AUTH_METHOD=trust docker-entrypoint.sh postgres",
    timeout=0,
    wait=False,
)
import time; time.sleep(6)  # wait for Postgres to initialize and start

try:
    with sb.tunnel(remote_port=5432, local_port=25432) as t:
        conn = psycopg2.connect(
            host="127.0.0.1",
            port=t.local_port,
            user="postgres",
        )
        cursor = conn.cursor()
        cursor.execute("SELECT version()")
        print(cursor.fetchone())
        conn.close()
finally:
    pg_handle.kill()
    client.delete_sandbox(sb.name)

Basic Usage — Redis

Build a snapshot from the redis:7 image. Redis self-daemonizes:

import redis

redis_snapshot = client.create_snapshot(
    "redis-snapshot",
    docker_image="redis:7",
    fs_capacity_bytes=2 * 1024**3,
)

with client.sandbox(snapshot_id=redis_snapshot.id) as sb:
    sb.run("redis-server --daemonize yes", timeout=10)

    with sb.tunnel(remote_port=6379, local_port=26379) as t:
        r = redis.Redis(host="127.0.0.1", port=t.local_port)
        r.set("key", "value")
        print(r.get("key"))  # b"value"

HTTP Services

Works with any TCP service. Start long-running services with wait=False and timeout=0 so they stay alive across commands:

sb = client.create_sandbox(snapshot_id=snapshot_id)
http_handle = sb.run("python3 -m http.server 3000", timeout=0, wait=False)
import time; time.sleep(2)

try:
    with sb.tunnel(remote_port=3000, local_port=13000) as t:
        import urllib.request
        resp = urllib.request.urlopen(f"http://127.0.0.1:{t.local_port}/")
        print(resp.status)  # 200
finally:
    http_handle.kill()
    client.delete_sandbox(sb.name)

Multiple Tunnels

Open several tunnels simultaneously to different services:

http_handle2 = sb.run("python3 -m http.server 3001", timeout=0, wait=False)
import time; time.sleep(1)

with sb.tunnel(remote_port=3000, local_port=23000) as t1, \
     sb.tunnel(remote_port=3001, local_port=23001) as t2:
    resp1 = urllib.request.urlopen(f"http://127.0.0.1:{t1.local_port}/")
    resp2 = urllib.request.urlopen(f"http://127.0.0.1:{t2.local_port}/")

http_handle2.kill()

Explicit Lifecycle

For notebooks or long-lived sessions where a context manager isn't convenient:

t = sb.tunnel(remote_port=3000, local_port=23002)

print(t.local_port)
# ... use the tunnel as long as needed ...

t.close()

Async Usage

async with await client.sandbox(snapshot_id=snapshot_id) as sb:
    async with await sb.tunnel(remote_port=5432) as t:
        conn = await asyncpg.connect(host="127.0.0.1", port=t.local_port)

Service URLs

Access HTTP services running inside a sandbox without opening a TCP tunnel. service() returns a ServiceURL object with a short-lived JWT that auto-refreshes transparently. Built-in HTTP helpers inject the auth header for you.

Basic Usage

with client.sandbox(snapshot_id=snapshot_id) as sb:
    # Start a web server inside the sandbox
    handle = sb.run("python -m http.server 3000", timeout=0, wait=False)
    import time; time.sleep(2)

    # Get a service URL for port 3000
    svc = sb.service(port=3000)

    # Make requests — token is injected automatically
    resp = svc.get("/")
    print(resp.status_code)  # 200

    # POST with JSON body
    resp = svc.post("/api/data", json={"key": "value"})

    # Access the raw token or URLs directly
    print(svc.token)        # JWT (auto-refreshes near expiry)
    print(svc.service_url)  # base URL for programmatic access
    print(svc.browser_url)  # URL that sets a cookie in a browser

    handle.kill()

Custom Token TTL

Tokens default to 10 minutes. Set expires_in_seconds for longer or shorter lifetimes (1 second to 24 hours):

# Token valid for 1 hour
svc = sb.service(port=3000, expires_in_seconds=3600)

Auto-Refresh

The ServiceURL object automatically refreshes its token before it expires. You never need to worry about token rotation — just keep using the object:

svc = sb.service(port=3000, expires_in_seconds=60)

# Even after 60 seconds, this still works — token refreshes transparently
resp = svc.get("/api/status")

Async Usage

async with await client.sandbox(snapshot_id=snapshot_id) as sb:
    svc = await sb.service(port=3000)

    # Async HTTP helpers
    resp = await svc.get("/api/data")

    # Async accessors for auto-refreshing properties
    token = await svc.get_token()
    url = await svc.get_service_url()

Snapshots

Snapshots are the starting point for every sandbox. They're built from Docker images or captured from running sandboxes, and many sandboxes can share the same snapshot.

Build a Snapshot from a Docker Image

from langsmith.sandbox import SandboxClient

client = SandboxClient()

# Build a snapshot — blocks until ready (default timeout=60s)
snapshot = client.create_snapshot(
    "my-python-env",
    docker_image="python:3.12-slim",
    fs_capacity_bytes=4 * 1024**3,  # 4 GB
)

# Create a sandbox from the snapshot (by ID)
with client.sandbox(snapshot_id=snapshot.id) as sb:
    result = sb.run("python --version")
    print(result.stdout)

# Or resolve by snapshot name — the server looks up the snapshot owned by
# your tenant and boots from it. Exactly one of snapshot_id or snapshot_name
# must be provided.
with client.sandbox(snapshot_name="my-python-env") as sb:
    result = sb.run("python --version")
    print(result.stdout)

Capture a Running Sandbox

Install packages or prepare files on a running sandbox, then capture the result as a reusable snapshot. The returned snapshot has source_sandbox_id set to the sandbox it was captured from, and can be used as the snapshot_id for any later create_sandbox / sandbox(...) call.

sb = client.create_sandbox(snapshot_id=base_snapshot_id, name="setup-box")
sb.run("pip install numpy pandas scikit-learn", timeout=180)
sb.write("/opt/config.yaml", "model: gpt-5\n")

# Either form works; the instance method just forwards to the client.
snapshot = sb.capture_snapshot("ml-ready", timeout=300)
# snapshot = client.capture_snapshot(sb.name, "ml-ready", timeout=300)
print(snapshot.id, snapshot.source_sandbox_id)

sb.delete()

# Later: spin up sandboxes from the captured snapshot
with client.sandbox(snapshot_id=snapshot.id) as sb:
    sb.run("python -c 'import numpy; print(numpy.__version__)'")
    assert sb.read("/opt/config.yaml") == b"model: gpt-5\n"

Note: capture_snapshot preserves only the persistent filesystem. Installed packages (under /usr/local, /root, /opt, the home directory, etc.) and files you wrote to those paths are kept. Running processes, open sockets, in-memory state, and anything under /tmp (which is a tmpfs) are not carried over — restart the processes you need in the new sandbox.

Snapshot CRUD

# List snapshots (server paginates with a default page size of 50)
snapshots = client.list_snapshots()

# Filter and paginate — all three kwargs are optional and independent.
# `limit` must be between 1 and 500 (inclusive); `offset` must be >= 0.
snapshots = client.list_snapshots(
    name_contains="python",  # case-insensitive substring match on name
    limit=100,
    offset=0,
)

# Get a snapshot by ID
snapshot = client.get_snapshot("550e8400-...")

# Delete a snapshot
client.delete_snapshot("550e8400-...")

# Build with longer timeout for large images
snapshot = client.create_snapshot(
    "heavy-image",
    docker_image="nvidia/cuda:12.0-devel-ubuntu22.04",
    fs_capacity_bytes=16 * 1024**3,
    timeout=600,
)

Start / Stop

Snapshot-based sandboxes can be stopped and restarted. The sandbox files are preserved across stop/start cycles.

sb = client.create_sandbox(snapshot_id=snapshot.id, name="my-vm")
sb.run("echo 'hello' > /tmp/state.txt")

# Stop the sandbox (preserves sandbox files)
sb.stop()

# Later: restart it
sb.start()  # blocks until ready (default timeout=120s)

result = sb.run("cat /tmp/state.txt")
assert result.stdout.strip() == "hello"

You can also use the client methods directly:

client.stop_sandbox("my-vm")
sandbox = client.start_sandbox("my-vm")

Sandbox Lifetime & TTL

Sandboxes follow a two-stage retention model anchored to idle activity and the stopped state — there is no wall-clock "max lifetime" TTL:

  • idle_ttl_seconds — Idle timeout. The launcher stops the sandbox after this many seconds of inactivity (any command execution or file I/O resets the timer). When omitted at creation, the server applies a default of 600 seconds (10 minutes); pass 0 explicitly to disable the idle stop and keep the sandbox running indefinitely.
  • delete_after_stop_seconds — Stop-anchored deletion. Once a sandbox enters the stopped state (either via the idle timer above or an explicit stop_sandbox call), this timer starts. After the deadline passes, the sandbox row and its filesystem clone are permanently deleted by a server-side sweep. Pass 0 to disable stop-anchored deletion (manual cleanup required); when omitted, the server applies its configured default (typically 14 days).

Both values must be multiples of 60 (minute-resolution). The lifecycle is:

running ──(idle for idle_ttl_seconds)──▶ stopped ──(delete_after_stop_seconds)──▶ deleted
# Default retention (server defaults: 10 min idle stop, 14 day delete)
with client.sandbox(snapshot_id=snapshot_id) as sb:
    result = sb.run("echo hello")

# Aggressive: stop after 5 min idle, delete 1 hour after stop
sb = client.create_sandbox(
    snapshot_id=snapshot_id,
    idle_ttl_seconds=300,
    delete_after_stop_seconds=3600,
)

# Long-running: never auto-stop, delete 7 days after manual stop
sb = client.create_sandbox(
    snapshot_id=snapshot_id,
    idle_ttl_seconds=0,
    delete_after_stop_seconds=604800,
)

# Inspect retention settings
print(sb.idle_ttl_seconds)            # e.g. 300
print(sb.delete_after_stop_seconds)   # e.g. 3600
print(sb.stopped_at)                  # None while running, ISO timestamp once stopped

Updating retention on existing sandboxes

You can update either retention setting on a running or stopped sandbox. Updating delete_after_stop_seconds on an already-stopped sandbox shifts its deletion deadline (stopped_at + delete_after_stop_seconds):

# Extend the idle stop to 30 minutes
sb = client.update_sandbox("my-sandbox", idle_ttl_seconds=1800)

# Push the deletion deadline out to 30 days after stop
sb = client.update_sandbox("my-sandbox", delete_after_stop_seconds=2592000)

# Disable both — sandbox keeps running and never auto-deletes
sb = client.update_sandbox(
    "my-sandbox",
    idle_ttl_seconds=0,
    delete_after_stop_seconds=0,
)

Migration note (alpha): the previous ttl_seconds (hard wall-clock TTL) and expires_at fields were removed. The hard TTL never reliably deleted stopped sandboxes; replace any usage with idle_ttl_seconds for stopping and delete_after_stop_seconds for deletion.

Reusing Existing Sandboxes

Get a sandbox that's already running:

# Create a sandbox (requires explicit cleanup)
sb = client.create_sandbox(snapshot_id=snapshot_id)
print(sb.name)  # e.g., "sandbox-abc123"

# Later, get the same sandbox
sb = client.get_sandbox("sandbox-abc123")
result = sb.run("echo 'Still running!'")

# Clean up when done
client.delete_sandbox("sandbox-abc123")

Async Sandbox Creation

By default, create_sandbox() blocks until the sandbox is ready. For non-blocking creation, pass wait_for_ready=False:

# Returns immediately with status="provisioning"
sb = client.create_sandbox(snapshot_id=snapshot_id, wait_for_ready=False)
print(sb.status)  # "provisioning"

# Poll until ready using the lightweight status endpoint
sb = client.wait_for_sandbox(sb.name, timeout=120, poll_interval=1.0)
print(sb.status)  # "ready"

# Now the sandbox is usable
result = sb.run("echo hello")

You can also poll manually for more control:

sb = client.create_sandbox(snapshot_id=snapshot_id, wait_for_ready=False)

while True:
    status = client.get_sandbox_status(sb.name)
    if status.status == "ready":
        sb = client.get_sandbox(sb.name)
        break
    if status.status == "failed":
        print(f"Failed: {status.status_message}")
        break
    time.sleep(1)

Note: Operations like run(), write(), and read() will raise SandboxNotReadyError if called on a sandbox that isn't ready yet.

Async Support

Full async support for all operations:

from langsmith.sandbox import AsyncSandboxClient

async def main():
    async with AsyncSandboxClient() as client:
        # Build a snapshot first
        snapshot = await client.create_snapshot(
            "async-python",
            docker_image="python:3.12-slim",
            fs_capacity_bytes=4 * 1024**3,
        )

        # Use the snapshot
        async with await client.sandbox(snapshot_id=snapshot.id) as sb:
            result = await sb.run("python -c 'print(1 + 1)'")
            print(result.stdout)  # "2\n"

            await sb.write("/app/test.txt", "async content")
            content = await sb.read("/app/test.txt")
            print(content.decode())

Async Streaming

async with await client.sandbox(snapshot_id=snapshot_id) as sb:
    handle = await sb.run("make build", timeout=600, wait=False)

    async for chunk in handle:
        print(chunk.data, end="")

    result = await handle.result

Error Handling

The module provides type-based exceptions with a resource_type attribute for specific handling:

from langsmith.sandbox import (
    SandboxClientError,       # Base exception for all sandbox errors
    ResourceCreationError,    # Resource provisioning failed (check resource_type, error_type)
    ResourceNotFoundError,    # Resource doesn't exist (check resource_type)
    ResourceTimeoutError,     # Operation timed out (check resource_type)
    SandboxNotReadyError,     # Sandbox not ready for operations yet
    SandboxConnectionError,   # Network/WebSocket error
    CommandTimeoutError,      # Command exceeded its timeout (extends SandboxOperationError)
    QuotaExceededError,       # Quota limit reached
    TunnelError,              # Base for tunnel errors
    TunnelPortNotAllowedError,       # Port blocked by daemon allowlist
    TunnelConnectionRefusedError,    # Nothing listening on remote port
    TunnelUnsupportedVersionError,   # Client/daemon protocol mismatch
)

try:
    with client.sandbox(snapshot_id=snapshot_id) as sb:
        result = sb.run("sleep 999", timeout=10)
except CommandTimeoutError as e:
    print(f"Command timed out: {e}")
except ResourceCreationError as e:
    print(f"{e.resource_type} creation failed: {e}")
except ResourceNotFoundError as e:
    print(f"{e.resource_type} not found: {e}")
except ResourceTimeoutError as e:
    print(f"Timeout waiting for {e.resource_type}: {e}")
except SandboxConnectionError as e:
    print(f"Connection error: {e}")
except SandboxClientError as e:
    print(f"Error: {e}")

API Reference

SandboxClient

Method Description
sandbox(snapshot_id=None, *, snapshot_name=None, idle_ttl_seconds=None, delete_after_stop_seconds=None, ...) Create a sandbox (auto-deleted on context exit). Exactly one of snapshot_id / snapshot_name must be set.
create_sandbox(snapshot_id=None, *, snapshot_name=None, wait_for_ready=True, ...) Create a sandbox (requires explicit delete). Exactly one of snapshot_id / snapshot_name must be set.
get_sandbox(name) Get an existing sandbox by name
get_sandbox_status(name) Get lightweight provisioning status (ResourceStatus)
wait_for_sandbox(name, *, timeout=120, poll_interval=1.0) Poll until sandbox is ready or failed
service(name, port, *, expires_in_seconds=600) Get a ServiceURL for an HTTP service on the given port
list_sandboxes() List all sandboxes
update_sandbox(name, *, new_name=None, idle_ttl_seconds=None, delete_after_stop_seconds=None) Update a sandbox's name or retention settings
delete_sandbox(name) Delete a sandbox
start_sandbox(name, *, timeout=120) Start a stopped sandbox, poll until ready
stop_sandbox(name) Stop a running sandbox (preserves sandbox files)
create_snapshot(name, docker_image, fs_capacity_bytes, *, timeout=60) Build a snapshot from a Docker image
capture_snapshot(sandbox_name, name, *, timeout=60) Capture a snapshot from a running sandbox
get_snapshot(snapshot_id) Get a snapshot by ID
list_snapshots(*, name_contains=None, limit=None, offset=None) List a page of snapshots (server paginates, default limit 50, max 500; name_contains is a case-insensitive substring match)
delete_snapshot(snapshot_id) Delete a snapshot
wait_for_snapshot(snapshot_id, *, timeout=300) Poll until snapshot is ready or failed

Sandbox

Property Description
name Display name
snapshot_id Snapshot ID used to create this sandbox
status Lifecycle status: "provisioning", "ready", "failed", or "stopped"
status_message Human-readable details when status is "failed", None otherwise
dataplane_url URL for runtime operations (only functional when status is "ready")
id Unique identifier (UUID)
idle_ttl_seconds Idle timeout in seconds before the launcher stops the sandbox (0 means disabled, None means not set). New sandboxes get a server-side default of 600 (10 minutes) when not explicitly provided.
delete_after_stop_seconds Seconds after entering stopped before the sandbox and its filesystem clone are permanently deleted (0 means disabled, None means server default).
stopped_at ISO 8601 timestamp when the sandbox transitioned to stopped, or None while running.
Method Description
run(command, *, timeout=60, on_stdout=None, on_stderr=None, idle_timeout=300, kill_on_disconnect=False, ttl_seconds=600, pty=False, wait=True) Execute a shell command. Returns ExecutionResult or CommandHandle (when wait=False).
reconnect(command_id, *, stdout_offset=0, stderr_offset=0) Reconnect to a running command. Returns CommandHandle.
write(path, content) Write file (str or bytes)
read(path) Read file (returns bytes)
tunnel(remote_port, *, local_port=0) Open a TCP tunnel. Returns Tunnel (context manager).
service(port, *, expires_in_seconds=600) Get a ServiceURL for an HTTP service. Auto-refreshes token.
start(*, timeout=120) Start a stopped sandbox and wait until ready.
stop() Stop a running sandbox (preserves sandbox files for later restart).
delete() Delete this sandbox.
capture_snapshot(name, *, timeout=60) Capture a snapshot from this sandbox.

ExecutionResult

Property Description
stdout Standard output (str)
stderr Standard error (str)
exit_code Exit code (int)
success True if exit_code == 0

ResourceStatus

Returned by client.get_sandbox_status().

Property Description
status Lifecycle status: "provisioning", "ready", or "failed"
status_message Human-readable details when "failed", None otherwise

CommandHandle

Returned by sb.run(wait=False). Iterable, yielding OutputChunk objects.

Property / Method Description
command_id Server-assigned command ID
pid Process ID on the sandbox
result Final ExecutionResult (blocks until complete)
kill() Send SIGKILL to the running command
send_input(data) Write string data to the command's stdin
reconnect() Reconnect from last known offsets

OutputChunk

Property Description
stream "stdout" or "stderr"
data Text content of this chunk (str)
offset Byte offset within the stream (int)

Tunnel

Returned by sb.tunnel(remote_port). Context manager that opens a local TCP listener forwarding to a port inside the sandbox.

Property / Method Description
local_port Local port the tunnel is listening on (int)
remote_port Target port inside the sandbox (int)
close() Shut down the tunnel and all connections

ServiceURL

Returned by sb.service(port). Holds a short-lived JWT for accessing an HTTP service in the sandbox. Properties auto-refresh the token near expiry.

Property Description
token Raw JWT for programmatic use (auto-refreshes)
service_url Base URL for programmatic HTTP access (auto-refreshes)
browser_url URL that exchanges the JWT for a cookie in a browser (auto-refreshes)
expires_at ISO 8601 expiration timestamp (auto-refreshes)
Method Description
request(method, path="/", **kwargs) Make an HTTP request with auth header injected. Returns httpx.Response.
get(path="/", **kwargs) HTTP GET
post(path="/", **kwargs) HTTP POST
put(path="/", **kwargs) HTTP PUT
patch(path="/", **kwargs) HTTP PATCH
delete(path="/", **kwargs) HTTP DELETE

AsyncServiceURL is the async variant. Use await svc.get_token(), await svc.get_service_url(), etc. for auto-refreshing access, and await svc.get(path) for async HTTP helpers.