Sandbox SDK for Python and TypeScript
The sandbox SDKs drive the Agent GPU Sandbox from your own code instead of through an AI connection. One call deploys a GPU VM with the sandbox on and waits until it is ready. Then you run shell commands on it, stream their output while they run, and read and write files, all with a GPU VM API key. This page covers installing the SDKs, creating a sandbox, commands, files, lifecycle and errors.
Use it from training scripts, notebooks, CI jobs and agent frameworks. A sandbox is a real VM that you keep for hours, so files, packages and models stay on it between jobs, and your own SSH key keeps working the whole time. A VM takes a few minutes to boot, so the sandbox suits training, fine-tuning, evals and benchmarks, not quick one-off snippets.
Prerequisites
- A GPU VM API key from Settings > API Keys. See User settings.
- An SSH key saved in Settings > SSH Keys, or a public key to pass for one instance. The sandbox never replaces your own access.
- Credit on your account for the GPU time. See Billing.
- Python 3.9 or later, or Node.js 18 or later (Bun and Deno also work).
Install the SDK
pip install spheron-sandboxExport your API key. Both SDKs read it from SPHERON_API_KEY:
export SPHERON_API_KEY=<your-api-key>Quick start
This example deploys the cheapest H100 offer up to $3 per hour, runs a training job and terminates the instance:
from spheron_sandbox import Sandbox
# Deploys and waits until commands can run (a few minutes)
sandbox = Sandbox.create(gpu="h100", gpu_count=1, max_hourly_price=3)
print(sandbox.commands.run("nvidia-smi -L").output)
sandbox.files.write("/root/train.py", open("train.py").read())
job = sandbox.commands.start("cd /root && python train.py", timeout_sec=6 * 3600)
for chunk in job.stream():
print(chunk, end="")
metrics = sandbox.files.read("/root/metrics.json")
sandbox.terminate()The instance bills at its normal hourly price from the moment it runs until you stop or terminate it. See Billing.
Create a sandbox
Sandbox.create() lists the GPU offers, deploys the cheapest one that matches your filter with the sandbox on, and waits until it is ready. When an offer is sold out or cannot take the sandbox, it moves on to the next one. A refused offer creates nothing and bills nothing.
| Python | TypeScript | Default | Description |
|---|---|---|---|
gpu | gpu | any | GPU model as one word: h100, a100, l40s, rtx4090 |
gpu_count | gpuCount | any | GPUs per instance |
max_hourly_price | maxHourlyPrice | none | Most to pay per hour for the whole instance, in USD |
spot | spot | False | Spot offers only. Cheaper, and can be reclaimed at any time. Without it, only dedicated offers are used. |
regions, providers | regions, providers | all | Narrow the search |
offer | offer | Deploy this exact offer from find_offers() | |
operating_system | operatingSystem | Ubuntu with CUDA | Matched against the offer's images |
team_id | teamId | your only team | Required when your key reaches more than one team |
ssh_key_id | sshKeyId | first saved key | A saved SSH key |
ssh_public_key | sshPublicKey | A public key for this instance only. Not saved. | |
volume_ids | volumeIds | Volumes to attach at launch | |
wait_until_ready | waitUntilReady | True | Return as soon as the deploy is accepted when false |
ready_timeout (seconds) | readyTimeoutMs | 20 minutes | How long to wait for the sandbox |
on_status | onStatus | Called with deploying, installing, then ready |
Pick an offer yourself
List the offers first, then deploy the one you want:
offers = Sandbox.find_offers(gpu="a100", max_hourly_price=2)
for o in offers:
print(o["provider"], o["gpuCount"], o["clusters"], o["price"])
sandbox = Sandbox.create(offer=offers[0])Connect to an existing sandbox
Pick up a sandbox again by its deployment ID, from any process or machine:
sandbox = Sandbox.connect("<deployment-id>")This works for any instance you deployed with the sandbox on, through the SDK, the API, or your AI agent.
Run commands
Commands run with bash as the instance's login user, with sudo available. Output is stdout and stderr together, up to 1 MB per command. The full log stays on the instance at the path in log_path (logPath in TypeScript). Read it with sandbox.files.read(r.log_path).
A finished command's status is done when it exits 0. It is failed for a non-zero exit (with the exit code), for a command killed by its timeout (error is timed out), or for a command the instance lost. r.ok is true only for done with exit code 0.
# Wait for it to finish. Killed after timeout_sec (default 600 seconds).
r = sandbox.commands.run("pip install -r requirements.txt", cwd="/root/app", timeout_sec=1800)
print(r.exit_code, r.output)
# Raise CommandExitError unless it exits 0
sandbox.commands.run("pytest -q", check=True)
# Print output while it runs
sandbox.commands.run("python bench.py", on_output=lambda chunk: print(chunk, end=""))
# Pass environment variables
sandbox.commands.run("python eval.py", env={"HF_TOKEN": "<your-token>"})Run long jobs in the background
start() returns at once and keeps the command running for up to 86,400 seconds (one day). Use the handle to stream its output, wait for it, or stop it:
job = sandbox.commands.start("python train.py")
print(job.id) # keep it to come back later
for chunk in job.stream(): # stop iterating any time; the job keeps running
print(chunk, end="")
job.wait(timeout=60) # raises TimeoutError after 60 seconds; the job keeps running
job.kill() # stops it and every process it startedPick a job up again later, for example from another script:
job = sandbox.commands.get("<command-id>")
result = job.wait()A command sent while the instance is briefly unreachable stays queued and runs once the sandbox is back. Do not send it again.
Read and write files
sandbox.files.write("/root/config.yaml", "lr: 0.001\n")
sandbox.files.write("/root/run.sh", script, mode="0755")
text = sandbox.files.read("/root/out.txt")
data = sandbox.files.read_bytes("/root/plot.png")
entries = sandbox.files.list("/root") # name, type and size of each entry
sandbox.files.exists("/root/checkpoint.pt")Paths are absolute. File operations run as root, and a file you write belongs to the instance's login user. Missing parent directories are created. Each read or write moves up to 5 MB, and an account moves at most two files at once (the SDK waits and retries). Download datasets and models on the instance itself:
sandbox.commands.run(
"huggingface-cli download meta-llama/Llama-3.1-8B --local-dir /root/model",
timeout_sec=3600,
)Manage the instance
| Python | TypeScript | What it does |
|---|---|---|
status() | status() | Sandbox state: installing, ready, failed or removed, and whether the instance is reachable |
info() | info() | The deployment: status, IP address, SSH command, GPU, hourly rate |
stop() | stop() | Stops the instance and waits until the provider has stopped it. It keeps its disk and bills only storage. |
start() | start() | Starts it again and waits until the sandbox is reachable. Retries for up to 10 minutes while the provider cannot start it yet, for example while a large machine is still saving its memory to disk. |
terminate() | terminate() | Deletes the instance and its disk. Billing stops. |
remove_sandbox() | removeSandbox() | Uninstalls the sandbox program. The instance keeps running. |
close() | Closes the HTTP connection. The instance keeps running. |
An instance must run for its offer's minimum runtime before you can terminate it. Before then, terminate() raises an error with the code minimum_runtime_not_met.
Use asyncio
The Python package has an asyncio client with the same methods:
from spheron_sandbox import AsyncSandbox
sandbox = await AsyncSandbox.create(gpu="h100", max_hourly_price=3)
r = await sandbox.commands.run("python eval.py")
job = await sandbox.commands.start("python train.py")
async for chunk in job.stream():
print(chunk, end="")
await sandbox.terminate()Handle errors
Every error extends SpheronError. Refusals from the API are SpheronApiError, with the HTTP status, the error code and the request ID.
| Error | When |
|---|---|
AuthenticationError | The API key is missing, wrong, or an Inference API key |
NotFoundError | No such instance, no sandbox on it, or a teammate deployed it |
InstanceNotRunningError | The instance is stopped, terminated or failed |
SandboxNotReadyError | The sandbox is still installing |
SandboxUnreachableError | Spheron cannot reach the instance. recovery holds a command to run over SSH. From commands.run() it also names the queued command (command_id, commandId in TypeScript). |
SandboxFailedError | The install failed or the instance died while you waited |
CommandExitError | check is on and the command exited non-zero |
NoOfferError | No offer matched, or every one tried was refused |
TimeoutError | A wait in the SDK ran out. The instance or command keeps running. |
The SDKs retry a safe request that hit a dropped connection or a 502, 503 or 504 with backoff (max_retries, maxRetries in TypeScript, default 3). A request refused with 429 is retried for up to 90 seconds, waiting as long as the API asks. File transfers queue in the SDK: at most 2 run at once per sandbox, the server's limit.
request_timeout (requestTimeoutMs in TypeScript) bounds a single HTTP request. Waits such as wait_until_ready() and job.wait() take their own timeout.
Use the REST API directly
The SDKs are built on the public API. To do the same with any HTTP client:
- Deploy with
"agentAccess": trueonPOST /api/deployments. - Wait until
GET /api/deployments/{deploymentId}/agentshows"state": "ready"and"reachable": true. - Run commands with
POST /api/deployments/{deploymentId}/agent/commands.
curl -X POST https://app.spheron.ai/api/deployments/<deployment-id>/agent/commands \
-H "Authorization: Bearer $SPHERON_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "nvidia-smi -L", "waitSec": 30}'Billing
The sandbox is free. You pay for the GPU instance at its normal price until you stop or terminate it, exactly as without the sandbox. A stopped instance bills only storage. If create() raises after the instance was made, for example because the install failed, the error's deployment_id (deploymentId in TypeScript) names the instance: terminate it if you no longer need it.
What's next
- Agent GPU Sandbox: how the sandbox works, its limits and security
- API reference: the sandbox endpoints the SDKs call
- AI connections: let Claude, Cursor or Codex use the sandbox for you
- Instance lifecycle: stop, start and terminate rules
Troubleshooting
Issue: The deploy is refused with agent_access_unsupported
Symptoms: create() with a hand-picked offer raises SpheronApiError with the code agent_access_unsupported.
Diagnosis: The offer accepts neither a startup script nor SSH setup, so the sandbox program cannot be installed.
Resolution: Pick an offer from another provider, or call create() with a filter instead of an offer so it moves on by itself.
Issue: create() raises SandboxFailedError
Symptoms: The instance was deployed, but create() raises SandboxFailedError.
Diagnosis: The sandbox install failed, or the instance failed while booting. The error's message says which.
Resolution: If the error has a recovery_command, SSH into the instance and run it. Otherwise terminate the instance with its deployment_id and create a new one.
Issue: Commands raise SandboxUnreachableError
Symptoms: File operations or commands.run() raise SandboxUnreachableError, or commands stay queued.
Diagnosis: The sandbox program stopped, or the instance lost its network connection.
Resolution: SSH into the instance and run the command in the error's recovery. Queued commands run once the sandbox is back. See Restore the sandbox.
Issue: NotFoundError on an instance you can see in the dashboard
Symptoms: Sandbox.connect() raises NotFoundError for an instance in your team.
Diagnosis: Only the person who deployed an instance can use its sandbox, and only if it was deployed with the sandbox on.
Resolution: Use the API key of the person who deployed it, or create a new sandbox.