Skip to main content

Sandbox SDK

A Python SDK for interacting with Flash Sandbox over HTTP. It talks to the coordinator (cluster binary) — point host/port (or address) at your coordinator, not at a bare node (the SDK uses the coordinator's /sandboxes API).

The SDK offers two layers of abstraction:

  • Low-level clients (HTTPClient, AsyncHTTPClient) – manage sandboxes via client methods and return Sandbox / AsyncSandbox objects from start_sandbox, so you can call methods directly on the result.
  • High-level wrappers (Sandbox, AsyncSandbox) – bind a single sandbox instance so you never need to pass the ID around.

Installation

pip install flash-sandbox

The package pulls in requests and aiohttp automatically.

Quick Start

HTTPClient.start_sandbox() returns a Sandbox object. You can call methods directly on it, or access the raw ID via .id:

from flash_sandbox import HTTPClient

client = HTTPClient(host="localhost", port=8080)

# start_sandbox returns a Sandbox object.
# No `type` → the cluster picks any available backend (Docker, Kubernetes, …).
# Pass type="docker" (etc.) to force a specific backend.
sandbox = client.start_sandbox(
    image="alpine:latest",
    command=["tail", "-f", "/dev/null"],
    memory_mb=128,
    cpu_cores=0.5,
)
print(f"Sandbox ID: {sandbox.id}")

# Call methods directly on the sandbox — no need to pass an ID
print(f"Status: {sandbox.get_status()}")

result = sandbox.exec_command(["echo", "Hello!"])
print(f"Output: {result.stdout.strip()}")
print(f"Exit: {result.exit_code}, server time: {result.duration_ms:.1f}ms")

# Optional `metadata` is forwarded uninterpreted to the exec trace /
# event log so execs can be classified downstream (see "Classifying
# execs" below). The backend ignores it; it doesn't change execution.
result = sandbox.exec_command(
    ["echo", "verified"],
    metadata={"query_type": "verification", "task_id": 7},
)

py_out = sandbox.run_python("print('Hello Python in Sandbox')")
print(f"Python Output: {py_out.strip()}")

details = sandbox.get_info()
print(f"Scheduled on: {details.node_id} ({details.backend})")

plat_info = sandbox.get_platform_info()
print(f"Platform: {plat_info['system']} {plat_info.get('release', '')}")

metrics = sandbox.get_metrics()
print(f"Memory: {metrics.memory_usage_bytes} / {metrics.memory_limit_bytes}")
print(f"CPU: {metrics.cpu_percent}%")

snap = sandbox.snapshot()
print(f"Snapshot: {snap.snapshot_path}")
sandbox.resume()

sandbox.write_file("/tmp/hello.txt", b"Hello from SDK!")
content = sandbox.read_file("/tmp/hello.txt")
print(f"File Content: {content.decode()}")

sandbox.stop()            # cleanup=True by default: removes the container
# sandbox.stop(cleanup=False)  # stop without removing resources
client.close()

Pre-warm a future batch with a claimable ticket:

ticket = client.demand(
    "trainer",
    "batch-43",
    image="python:3.11-slim",
    backend="libkrun",
    expect=next_batch_at,  # RFC3339 string or timezone-aware datetime
    count=8,
    cpu=2,
    memory=1024,
)

sandbox = client.claim(ticket)
cleanup = client.cancel(ticket)  # cleans up anything still unclaimed

claim(ticket) waits for one prepared sandbox, returns a normal Sandbox, and can be repeated while ticket.remaining_claims is nonzero. Retrying with the same live ticket safely replays an ambiguous claim. Use the lower-level claim_warm_demand(..., claim_id=...) API when the claim ID must survive a client process restart. The async client exposes the same methods with await. Cancelling a ticket does not stop sandboxes that were already claimed.

Use as a context manager for automatic cleanup:

from flash_sandbox import HTTPClient, Sandbox

client = HTTPClient(host="localhost", port=8080)

with Sandbox.create(
    client,
    type="docker",
    image="alpine:latest",
    command=["tail", "-f", "/dev/null"],
) as sandbox:
    result = sandbox.exec_command(["echo", "Hello!"])
    print(result.stdout.strip())

# sandbox.stop() is called automatically on exit
client.close()

You can also wrap an existing sandbox ID:

from flash_sandbox import HTTPClient, Sandbox

client = HTTPClient(host="localhost")
sandbox = Sandbox("existing-sandbox-id", client)
print(sandbox.get_status())

Async usage

AsyncHTTPClient.start_sandbox() returns an AsyncSandbox object. Call await on its methods:

import asyncio
from flash_sandbox import AsyncHTTPClient

async def main():
    client = AsyncHTTPClient(host="localhost", port=8080)

    # start_sandbox returns an AsyncSandbox object
    sandbox = await client.start_sandbox(
        type="docker",
        image="alpine:latest",
        command=["tail", "-f", "/dev/null"],
    )
    print(f"Sandbox ID: {sandbox.id}")

    result = await sandbox.exec_command(["echo", "Hello from Async!"])
    print(f"Output: {result.stdout.strip()}")

    await sandbox.write_file("/tmp/async.txt", b"Async hello!")
    content = await sandbox.read_file("/tmp/async.txt")
    print(f"Async File Content: {content.decode()}")

    await sandbox.stop()
    await client.close()

asyncio.run(main())

Class-based pattern

When the client lives as an instance attribute and sandbox start/stop happen across different methods:

from flash_sandbox import AsyncHTTPClient

class MyAgent:
    def __init__(self, orchestrator_host: str = "localhost"):
        self.sandbox_client = AsyncHTTPClient(host=orchestrator_host, port=8080)

    async def forward(self, prompt: str, metadata: dict):
        # start_sandbox returns an AsyncSandbox object
        sandbox = await self.sandbox_client.start_sandbox(
            type="docker",
            image=metadata["image_name"],
            command=["tail", "-f", "/dev/null"],
        )

        result = await sandbox.exec_command(["echo", "Hello!"])
        print(f"Output: {result.stdout.strip()}")

        platform_info = await sandbox.get_platform_info()
        print(f"Platform: {platform_info['system']} {platform_info.get('release', '')}")

        await sandbox.stop()

    async def close(self):
        await self.sandbox_client.close()

Context manager pattern

Use async with for automatic cleanup:

import asyncio
from flash_sandbox import AsyncHTTPClient, AsyncSandbox

async def main():
    async with AsyncHTTPClient(host="localhost", port=8080) as client:
        async with await AsyncSandbox.create(
            client,
            type="docker",
            image="alpine:latest",
            command=["tail", "-f", "/dev/null"],
        ) as sandbox:
            status = await sandbox.get_status()
            print(f"Status: {status}")

            result = await sandbox.exec_command(["echo", "Hello!"])
            print(f"Output: {result.stdout.strip()}")

        # sandbox.stop() is called automatically

asyncio.run(main())

API Reference

Sandbox / AsyncSandbox (high-level)

The Sandbox class wraps a sandbox ID and a client, exposing all operations as simple method calls. AsyncSandbox is the async equivalent — all I/O methods are coroutines.

Both work with HTTPClient and AsyncHTTPClient.

Construction

# HTTP clients return Sandbox / AsyncSandbox directly from start_sandbox
sandbox = client.start_sandbox(type="docker", image="alpine:latest", ...)
sandbox = await async_client.start_sandbox(type="docker", image="alpine:latest", ...)

# Or wrap an existing sandbox ID (works with any client)
sandbox = Sandbox(sandbox_id, client)
sandbox = AsyncSandbox(sandbox_id, async_client)

# Or use the create() factory (calls start_sandbox, then wraps if needed)
sandbox = Sandbox.create(client, type="docker", image="alpine:latest", ...)
sandbox = await AsyncSandbox.create(async_client, type="docker", image="alpine:latest", ...)

Methods

Method Description
get_status() Return the status string ("running", "stopped", etc.).
get_info() / describe() Return SandboxDetails with status, node/backend placement, image, resources, TTL, and creation metadata when the coordinator provides it.
exec_command(command, *, metadata=None, stream=False) Execute a command. Returns ExecResult with stdout, stderr, exit_code, client elapsed_ms, server duration_ms when available, prepared command, and byte-count helpers. metadata is an optional caller bag forwarded to the exec trace / event log for downstream classification; the backend ignores it.
run_python(code) Execute Python code inside the sandbox. Returns stdout.
run_python_result(code) Execute Python code and return PythonResult with result text plus stdout/stderr/exit metadata when available.
get_platform_info() Get platform information. Returns a dict with keys like system, node, release, etc.
get_metrics() Return point-in-time resource-usage metrics.
read_file(path) Read a file from the sandbox as bytes.
read_file_result(path) Read a file and return ReadFileResult with path, content, size, and optional stat metadata.
write_file(path, content) Write bytes to a path in the sandbox.
write_file_result(path, content) Write bytes and return WriteFileResult with bytes-written acknowledgement.
agent_health_detail() Probe the in-sandbox agent and return status, detail, and latency.
snapshot() Create a snapshot. Returns SnapshotResult with paths, owning snapshot node ID, and size_bytes when available.
resume() Resume a paused / snapshotted sandbox.
resume_result() Resume and return a lifecycle acknowledgement.
stop(cleanup=True) Stop the sandbox. When cleanup=True (default), backing resources are removed. Pass cleanup=False to stop without removing resources.
stop_result(cleanup=True) Stop and return a lifecycle acknowledgement.

Properties

Property Description
id The raw sandbox ID string.
details Cached SandboxDetails from creation or the last get_info() call, when available.

Context manager

Sandbox supports with and AsyncSandbox supports async with. The sandbox is automatically stopped when the block exits:

with Sandbox.create(client, type="docker", image="alpine:latest") as sandbox:
    sandbox.exec_command(["echo", "hi"])
# sandbox.stop(cleanup=True) is called automatically on exit

async with await AsyncSandbox.create(client, type="docker", image="alpine:latest") as sandbox:
    await sandbox.exec_command(["echo", "hi"])
# await sandbox.stop(cleanup=True) is called automatically on exit

Low-level client methods

HTTPClient and AsyncHTTPClient expose the same set of methods. AsyncHTTPClient methods must be awaited.

SandboxID — flexible sandbox identifiers

Every method that accepts a sandbox_id parameter uses the SandboxID type, which is Union[str, Sandbox, AsyncSandbox]. This means you can pass either a plain string ID or a Sandbox / AsyncSandbox object — they are fully interchangeable:

sandbox = client.start_sandbox(type="docker", image="alpine:latest")

# All three forms are equivalent:
client.get_status(sandbox)            # pass the Sandbox object
client.get_status(sandbox.id)         # pass the .id string
client.get_status("sb-abc123")        # pass any string ID

This makes it easy to mix high-level wrapper usage with low-level client calls during migration or when you need both styles in the same codebase.

Method Description
start_sandbox(image, command, memory_mb, cpu_cores, type="", ...) Start a new sandbox. Returns a Sandbox/AsyncSandbox. type is optional — omit it (or pass "auto") to let the cluster pick any available backend; set it ("docker", "kubernetes", …) to force one.
stop_sandbox(sandbox_id, cleanup=True) Stop a sandbox. When cleanup=True (default), backing resources are removed. Pass cleanup=False to keep resources for inspection. Accepts SandboxID.
exec_command(sandbox_id, command, *, metadata=None, stream=False) Execute a command in a sandbox. Returns an object with stdout, stderr, and exit_code. metadata is an optional caller bag forwarded to the exec trace / event log. Accepts SandboxID.
get_status(sandbox_id) Return the status string ("running", "stopped", etc.). Accepts SandboxID.
get_metrics(sandbox_id) Return point-in-time resource-usage metrics (memory, CPU, network, block I/O). Accepts SandboxID.
snapshot_sandbox(sandbox_id) Create a snapshot. Returns a SnapshotResult. Accepts SandboxID.
resume_sandbox(sandbox_id) Resume a paused / snapshotted sandbox. Accepts SandboxID.
run_python(sandbox_id, code) Execute arbitrary Python code inside the sandbox. Returns the stdout output. Accepts SandboxID.
get_platform_info(sandbox_id) Get platform information from the sandbox. Returns a dict with platform data. Accepts SandboxID.
read_file(sandbox_id, path) Read a file from the sandbox as bytes. Accepts SandboxID.
write_file(sandbox_id, path, content) Write bytes to a path in the sandbox. Accepts SandboxID.
close() Release the underlying connection / session.

Constructor options

HTTPClient

HTTPClient(
    host="localhost",       # Coordinator hostname — or a full URL (see below)
    port=8080,              # Coordinator HTTP port (default 8080)
    address=None,           # Full URL, overrides host/port (e.g. "http://proxy:9090/v1/service/sandbox")
    timeout=30.0,           # Request timeout in seconds (None = no timeout)
    session=None,           # Optional requests.Session for custom TLS / auth / retries
    api_key=None,           # Sent as "Authorization: Bearer <key>"; defaults to $FLASH_API_KEY
)

AsyncHTTPClient

AsyncHTTPClient(
    host="localhost",       # Coordinator hostname — or a full URL (see below)
    port=8080,              # Coordinator HTTP port (default 8080)
    address=None,           # Full URL, overrides host/port
    timeout=30.0,           # Request timeout in seconds
    session=None,           # Optional aiohttp.ClientSession
    api_key=None,           # Sent as "Authorization: Bearer <key>"; defaults to $FLASH_API_KEY
)

Connecting with just a URL

The first positional argument accepts a full URL, so a hosted coordinator needs nothing else:

client = HTTPClient("https://sandbox.example.com")
client = HTTPClient("localhost:8080")
client = HTTPClient("http://proxy:9090/v1/service/sandbox")

A value is treated as a URL when it carries a scheme, a path, a bracketed IPv6 authority, or an explicit port; a bare hostname (HTTPClient("localhost")) still pairs with port. When the first argument is a URL, port is ignored — the URL is the single source of truth. The explicit address= keyword keeps working and wins over host.

Authentication

Against a coordinator started with --api-key, pass the key as api_key (or set $FLASH_API_KEY); it is sent as Authorization: Bearer <key> on every request:

client = HTTPClient("https://sandbox.example.com", api_key="s3cret")

The orchestrator also accepts the key as an X-API-Key header, which is what the admin tooling under meta/tools/ uses.

HTTPClient-only extras

The HTTP transport includes a few additional features:

  • Firecracker fields on start_sandbox: kernel_image, initrd_path, snapshot_path, mem_file_path.
  • Custom timeouts per-client via the timeout parameter.
  • Session injection – pass your own requests.Session for connection pooling, mutual TLS, retry policies, or authentication headers.
  • Typed exceptions: SandboxHTTPError (with .status_code and .detail) and the more specific SandboxNotFoundError for 404 responses.
  • Auto-expiry via ttl_seconds on start_sandbox — see below.

Auto-expiry (ttl_seconds)

Pass ttl_seconds=N to start_sandbox to cap the sandbox's wall-clock lifetime. The node-side reaper deletes the sandbox once the deadline passes, even if the caller exits without calling stop_sandbox. Useful as a safety net for batch jobs and benchmarks that can leak sandboxes on crash.

# Sandbox is auto-deleted ~10 minutes after creation, even if this
# process is killed before reaching stop().
sandbox = client.start_sandbox(
    type="kubernetes",
    image="alpine:latest",
    ttl_seconds=600,
)

Currently enforced by the kubernetes backend only; other backends accept the field and silently ignore it. The deadline is stamped on the pod as the flash-sandbox/ttl-expires-at annotation, so it survives orchestrator restarts.

Classifying execs (metadata)

exec_command (sync and async, on both HTTPClient/AsyncHTTPClient and Sandbox/AsyncSandbox) accepts an optional metadata mapping. It is opaque to the backend — backends ignore it and never change the command based on it — and is forwarded verbatim onto the exec trace (.jsonl, on the start and finish events), the structured event log (container_execs, under the client key), and the raw request body captured by FSB_DEBUG. This lets you classify execs downstream (e.g. interactive vs. verification, by task id) without encoding semantics into the command:

# verification query
sb.exec_command(["pytest", "-x"],
                metadata={"query_type": "verification", "task_id": 7})

# interactive query
sb.exec_command(["git", "status"],
                metadata={"query_type": "interactive", "task_id": 7})

# async is identical
await sb.exec_command(["pytest"],
                      metadata={"query_type": "verification", "task_id": 7})

# streaming variant honors metadata too
for frame in sb.exec_command(["pytest", "-v"], stream=True,
                             metadata={"query_type": "verification"}):
    ...

Values must be JSON-serializable. Omit metadata (the default) to keep the wire body minimal — existing calls are byte-identical to before, so this is fully backwards-compatible. To attribute CPU to a query type by bracketing an exec with get_metrics() samples, see docs/guides/exec-cpu-attribution.md.

Receiving metadata in Go

On the server side, metadata is a plain map[string]any that flows through four layers. None of them interpret it — the field is read from the request and forwarded onto the downstream captures.

Layer Type / location Notes
HTTP request api.ExecRequest.Metadata (api/handlers.go) Decoded from the JSON body; json:"metadata,omitempty".
Backend call pebble.ExecOpts.Metadata (internal/pebble/sandbox.go) Passed into Manager.Exec(...) / Sandbox.Exec(...); backends MUST NOT interpret it.
Coordinator request DB (FSB_DEBUG) api_calls.metadata (api/request_log_sqlite.go) The top-level metadata object is compacted into a queryable column (no BLOB parse needed).
Structured event log metadata["client"] in Manager.exec (internal/pebble/manager.go) The container_execs event recorder writes the bag under client.

To add a new exec-like endpoint that also receives metadata, decode the same field into a pebble.ExecOpts and pass it to Manager.Exec:

var req struct {
    Command  []string       `json:"command"`
    Metadata map[string]any `json:"metadata,omitempty"`
}
// ... decode req ...
res, err := m.Exec(ctx, id, req.Command, "my_op",
    pebble.ExecOpts{Metadata: req.Metadata})

For execs dispatched outside Manager.Exec (the streaming path), the coordinator records the same bag: Manager.RecordExecTerminal(id, metadata, valid) applies RLVR terminal-outcome accounting, and the per-exec metadata lands in the coordinator's api_calls.metadata column when FSB_DEBUG=1 (the former node-side TraceExecStart JSONL trace has been removed).

Response types (HTTP client)

Class Fields
ExecResult stdout: str, stderr: str, exit_code: int
MetricsResult memory_usage_bytes, memory_limit_bytes, memory_percent, cpu_percent, pids_current, net_rx_bytes, net_tx_bytes, block_read_bytes, block_write_bytes
SnapshotResult snapshot_path: str, mem_file_path: str, node_id: str

All response dataclasses are frozen (immutable).

Context manager

All clients support the context-manager protocol (with for sync, async with for async):

from flash_sandbox import HTTPClient

with HTTPClient(host="localhost") as client:
    sandbox = client.start_sandbox(type="docker", image="alpine:latest")
    sandbox.exec_command(["echo", "hello"])
    sandbox.stop()
# Connection is automatically closed here.

Proxy / reverse-proxy support

Both clients support routing through a reverse proxy that uses path-based routing. Pass the full address including the path prefix:

http_client = HTTPClient(address="http://proxy.example.com:8092/v1/service/sandbox")

The Sandbox / AsyncSandbox wrappers inherit proxy support from whichever client you pass in.

Running tests

pip install -e ".[dev]"
pytest tests/

CLI (fsb)

Installing flash-sandbox also installs the fsb command.

pip install flash-sandbox

Point it at a coordinator with -H/--address or the FLASH_SANDBOX_ADDR env var (default localhost:8080). Like the SDK, fsb uses the coordinator's /sandboxes API, not a bare node's /native/*:

export FLASH_SANDBOX_ADDR=coordinator.example:8080

fsb ps                       # list running sandboxes
fsb inspect sb-a1b2c3        # status + platform info
fsb nodes                    # cluster nodes
fsb templates                # registered templates
fsb status                   # cluster summary

fsb create --template python # create from a template (or --image IMG)
fsb stop sb-a1b2c3
fsb snapshot sb-a1b2c3
fsb resume sb-a1b2c3

fsb exec sb-a1b2c3 -- ls -la /workspace   # exits with the remote exit code
fsb metrics sb-a1b2c3
fsb cp ./local.txt sb-a1b2c3:/tmp/x       # upload
fsb cp sb-a1b2c3:/tmp/x ./out.txt         # download

Add --json (before the subcommand) for machine-readable output:

fsb --json ps | jq '.[].id'

fsb speaks the coordinator's HTTP API only. For host-level containerd administration on a node (images/snapshots/content cleanup), use the containerd CLI (ctr) or the Docker CLI directly.

Metadata

Release files for flash-sandbox 0.2.12

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flash-sandbox 0.2.12
File Size Uploaded
flash_sandbox-0.2.12.tar.gz 113.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flash-sandbox 0.2.12
File Interpreter ABI Platform
flash_sandbox-0.2.12-py3-none-any.whl Python 3 none any Details

Total release size: 181.6 kB

Release files / flash_sandbox-0.2.12.tar.gz

Download URL flash_sandbox-0.2.12.tar.gz
Size 113.7 kB
Tags Source
SHA-256 checksum
How to use checksums
21a6a2c56ce2ee4b6958992ae3ded42bd4eee0eff0ed60f778bf49d1faa99f1b
BLAKE2b-256 checksum
How to use checksums
b26bda8a72b11667a60d95c84b7575cc99b440e3808a59fdb8538de00e2eab8c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.13

Release files / flash_sandbox-0.2.12-py3-none-any.whl

Download URL flash_sandbox-0.2.12-py3-none-any.whl
Size 67.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3662a33149f3cc937310f4029fce52d87a2c7b81f2fd13fe6ac6e25d9fc08322
BLAKE2b-256 checksum
How to use checksums
4c30133969a6646a97f390025966080e63024964ae716f0f31094345d0b35301
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.13

Release history Release notifications | RSS feed

0.4.0

2 release files

0.3.0

2 release files

This release

0.2.12 This release

2 release files

0.2.11

2 release files

0.2.10

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.1.13

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page