Just like OpenRouter gives you a single API across LLM providers, bespokelabs-sandbox gives you a unified interface across sandbox providers. Write your code once, swap backends with a single parameter.
Use Sandbox / SandboxClient when your application owns provider credentials,
or RemoteSandboxClient when it connects to a control plane that you operate.
The direct SDK provides execution, files, workspaces, agent helpers, and session
resume. The optional server adds organization keys, policies, metering, and an
operations dashboard.
Contents: Install · Quickstart · Hosted control plane · API reference · Feature support · Examples · Development
- Common interface — Use the same execution and file APIs across nine backends, with provider-specific configuration where needed.
- Easily move between providers — If one provider has an outage or capacity issue, change one string and keep running.
- Cost tracking — Track Claude Code token usage, estimated sandbox compute cost, and compare backend pricing.
- Optional hosted access — Operate multiple providers behind organization API keys, with lifecycle costs, policies, audit history, and a dashboard.
Backend selection is explicit. Automatic cheapest-provider routing and automatic failover are not implemented; the benchmark example helps compare providers.
The core SDK declares Python 3.10+. Use Python 3.11+ for the control
plane and its tests: the current server implementation imports datetime.UTC.
The only required core dependency is Pydantic; provider SDKs are optional.
pip install bespokelabs-sandboxSandbox can share an environment with Curator:
python -m pip install bespokelabs-curator bespokelabs-sandboxfrom bespokelabs import curator, sandboxEach distribution owns its own subpackage. Sandbox installs only
bespokelabs/sandbox; it does not install bespokelabs/__init__.py.
The separate bespokelabs distribution is not needed for these imports.
The Nimble SDK uses the bespokelabs-nimble distribution.
For upgrades from older releases that shared the root file, use a fresh virtual
environment with namespace-compatible versions of all installed Bespoke packages.
Use python -m pip with the interpreter used by your app or notebook, and check
for old editable checkouts or a local bespokelabs.py if imports fail.
With a specific backend:
pip install 'bespokelabs-sandbox[docker]'
pip install 'bespokelabs-sandbox[daytona]'
pip install 'bespokelabs-sandbox[tensorlake]'
pip install 'bespokelabs-sandbox[modal]'
pip install 'bespokelabs-sandbox[e2b]'
pip install 'bespokelabs-sandbox[ray]'
pip install 'bespokelabs-sandbox[all]'[all] installs all optional provider SDKs, but does not include [server]
or [dev]. Install 'bespokelabs-sandbox[server,e2b]', for example, to run
the API with E2B support. The remote HTTP client needs only the core package.
The RunPod backend has no Python extra; it uses the system OpenSSH client. The Safehouse backend also has no Python extra. Install its CLI separately on macOS:
brew install eugene1g/safehouse/agent-safehouseLocal execution needs no provider account. Docker, Ray, and Safehouse have the runtime requirements listed below.
| Backend | Extra | Requires |
|---|---|---|
| Local subprocess | (none) | Python installed |
| Agent Safehouse | (none) | macOS + safehouse CLI |
| Docker | [docker] |
Docker daemon running |
| Ray | [ray] |
Ray installed (local or remote cluster) |
| Backend | Extra | Auth |
|---|---|---|
| Daytona | [daytona] |
DAYTONA_API_KEY |
| Tensorlake | [tensorlake] |
TENSORLAKE_API_KEY or tl login |
| Modal | [modal] |
MODAL_TOKEN_ID + MODAL_TOKEN_SECRET |
| RunPod | (none) | RUNPOD_API_KEY + registered SSH key |
| E2B | [e2b] |
E2B_API_KEY |
You only need to install the backend you use. The others are lazily imported.
Local and Ray execute subprocesses on the host or Ray worker and inherit its environment. Their workspace path rewriting is a convenience, not an OS security boundary. Safehouse wraps subprocesses with its macOS policy, while its file helpers still access the host directly. Use the isolation properties of your selected backend when deciding which workloads to run.
from bespokelabs.sandbox import Sandbox
# Zero setup — runs locally
with Sandbox("local") as sb:
result = sb.execute_code('print("hello")')
print(result.stdout)
# Or use Safehouse on macOS
with Sandbox("safehouse") as sb:
result = sb.execute_code('print("hello from safehouse")')
print(result.stdout)
# Or use Docker
with Sandbox("docker") as sb:
result = sb.execute_code('print("hello from a container")')
print(result.stdout)
# Or any cloud provider — same interface
with Sandbox("e2b") as sb:
result = sb.execute_code('print("hello from the cloud")')
print(result.stdout)Switch backends by changing one string:
for backend in [
"local", "safehouse", "docker", "modal", "runpod",
"e2b", "daytona", "tensorlake", "ray",
]:
options = {"gpu": "NVIDIA L4"} if backend == "runpod" else {}
with Sandbox(backend, **options) as sb:
sb.execute_code('print("same code, any backend")')The optional control plane lets a customer use one Bespoke API key across all of the providers you operate. Provider credentials remain on the server, and every execution is automatically attributed to the customer's organization and sandbox.
This is a self-hosted service shipped in the repository. Run one API process per SQLite database: live sandbox handles are held in that process, and execution cannot resume automatically after a restart. Graceful shutdown attempts to destroy the process's attached sandboxes.
Install only the provider adapters enabled by this deployment. For a local and E2B development server:
pip install 'bespokelabs-sandbox[server,e2b]'From a source checkout, use:
pip install -e '.[server,e2b]'Provider credentials belong on the server. Customers never receive them.
export BESPOKE_API_KEY_PEPPER='replace-with-a-long-random-secret'
export BESPOKE_CONTROL_PLANE_ADMIN_TOKEN='replace-with-another-random-secret'
export BESPOKE_CONTROL_PLANE_DB='/absolute/path/to/bespoke-control-plane.db'
export BESPOKE_ALLOWED_BACKENDS='local,e2b'
export BESPOKE_CUSTOMER_MARKUP='1.20'
export BESPOKE_SUPERVISION_INTERVAL_SECS='30'
export BESPOKE_SESSION_COOKIE_SECURE='1'
export BESPOKE_DASHBOARD_SESSION_TTL_SECS='28800'
# Provider credential — server-side only.
export E2B_API_KEY='your-e2b-api-key'
bespokelabs-sandbox-apiVerify that it is running:
curl http://127.0.0.1:8000/healthz
# {"status":"ok"}BESPOKE_API_KEY_PEPPER and BESPOKE_CONTROL_PLANE_DB are persistent server
identity. Keep both values unchanged across restarts. Changing the pepper or
pointing at another database makes previously issued product keys invalid.
The backend allowlist is also read at startup, so restart the server after
changing BESPOKE_ALLOWED_BACKENDS. If omitted, all registered backends are
allowed; set an explicit list for your deployment. HOST and PORT default to
127.0.0.1 and 8000. The CLI serves HTTP; terminate HTTPS at a reverse proxy
for the secure dashboard cookie.
Provider configuration is server-owned. The control plane reads the standard
Daytona, E2B, Modal, Runpod, and Tensorlake credential variables into memory;
it never returns their names or values through the product API. Scoped
provider-health responses expose only the backend, configured state, health
state, check time, and a fixed safe message. A configured provider remains
unchecked unless a deployment-supplied health checker actually runs; only a
successful check is reported as healthy.
The stock bespokelabs-sandbox-api CLI wires provider settings for safe
configuration visibility only. It does not construct provider health
checkers, reconcilers, or out-of-process terminators. Consequently, stock-CLI
provider health remains unchecked, reconciliation is unavailable, and a
restarted process cannot reclaim a real cloud resource whose in-memory runtime
was lost. A production deployment requiring those capabilities must instantiate
ControlPlane in deployment code and inject provider-specific health-check,
reconciler, and terminator adapters after testing them against that provider.
Issue an organization's initial product key through the protected bootstrap
endpoint. The returned secret is shown only in this response:
curl -X POST http://127.0.0.1:8000/v1/organizations \
-H "X-Control-Plane-Admin: $BESPOKE_CONTROL_PLANE_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"Acme"}'Copy the returned value beginning with bsk_live_. That is the customer-facing
product key. It is different from E2B_API_KEY and works across every backend
enabled by the server.
Set the product key in the client environment:
export BESPOKE_SANDBOX_KEY='bsk_live_...'Then create and use a sandbox through the remote client:
import os
from bespokelabs.sandbox import RemoteSandboxClient
client = RemoteSandboxClient(
"http://127.0.0.1:8000",
os.environ["BESPOKE_SANDBOX_KEY"],
)
with client.create(
"e2b",
timeout_secs=60,
idempotency_key="example-launch-001",
) as sandbox:
result = sandbox.execute_code(
'print("hello from E2B")',
idempotency_key="example-job-001",
)
print(result.stdout)
print(result.usage)
# The same key can create a different provider's sandbox.
with client.create("local") as sandbox:
result = sandbox.execute_command("python", ["--version"])
print(result.stdout)
costs = client.costs(group_by="backend")
print(costs)The context manager requests sandbox destruction on exit. Creation keys are scoped to an organization: the same key and payload return the existing record, including its current status, while a different payload is rejected. Execution keys are scoped to an organization and sandbox. A completed execution can return its cached response while the runtime remains attached and running; an in-progress or failed execution returns a conflict. Execution payloads are not compared, so use a new key for each distinct command and reuse it only to retry that command. The client does not automatically retry HTTP requests.
RemoteSandboxClient(..., timeout_secs=60) sets the HTTP request timeout;
client.create(..., timeout_secs=60) sets the sandbox timeout. Allow enough
HTTP time for provider creation. A client timeout does not cancel server work.
The remote surface currently supports creation, inventory (list / get),
code/command execution, destruction, costs, and reconciliation. It does not
expose the direct SDK's file transfers, Manifest, backend_options, agent
helpers, structured return_type, snapshot creation, or session resume. See the
API contract for the remaining HTTP endpoints.
Cost amounts and durations arrive as decimal strings; group_by accepts
"sandbox", "backend", or "day".
Provider failures expose a stable error contract in both HTTP responses and
RemoteSandboxError: code, backend, op, retryable, outcome, and a
small, redacted context. Sandbox responses include attempt_count,
latest_error, retry_status, cleanup_status, and provider_resource_id.
An ambiguous create has outcome="unknown" and
retry_status="blocked_cleanup_unknown"; do not issue a new provider create
until the possible orphan has been reconciled manually.
Sandbox records also expose requested, provisioning, running, stopping,
terminated, failed, and last-provider-observation timestamps. Lifecycle cost
uses the rate, currency, and pricing-source snapshot captured at creation and
accrues across the whole provider-billable window, including failed setup and
cleanup. GET /v1/reconciliation returns tenant-scoped health; an operator can
run a configured provider reconciler with
POST /v1/reconciliation/{backend} when the deployment injected that backend's
reconciler. Provider-reported cost replaces the
estimate through an idempotent delta ledger, so repeated observations do not
double-count spend. Billable-time deltas are split at UTC day boundaries, so
refreshes and later provider-cost adjustments do not move historical usage
between reporting days.
Sandbox responses also include the creating API-key ID/name and a numeric
version. GET /v1/sandboxes/{id}/detail returns the tenant-scoped creation
attempts, executions, lifecycle cost, and provider-observation history. A
termination request may send that version in If-Match; stale versions are
rejected with 409, while repeating a successful termination remains
idempotent. GET /v1/session reports the current key's scopes and whether the
dashboard may show termination controls.
Organization guardrails are managed with GET and PUT requests to
/v1/policies/current. They can cap concurrent sandboxes, rolling-hour and
UTC-day customer spend, allowed backends, allowed GPU types, and requested
sandbox lifetime. Each create request reserves its concurrency slot and checks
all limits in one database transaction before any provider call. A denial is a
stable, non-retryable policy_denied response and is attributed to the API key
that made the request. GET /v1/policy-summary returns current quota/spend and
recent denials. Redacted provider health is available from GET /v1/providers
and POST /v1/providers/{backend}/health-check.
An explicit creation timeout_secs also sets the control plane's expires_at
on every backend, independently of the provider's own timeout semantics. Omitted
values do not create a supervisor deadline, even when a preset supplies a
recommended timeout. The control-plane supervisor terminates expired sandboxes
even when the client disconnects. Its durable claim state is safe across process
restarts, and configured provider terminators can clean up resources whose
in-memory runtime was lost. Reconciliation watchdogs also remove provider resources that do not
belong to any recorded sandbox, with durable idempotency so a confirmed cleanup
is not repeated. Set BESPOKE_SUPERVISION_INTERVAL_SECS to tune the scan
interval. These restart-recovery and reconciliation behaviors require injected
terminator/reconciler adapters; the stock CLI does not supply them.
Open http://127.0.0.1:8000/dashboard and
enter the bsk_live_... product key. For this plain-HTTP loopback example,
set BESPOKE_SESSION_COOKIE_SECURE=0 before starting the server so the browser
can send its session cookie. Keep the default 1 when serving over HTTPS.
The initial organization key has every scope. A read-only dashboard key needs
both usage:read and sandboxes:read.
The dashboard provides:
- operational cards for active resources, failed launches, unreconciled spend, and resources nearing their TTL;
- budget/quota progress, backend/GPU/lifetime policy, provider health, and recent policy denials when the key has the corresponding read scopes;
- launches for keys with
sandboxes:create(choose an enabled backend and clear the form'sdashboard-cpupreset default unless your deployment has registered that custom preset); - status, provider, and creation-date filtering with client-side pagination;
- creator key, provider resource, compute/GPU, rate, age/TTL, cleanup, and reconciliation state for every sandbox;
- a detail view for lifecycle, attempts, executions, errors, cost, and provider observations;
- confirmed, revision-safe termination for keys with
sandboxes:terminate; legacysandboxes:writekeys remain compatible; and - automatic refresh every 15 seconds while the tab is visible, plus manual refresh.
The production dashboard exchanges the product key for an expiring, HTTP-only,
Secure, SameSite=Strict cookie and never stores that key in browser storage.
State-changing requests carry a session-bound CSRF token. For an explicitly
enabled local-only workflow, set BESPOKE_ENABLE_LOCAL_DASHBOARD_LOGIN=1, set
BESPOKE_SESSION_COOKIE_SECURE=0 only when serving plain HTTP on loopback, and
open /dashboard/local; that development route keeps the key in the current
tab's sessionStorage. Never expose that route on a shared network. Local
sandboxes record runtime but have a provider cost of $0; use a priced fake or
cloud backend to exercise spend counters.
The Activity view shows durable alerts and append-only audit history. Operators
with exports:read can download paginated CSV pages for usage, lifecycle costs,
and ledger entries. Dashboard export buttons download the first page of up to
500 rows; full exports require following the HTTP X-Next-Cursor header as
described in the API contract.
Alert thresholds and retention periods are tenant-owned and available through
the API. Retention defaults to 90 days for operational history and 365 days for
audit; an explicit retention request preserves usage and financial ledgers.
API keys, browser session cookies, and CSRF tokens are HMAC-hashed at rest.
Runtime scopes are sandboxes:read,
sandboxes:create, sandboxes:execute, and sandboxes:terminate; governance
uses usage:read, policies:read, policies:write, providers:read, and
providers:write; reconciliation uses providers:reconcile; and key issuance
uses keys:write. Operational access uses alerts:read, alerts:write,
audit:read, exports:read, retention:read, and retention:write. The legacy
sandboxes:write scope grants create, execute, terminate, and provider
reconciliation for compatibility. Runtime events and provider/customer costs
are written to an idempotent ledger. All responses carry a restrictive content
security policy and browser security headers. See the
operations runbook and
hosted API contract before deployment.
Never commit the API-key pepper, admin token, provider credentials, or product keys. Revoke and replace a product key if it appears in source code, logs, or chat. Environment-variable ownership is:
| Value | Where it belongs |
|---|---|
E2B_API_KEY and other provider credentials |
Control-plane server only |
BESPOKE_API_KEY_PEPPER |
Control-plane server only; stable across restarts |
BESPOKE_CONTROL_PLANE_ADMIN_TOKEN |
Administrative bootstrap tooling only |
bsk_live_... / BESPOKE_SANDBOX_KEY |
Customer client or dashboard |
Common errors:
| Error | Resolution |
|---|---|
backend is not enabled: e2b |
Add e2b to BESPOKE_ALLOWED_BACKENDS and restart the server. |
invalid API key |
Confirm the full bsk_live_... value, database path, and original pepper. |
| Provider authentication failure | Install that provider's extra and configure its credential on the server. |
from bespokelabs.sandbox import Sandbox
sb = Sandbox(
"local", # "local" | "safehouse" | "docker" | "ray" | "daytona" | "tensorlake" | "modal" | "runpod" | "e2b"
preset=None, # Preset name or SandboxPreset object
cpu=1.0, # vCPUs (Tensorlake, Modal, Docker, Daytona, RunPod)
memory_mb=1024, # RAM in MB (Tensorlake, Modal, Docker, Daytona, RunPod)
disk_mb=None, # Disk in MB (Daytona, RunPod)
gpu=None, # GPU type/count (Modal, RunPod)
timeout_secs=None, # Omitted: preset default, otherwise 600 seconds
image=None, # OCI image, or Tensorlake project image name
template=None, # Template ID (E2B, RunPod)
env_vars=None, # dict of environment variables
allow_internet=True, # Mapped by Docker and Tensorlake; rejected if false on RunPod
app_name=None, # App name (Modal)
snapshot_id=None, # Restore from snapshot (Tensorlake, Modal, Daytona)
workdir=None, # Sandbox root or command working directory
backend_options=None, # dict merged into the backend's native create call
files=None, # {path: bytes|str} written into the sandbox on create
git_repo=None, # repo URL cloned into the sandbox on create
git_ref=None, # branch/tag for git_repo
workspace=None, # Manifest of files/dirs/repos to materialize on create
)Not every backend uses every parameter. Most unsupported parameters are ignored; a backend rejects values it cannot safely honor, such as network isolation on RunPod.
Modal GPU sandboxes accept Modal's GPU reservation strings, including a GPU
type such as gpu="L4" or a type and count such as gpu="H100:2":
with Sandbox("modal", gpu="A100") as sb:
result = sb.execute_command("nvidia-smi")
print(result.stdout)GPU sandboxes can be preempted, so GPU workloads should tolerate interruption.
RunPod uses exact GPU type IDs and requires a GPU selection. Its official PyTorch image is used by default because it includes SSH support:
with Sandbox(
"runpod",
gpu="NVIDIA H100 80GB HBM3:2",
backend_options={"ssh_private_key_path": "~/.ssh/id_ed25519"},
) as sb:
result = sb.execute_command("nvidia-smi")
print(result.stdout)For capacity fallback, pass RunPod's native creation fields through
backend_options, such as a gpuTypeIds list containing "NVIDIA L40S" and
"NVIDIA RTX A6000", with gpuTypePriority="availability". Custom images and
templates must run an SSH daemon on port 22. Add the matching public key to the
RunPod account before creating a sandbox. ssh_private_key_path is optional
when the key is already discoverable by OpenSSH.
timeout_secs is a command timeout on Local, Safehouse, Docker, Ray, and
RunPod. E2B and Modal pass it as the provider sandbox timeout; Tensorlake passes
it to create_and_connect. Daytona maps an explicit value to a wall-clock
ttl_minutes deadline, rounded up to at least one minute.
RunPod creation has a separate 10-minute default readiness timeout, adjustable
with backend_options={"create_timeout_secs": ...}. Daytona
destroys the sandbox when that deadline elapses in whatever state it is in,
running work included; no activity resets the clock, and the clock starts at
creation, so image pull and boot count against it. The mapping happens only
when you pass timeout_secs yourself. Omit it — including when a preset
supplies its own recommended value — and the Daytona sandbox gets no TTL at
all, leaving it bounded only by Daytona's default 15-minute idle auto-stop.
Constructing a Sandbox creates the underlying sandbox immediately. To launch
many sandboxes on one backend, or to use async/await, see
Reusing a client across many sandboxes
and Async.
result = sb.execute_code('print(1 + 1)', language="python")
print(result.stdout) # "2"
print(result.stderr) # ""
print(result.exit_code) # 0language defaults to "python". The Daytona and E2B adapters ignore this
argument and call their Python code endpoints. Other adapters invoke an
installed interpreter as <language> -c <code>; for runtimes with different
flags, use execute_command explicitly.
By default, inspect exit_code on the returned SandboxResult; a nonzero
exit is not uniformly converted into an exception. Local, Safehouse, Docker,
and Ray return exit code 124 on execution timeout. Provider/transport
failures can instead raise SandboxError subclasses.
result = sb.execute_command("ls -la /tmp")
result = sb.execute_command("grep", args=["-r", "TODO", "/app"])Pass return_type to execute_code or execute_command to parse stdout into
a Pydantic model, dataclass, or class accepting keyword arguments:
from pydantic import BaseModel
from bespokelabs.sandbox import Sandbox
class Stats(BaseModel):
count: int
mean: float
with Sandbox("local") as sb:
stats = sb.execute_code(
'import json; print(json.dumps(dict(count=3, mean=2.0)))',
return_type=Stats,
)
print(stats.count, stats.mean)
# Parse existing text, including a downloaded file's decoded contents.
stats = Sandbox.parse_as('{"count": 3, "mean": 2.0}', Stats)Parsing accepts JSON objects, including objects in Markdown fences or
surrounding text. It raises CommandFailedError if execution exited nonzero,
and SandboxExecutionError if no object can be parsed or constructed.
json_schema(Stats) generates a prompt instruction; for command calls,
inject_schema=True appends that instruction to the last item in args when
return_type is supplied. These helpers do not force the command to emit JSON.
# List files
files = sb.list_files("/home")
for f in files:
print(f.path, f.is_dir, f.size)
# Read / write in-memory content
sb.write_file("/tmp/config.json", '{"key": "value"}')
data = sb.read_file("/tmp/config.json") # returns bytes
# Upload a local file into the sandbox
sb.upload_file("./local_data.csv", "/home/user/data.csv")
# Download a file from the sandbox to local disk
sb.download_file("/home/user/results.json", "./results.json")
# Move a whole directory tree in or out (preserves structure + executable bits).
# Defaults to a single tar.gz transfer, falling back to a per-file loop.
sb.upload_dir("~/.claude/skills/my-skill", ".claude/skills/my-skill")
sb.download_dir("/workspace/output", "./results")Directory transfer is built on the single-file primitives above, so it works on every backend with the required shell tools. To seed a tree during creation, use
build_files_map(local, remote)withSandbox(files=...), or aManifestcontainingLocalDir. Seeexamples/move_files_into_sandbox.py.
Directory methods return the number of transferred files. Uploads skip symlinks
and empty directories. method="auto" uses tar when available, otherwise
"per_file"; the per-file download fallback does not preserve executable bits.
build_files_map and files= carry contents only. Some provider file APIs
require parent directories to exist, so create them before writing nested paths.
On macOS, COPYFILE_DISABLE=1 avoids AppleDouble metadata files in local tar
downloads.
files=, git_repo=, and upload_dir() cover the imperative cases. For
anything richer, pass a Manifest — a typed, ordered map of destination →
source materialized at creation (built on the primitives above, so it works on
every backend):
from bespokelabs.sandbox import Sandbox, Manifest, GitRepo, LocalDir, LocalFile, File
with Sandbox("daytona", workspace=Manifest(entries={
"repo": GitRepo("https://github.com/org/proj", ref="main"),
".claude/skills/pirate": LocalDir("~/.claude/skills/talk-like-a-pirate"),
"data/seed.csv": LocalFile("./seed.csv"),
"config.json": File('{"env": "prod"}'),
})) as sb:
...Default creation order is git_repo clone → files → workspace entries →
preset setup (when needed). OpenCode opts into setup before workspace
materialization so npm and Git are ready before cloning. All of this happens after the provider resource exists;
setup/materialization failure triggers a cleanup attempt. Entries materialize
in insertion order, so a later one overlays an earlier one.
GitRepo clones, LocalDir / LocalFile upload (preserving the executable
bit), and File writes in-memory content. Subclass WorkspaceEntry for custom
sources, or call manifest.apply(sb) on a live sandbox.
SDK operation errors use SandboxError subclasses carrying a machine-readable
code, the backend and op when supplied, a retryable flag, outcome, and
a context dict. Branch on these fields instead of parsing message strings:
from bespokelabs.sandbox import Sandbox, SandboxError
try:
with Sandbox("daytona") as sb:
sb.execute_command("…")
except SandboxError as e:
print(e.code, e.backend, e.op, e.retryable, e.outcome, e.context)
# Decide whether retrying this operation is safe before repeating it.Subtypes include SandboxConfigurationError, SandboxCreationError,
CommandFailedError (with exit_code / stdout / stderr),
SandboxTimeoutError and SandboxConnectionError (both retryable),
SandboxAuthenticationError, SandboxNotFoundError, WorkspaceError,
BackendNotInstalledError, and FeatureNotSupportedError. Invalid helper
arguments can still raise standard Python exceptions, such as KeyError for
an unknown preset. RemoteSandboxError is a separate HTTP-client exception
and does not inherit from SandboxError.
A retryable transport failure alone does not prove that repeating a create or
command is safe. Check outcome and cleanup state: "unknown" can mean the
provider performed the operation but its response was lost.
Sandboxes can also be bound to agents without replacing the low-level sandbox API. Agent placement is explicit:
inside: the agent process runs inside the sandbox, useful for CLI agents such as Codex CLI, Claude Code, or a custom inference runner.outside: the agent process runs outside the sandbox and drives it through capability-checked sandbox tools.
Inside-sandbox agent:
from bespokelabs.sandbox import AgentSpec, Sandbox
with Sandbox(
"docker",
preset="codex",
git_repo="https://github.com/bespokelabsai/sandbox",
) as sb:
agent = sb.agent(AgentSpec.inside(
name="codex",
command=["codex", "exec"],
cwd="sandbox",
))
result = agent.run("Run the eval suite and summarize failures")
print(result.stdout)Outside-driver agent:
from bespokelabs.sandbox import AgentSpec, Sandbox
def run_eval(ctx, prompt: str) -> str:
ctx.write_file("/workspace/task.txt", prompt)
result = ctx.shell("python3", ["/workspace/eval.py"])
return result.stdout
with Sandbox(
"docker",
files={"/workspace/eval.py": "print('ok')"},
) as sb:
agent = sb.agent(AgentSpec.outside(
name="eval-runner",
capabilities=["shell", "files"],
runner=run_eval,
))
print(agent.run("Evaluate this input"))For a complete Claude Code outside-sandbox example, see
examples/claude_code_outside.py. It starts
Claude Code on the host, clones a repository into a sandbox with git_repo=...,
and gives Claude a localhost bridge for running commands inside the sandbox. The
example asks Claude to count regular files directly in the cloned repository
root.
For outside agent frameworks, use agent_tools(...) directly:
with Sandbox("docker") as sb:
tools = sb.agent_tools(capabilities=["shell", "files", "patch"])
tools.write_file("/workspace/input.txt", "hello")
print(tools.shell("cat", ["/workspace/input.txt"]).stdout)The generic context exposes shell, files, and patch operations. ports
and artifacts are accepted capability names but have no operations yet.
Capabilities gate context methods, not the sandbox OS: shell can access files,
and the runner can access the underlying sandbox through ctx.sandbox.
Inside agents accept input_mode="stdin" (default), "argv" (append the
prompt), "file" (write the prompt and append its path), or "none". They
return SandboxResult; outside agents return their runner's value. Install and
authenticate the agent CLI separately from provider authentication. Presets
install tools but do not supply agent credentials.
To run GLM through the OpenCode harness:
import os
from bespokelabs.sandbox import Sandbox
with Sandbox(
"local", preset="opencode",
env_vars={"ZHIPU_API_KEY": os.environ["ZHIPU_API_KEY"]},
) as sb:
result = sb.run_agent(
"Review this project.", harness="opencode", model="zai/glm-4.7",
)
print(result.text)
print(result.usage.total_tokens, result.usage.total_cost_usd)For a Z.AI Coding Plan, use
model="zai-coding-plan/glm-4.7" with the same environment variable. Other
GLM models can be selected using their OpenCode provider/model identifier.
resume=True continues the latest conversation in the same workspace;
extra_args=["--session", session_id] selects a specific session.
The preset installs OpenCode at startup, requiring npm and install permissions
on the sandbox; its Docker setup installs npm and Git on the default Debian image
before cloning repositories. OpenCode opts into setup_before_workspace=True;
other presets retain setup after workspace materialization by default.
No prebuilt OpenCode image is assumed. The preset installs the CLI; select the
harness separately with harness="opencode" when calling run_agent.
OpenCode JSON step costs and token counts are summed across the run, including
cache usage and reasoning tokens (included in output_tokens). Original events
are available in result.raw["events"]; stdout, stderr and exit status are
preserved. See the runnable GLM example. Its local
workspace defaults to the Git-ignored examples/.sandbox_workdir/opencode_glm
directory and survives cleanup, so --resume works across invocations. Use
--workdir to override it. Recreating a cloud sandbox does not retain its files.
run_agent(...) runs Claude Code on a prompt and reports what the run cost.
It drives the CLI with JSON output under the hood, so it can return both the
assistant's text answer and a Usage breakdown — LLM token counts and dollar
cost (from Claude Code's own total_cost_usd), plus an estimate of the sandbox
compute the call consumed (elapsed wall-clock × the backend's per-second price).
with Sandbox("local", preset="claude-code") as sb:
result = sb.run_agent("Review /code.py and suggest fixes.")
print(result.text) # the assistant's answer
u = result.usage # usage for this call
print(u.input_tokens, u.output_tokens) # token counts
print(u.llm_cost_usd) # tokens, in dollars
print(u.compute_cost_usd) # sandbox compute, in dollars
print(u.total_cost_usd) # llm + compute
# Continue the conversation; usage accumulates on the sandbox.
sb.run_agent("Now apply those fixes.", resume=True)
print(sb.usage.total_cost_usd) # running total across both calls
print(sb.usage.total_tokens)sb.usage is the running total across every run_agent call in the sandbox's
lifetime (its compute component sums the agent-call durations, not idle time
between calls). Pass extra CLI flags with extra_args=[...]. Claude Code and
OpenCode are supported; token counts and llm_cost_usd reflect what the CLI's
JSON output reports, so llm_cost_usd can be 0 under subscription auth that
omits total_cost_usd (the token counts are still captured). The async
AsyncSandbox exposes the same run_agent(...) / usage. This is distinct
from bespokelabs.sandbox.pricing, which prices sandbox compute on its own.
For standalone sandbox compute estimates, use the bundled pricing helpers:
from bespokelabs.sandbox.pricing import cost_per_second, get_backend_pricing
print(get_backend_pricing("modal")) # raw bundled pricing metadata
print(cost_per_second("modal", vcpu=2.0)) # estimated $/sec for a sandboxsb.estimate_compute_cost(elapsed_secs) estimates a measured interval using
the live session's hourly rate when available (RunPod), otherwise bundled
CPU/RAM rates. Bundled estimates omit GPU, disk, network, and other charges;
an unknown backend also returns zero, which does not establish that it is free.
Direct SDK sb.usage tracks only run_agent calls. Remote execution usage
tracks each command's runtime, while hosted cost summaries use lifecycle costs
when available, including idle time, without adding execution costs twice.
Pricing data is best-effort and local to the installed package. To compare
available backends for a real workload, see
examples/find_cheapest.py, which benchmarks
cold-start and execution time, then estimates cost with the same pricing data.
Presets are predefined sandbox configurations with setup commands that run after creation.
The built-in presets are focused on agent CLIs: codex, claude-code, claude-code-codex, and opencode.
The npm-based setup commands require Node.js and npm in the execution environment when setup commands are used as a fallback.
Built-in presets have prebuilt OCI images published to GitHub Container Registry:
ghcr.io/bespokelabsai/sandbox/<preset>:v2
Docker, Daytona, and Modal use these images automatically when you pass a preset, then skip the preset's setup commands because the tools are already baked into the image.
The main advantage is that prebuilt images move setup work from sandbox startup time to image build time:
- Sandboxes start faster because they do not reinstall the same tools for every run.
- Startup is more reliable because it depends less on package registry availability during sandbox creation.
- Preset environments are more reproducible because images use pinned tags instead of a moving
latesttag.
For example, this Docker sandbox starts from ghcr.io/bespokelabsai/sandbox/codex:v2 and does not run npm install -g @openai/codex at startup:
with Sandbox("docker", preset="codex") as sb:
sb.execute_command("codex --version")You can still override the image explicitly when you need a custom base image:
with Sandbox("docker", preset="codex", image="my-registry/codex-tools:v3") as sb:
sb.execute_command("codex --version")An explicit image different from the preset image makes setup commands run
again, so that custom image must support the preset's installation commands.
On Local and Ray, those commands run on the host/worker and can install global
npm packages. Use Sandbox.list_presets() to inspect registered defaults.
The Dockerfiles live under images/<preset>/. Local, Safehouse, Ray, and other
backends that cannot use the prebuilt image still fall back to the preset setup
commands. RunPod deliberately does this on its SSH-ready default image.
Tensorlake image names are project-scoped, so you can build/register equivalent
images from the same Dockerfiles when you need Tensorlake-specific preset
images.
# Sandbox with Codex CLI installed
with Sandbox("docker", preset="codex") as sb:
sb.execute_command("codex --version")
# Sandbox with Claude Code installed
with Sandbox("docker", preset="claude-code") as sb:
sb.execute_command("claude --version")
# Sandbox with both Claude Code and Codex CLI installed
with Sandbox("docker", preset="claude-code-codex") as sb:
sb.execute_command("claude --version")
sb.execute_command("codex --version")Built-in presets:
| Preset | What it installs | Defaults |
|---|---|---|
claude-code |
@anthropic-ai/claude-code via npm |
2GB RAM, 30min timeout |
claude-code-codex |
@anthropic-ai/claude-code and @openai/codex via npm |
2GB RAM, 30min timeout |
codex |
@openai/codex via npm |
2GB RAM, 30min timeout |
opencode |
opencode-ai via npm |
2GB RAM, 30min timeout |
The preset controls which CLI is installed in the sandbox. It does not grant the CLI permission to use its own web tools. When you run an agent inside a remote sandbox such as Daytona, preconfigure the CLI for non-interactive runs instead of waiting for an in-terminal approval prompt.
For Claude Code, WebFetch and WebSearch are permission-gated tools. Use
--permission-mode dontAsk with the narrowest --allowedTools entries that
fit the task:
agent = sb.agent(AgentSpec.inside(
name="claude",
command=[
"claude",
"-p",
"--permission-mode",
"dontAsk",
"--allowedTools",
"WebFetch(domain:github.com)",
"WebSearch",
],
input_mode="argv",
))
result = agent.run("Summarize https://github.com/bespokelabsai/sandbox")For Codex CLI, use codex exec with explicit approval, sandbox, and search
settings. For read-only website summaries, keep the Codex sandbox read-only,
disable approval prompts, and enable live search:
agent = sb.agent(AgentSpec.inside(
name="codex",
command=[
"codex",
"exec",
"--ask-for-approval",
"never",
"--sandbox",
"read-only",
"--search",
],
input_mode="argv",
))
result = agent.run("Summarize https://github.com/bespokelabsai/sandbox")Use broader modes only when the outer sandbox is the trust boundary. For
example, codex exec --sandbox workspace-write --ask-for-approval never lets
Codex edit files without pausing, and Claude Code's --permission-mode bypassPermissions skips most permission prompts. Those modes are best kept to
isolated sandboxes with scoped credentials.
Create your own:
from bespokelabs.sandbox import Sandbox, SandboxPreset
Sandbox.register_preset(SandboxPreset(
name="my-stack",
description="My custom environment",
setup_commands=["pip install my-library", "npm install -g my-tool"],
cpu=2.0,
memory_mb=4096,
))
with Sandbox("docker", preset="my-stack") as sb:
...Explicit kwargs always override preset defaults.
Populate the sandbox at creation instead of scripting uploads afterwards.
files are written after any git_repo clone. Preset setup normally follows
workspace materialization; OpenCode runs setup first to install its prerequisites.
files needs nothing special and works on every backend and image:
with Sandbox(
"docker",
image="python:3.12-slim",
files={"/work/run.py": "print('ready')"},
) as sb:
print(sb.execute_command("python", ["/work/run.py"]).stdout)files values may be str or bytes.
git_repo runs git clone inside the sandbox, so git must be present in
the image. All prebuilt preset images include git, so git_repo works with
any preset on Docker/Daytona/Modal. For a custom image, make sure git is
installed; the host-based backends (local, safehouse) use the host's git.
with Sandbox(
"local",
git_repo="https://github.com/psf/requests",
git_ref="main", # optional branch/tag
) as sb:
entries = sb.list_files("/requests") # local workspace root / repo name
print(f"cloned {len(entries)} entries")git_repo= clones into a relative <repo-name> under the command working
directory; the location is not always /<repo-name> on cloud backends. Use
Manifest(entries={"/explicit/path": GitRepo(...)}) to choose a destination.
For Local and Safehouse, workdir is a host directory: an explicit directory
survives destroy(), while an automatically created temporary directory is
removed. Daytona and Tensorlake use workdir for shell commands only; their
code and file APIs keep provider path semantics. RunPod defaults command
execution to /workspace, while its file helpers use SSH path semantics.
Docker, Modal, E2B, and Ray do not map the top-level workdir option.
backend_options is an escape hatch: the dict is merged into the backend's
native creation call, so you can reach provider features the unified API
doesn't model — without waiting for a new keyword. It is forwarded to Docker
containers.run, Modal Sandbox.create, E2B Sandbox.create, Tensorlake
create_and_connect, Daytona's create params, and the RunPod REST creation
payload; ignored by local, safehouse, and ray.
# e.g. set the container hostname (a Docker-only knob)
with Sandbox("docker", backend_options={"hostname": "build-box"}) as sb:
sb.execute_command("hostname")On Daytona, env_vars given here is merged over the env_vars= parameter
rather than replacing it, and create_timeout (seconds) is consumed by the SDK
adapter to bound the create call itself instead of being passed as a sandbox
parameter.
On RunPod, transport-only options are consumed by the adapter rather than sent
to the REST API: create_timeout_secs, api_timeout_secs,
poll_interval_secs, ssh_private_key_path, ssh_user,
ssh_connect_timeout_secs, ssh_strict_host_key_checking, and
ssh_known_hosts_file. Other keys use RunPod's native camelCase field names.
A snapshot saves state to restore later; session state is a lightweight, serializable handle that reattaches to a sandbox that is still running — including from another process or machine:
sb = Sandbox("e2b", timeout_secs=600)
sb.execute_command("echo hi > /tmp/work.txt")
state = sb.session_state()
blob = state.to_json() # JSON-safe; stash in a queue/DB/file
# ... do NOT destroy sb — the sandbox must stay alive to reattach ...
# Elsewhere — another worker, another process:
from bespokelabs.sandbox import Sandbox, SandboxSessionState
sb2 = Sandbox.resume(SandboxSessionState.from_json(blob))
print(sb2.read_file("/tmp/work.txt")) # b"hi\n"SandboxClient("e2b").resume(state) is equivalent; an existing client can
reuse its provider connection.
Resume skips preset setup and workspace materialization. Provider credentials
must be available in the resuming process; host-directory sessions require the
same accessible directory. Session state is not a complete configuration or
usage checkpoint: wrapper configuration and cumulative agent usage reset on
resume. Local/Safehouse state includes the supplied environment overlay, which
may contain secrets. Both attached handles refer to the same resource, so
destroying either affects the other.
| Backend | Resume by | session_state payload |
|---|---|---|
| Docker | container id (containers.get) |
container_id |
| E2B | sandbox id (Sandbox.connect) |
sandbox_id |
| Modal | sandbox id (Sandbox.from_id) |
sandbox_id |
| Tensorlake | sandbox id (client.connect) |
sandbox_id |
| Daytona | sandbox id (client.get) |
sandbox_id, workdir (when set) |
| RunPod | Pod id (GET /pods/{id}) |
pod_id, workdir and SSH transport settings |
| Local, Safehouse | host workdir | workdir, env overlay |
| Ray | — (not supported) | raises FeatureNotSupportedError |
snap = sb.snapshot()
print(snap.snapshot_id)
# Restore later
sb2 = Sandbox("tensorlake", snapshot_id=snap.snapshot_id)| Backend | Snapshot support |
|---|---|
| Docker | Yes (container.commit()) |
| Tensorlake | Yes (filesystem + memory) |
| Modal | Yes (filesystem) |
| Daytona, E2B, Local, Ray, RunPod, Safehouse | No |
Restore a Docker snapshot using image=snap.snapshot_id; Docker does not map
snapshot_id. Modal and Tensorlake accept snapshot_id. Daytona can create
from an existing provider snapshot via snapshot_id, although this adapter
cannot create snapshots with snapshot().
# Context manager (recommended) — auto-destroys on exit
with Sandbox("local") as sb:
sb.execute_code("print('hi')")
# Manual cleanup
sb = Sandbox("docker")
sb.execute_code("print('hi')")
sb.destroy()
# Check state
sb.is_alive # True/False
sb.backend_name # "docker"
sb.provider_resource_id # provider ID, or None for local-style backendsis_alive tracks whether this wrapper has been destroyed; it does not poll the
provider or detect external termination. Cleanup behavior varies by adapter:
some suppress provider deletion errors. Use provider inventory or configured
control-plane reconciliation when you need confirmation of resource removal.
Sandbox(backend, ...) builds a fresh provider connection per sandbox. When
launching many sandboxes on one backend, create a SandboxClient once and
reuse it — provider-level state (the Docker daemon connection, Daytona auth,
the Ray runtime) is shared across create() calls:
from bespokelabs.sandbox import SandboxClient
client = SandboxClient("docker")
for task in tasks:
with client.create(image="python:3.12-slim") as sb:
sb.execute_code(task)client.create(...) accepts the same keyword arguments as Sandbox(...)
and returns a regular Sandbox session. SandboxClient(backend) validates
the backend name and SDK availability up front — it raises
BackendNotInstalledError immediately if the backend's extra isn't
installed — but performs no network I/O until create().
AsyncSandboxClient / AsyncSandbox mirror the sync API with coroutine
methods, so you can create and drive many sandboxes concurrently from one
event loop:
import asyncio
from bespokelabs.sandbox import AsyncSandbox, AsyncSandboxClient
async def run_snippet(client: AsyncSandboxClient, code: str) -> str:
async with await client.create() as sb:
result = await sb.execute_code(code)
return result.stdout
async def main():
client = AsyncSandboxClient("local")
snippets = ["print(1 + 1)", "print(2 + 2)"]
outputs = await asyncio.gather(*(run_snippet(client, c) for c in snippets))
print(outputs)
asyncio.run(main())One-step creation works too: sb = await AsyncSandbox.create("local").
Use one AsyncSandboxClient per event loop. Execution, files, run_agent,
snapshots, resume, and destruction are awaitable; session_state() and
properties remain synchronous. The wrapper does not expose agent() or
agent_tools().
Backend SDKs are synchronous, so async calls are offloaded to worker
threads — the event loop is never blocked. Note that the missing-SDK check
(BackendNotInstalledError) surfaces at the first await client.create()
rather than at AsyncSandboxClient(...) construction, which does no I/O.
This table describes the adapters in this repository, not every feature offered
by each provider's native SDK. “Interpreter” means an installed binary that
accepts -c; use shell commands for other language runtimes.
| Feature | Local | Safehouse | Docker | Ray | Daytona | Tensorlake | Modal | RunPod | E2B |
|---|---|---|---|---|---|---|---|---|---|
execute_code |
Interpreter | Interpreter | Interpreter | Interpreter | Python | Interpreter | Interpreter | Interpreter | Python |
execute_command |
Shell | Shell | Shell | Shell | Shell | Shell | Shell | SSH | Shell |
list_files |
Native | Native | find / ls |
Native | Native SDK | via ls |
Native SDK | via SSH | Native SDK |
read_file |
Native | Native | get_archive |
Native | Native SDK | via cat |
Native SDK | via SSH | Native SDK |
write_file |
Native | Native | put_archive |
Native | Native SDK | via base64 | Native SDK | via SSH | Native SDK |
upload_file |
shutil.copy |
shutil.copy |
put_archive |
Actor RPC | Native SDK | via base64 | Native SDK | via SSH | Native SDK |
download_file |
shutil.copy |
shutil.copy |
get_archive |
Actor RPC | Native SDK | via base64 | Native SDK | via SSH | Native SDK |
snapshot |
No | No | Yes | No | No | Yes | Yes | No | No |
| Resource limits | No | No | cpu, memory | cpu (Ray) | cpu, memory, disk | cpu, memory | cpu, memory, gpu | cpu, memory, disk, gpu | Tier-based |
allow_internet=False |
Ignored | Ignored | Yes | Ignored | Ignored | Yes | Ignored | Rejected | Ignored |
| Isolation | Process-level | macOS sandbox-exec |
Container | Process | Full VM | Container | Container | Container | Full VM |
gpu= reservation |
No | No | No | No | No | No | Yes | Yes | No |
| Needs install | Nothing | safehouse CLI |
Docker daemon | ray |
API key | tl login |
API key | API key + OpenSSH | API key |
Daytona applies CPU/RAM/disk overrides when creating from image; the snapshot
creation path uses the snapshot's resources. RunPod CPU/RAM values are minimums
per GPU, not exact allocations. Ray reserves CPU scheduling resources without
requesting GPU or memory limits. Tensorlake currently does not forward the
top-level env_vars option; inside-agent AgentSpec.env can supply per-command
variables. Its read_file converts text stdout to bytes; use download_file
for binary data.
from bespokelabs.sandbox import (
SandboxError, # Base class for all errors
SandboxCreationError, # Sandbox failed to start
SandboxExecutionError, # Code or command execution failed
BackendNotInstalledError, # pip package missing for chosen backend
FeatureNotSupportedError, # Backend doesn't support this operation
)All exceptions inherit from SandboxError, so you can catch broadly or narrowly:
try:
sb.snapshot()
except FeatureNotSupportedError:
print("This backend doesn't support snapshots")
except SandboxError as e:
print(f"Something else went wrong: {e}")# Docker — no auth needed, just a running Docker daemon
# Local — no auth needed
# Ray — optional remote cluster
export RAY_ADDRESS=ray://head-node:10001 # omit for local cluster
# Daytona
export DAYTONA_API_KEY=your_key
export DAYTONA_API_URL=https://app.daytona.io/api # optional
export DAYTONA_TARGET=us # optional
# Tensorlake
export TENSORLAKE_API_KEY=your_key
# Or authenticate via CLI:
tl login
# Modal
export MODAL_TOKEN_ID=your_id
export MODAL_TOKEN_SECRET=your_secret
# RunPod — add the matching public key to your RunPod account
export RUNPOD_API_KEY=your_key
# E2B
export E2B_API_KEY=your_keyThe optional Envy supervisor registers sandbox agents with a
shared human token and wakes their Codex or Claude Code conversations through
an authenticated event stream. It queues incoming messages, resumes the saved
conversation, posts replies, and retries interrupted deliveries. No inbox
polling or model calls happen while idle. Start with
examples/envy_listener.py.
Run examples from a source checkout after installing the package. Each script lists its own provider and agent prerequisites.
| Example | Demonstrates |
|---|---|
| move_files_into_sandbox.py | Generated local directory, file-map seeding, and live transfers |
| find_cheapest.py | Cold-start/execution benchmarks and bundled compute estimates |
| sandbox_repo.py | Claude Code or Codex inside Daytona/Tensorlake, with a clone or web input |
| claude_code_outside.py | Host-side agent driving sandbox commands through a local bridge |
| claude_code_persona.py | Declarative workspace and agent persona files |
| github_stats.py | Parsing agent output into a Pydantic model |
A local transfer demo needs no cloud credentials or agent CLI:
python examples/move_files_into_sandbox.py --backend localUse Python 3.11+ to include the server modules and tests:
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[dev,server]'
# Unit/API tests using local execution and mocked providers.
python -m pytest -q --ignore=tests/test_regression.py
# Repository formatting and lint configuration.
python -m pyink --check src tests examples
python -m ruff check src tests examplesOn macOS, prefix the test command with COPYFILE_DISABLE=1 to prevent the
system tar from adding AppleDouble ._* files to directory-transfer fixtures:
COPYFILE_DISABLE=1 python -m pytest -q --ignore=tests/test_regression.pytests/test_regression.py probes installed providers at collection time and
can create real resources when SDKs/authentication are available. Run
python -m pytest -q when you intend to exercise those integrations. Mocked
Daytona adapter tests also need the [daytona] extra and skip without it.
The test suite exercises Python/API behavior and dashboard asset contracts;
tests/dashboard_browser_app.py is a separate deterministic browser fixture.
| Path | Responsibility |
|---|---|
src/bespokelabs/sandbox/sandbox.py |
Direct SDK, lifecycle, workspace setup, structured output, agent cost tracking |
src/bespokelabs/sandbox/aio.py |
Thread-backed async client and session wrapper |
src/bespokelabs/sandbox/backends/ |
Nine lazy-loaded provider clients and sessions |
src/bespokelabs/sandbox/protocols.py, types.py, exceptions.py |
Backend contracts and shared public types/errors |
src/bespokelabs/sandbox/workspace.py, _transfer.py |
Manifest entries and directory transfer strategies |
src/bespokelabs/sandbox/agents.py, _agent_runtime.py, _usage.py |
Agent placement, command preparation, Claude/OpenCode usage parsing |
src/bespokelabs/sandbox/presets.py, pricing.py, pricing.json |
Preset configuration and bundled compute rates |
src/bespokelabs/sandbox/remote.py |
Standard-library HTTP client for the control plane |
src/bespokelabs/sandbox/control_plane/ |
FastAPI routes, tenant service, SQLite migrations/ledgers, supervision, dashboard assets |
images/, .github/workflows/build-images.yml |
Preset Dockerfiles and image publishing workflow |
tests/, examples/, docs/ |
Regression coverage, usage examples, API contract and operations runbook |
See DEVELOPMENT.md for design direction and compatibility
rules. It includes proposed integrations as well as implemented concepts;
the current public API is exported by src/bespokelabs/sandbox/__init__.py.
For server deployments, read the operations runbook.