thunder-sandbox, then import it under the short thunder alias.
Client, Sandbox, and Process classes support synchronous and asynchronous applications. Every blocking operation has an awaitable _async twin, such as Sandbox.create_async(), sandbox.exec_async(), and process.wait_async(). Properties such as id, status, and returncode remain synchronous.
Sandbox.create
Starts a sandbox and returns immediately. If an image is supplied, it is built or imported first. If command arguments are supplied, it waits for the sandbox and starts that command before returning.
Signature
Environment variables set for SSH sessions. Entries whose value is
None are omitted. Names must match [A-Za-z_][A-Za-z0-9_]*.Sandbox lifetime in seconds, measured from when the sandbox becomes ready. Time spent queued or starting doesn’t count. Use
None to disable automatic expiry.Waits for capacity instead of failing with
CapacityError. Without command arguments, create returns right away while the sandbox waits. See Queueing.GPU type, such as
thunder.GPUType.H100. Supply it together with gpu_count. For a CPU-only sandbox, leave out both GPU options or pass thunder.GPUType.NONE with gpu_count=0.Number of GPUs. Currently, sandboxes support one GPU. Supply it together with
gpu_type. Contact us if you are interested in larger configurations.Optional container image to run commands in. See Images.
Workspace name or
ws- ID to create the sandbox in. Defaults to TNR_WORKSPACE, then the main workspace. See Workspaces.InvalidRequestError.
SSH authentication uses a workspace certificate as described in SSH authentication.
Public internet access is allowed by default. Private, metadata, and other sandbox networks remain blocked, and inbound access is limited to SSH.
outbound_domain_allowlist=["*"] allows TLS to any domain. outbound_cidr_allowlist=["0.0.0.0/0"] allows every public address, so a domain allowlist beside it restricts nothing. Passing neither leaves the sandbox open.
Domain allowlists match TLS connections on the server name, so plain HTTP to an allowed domain fails. For a plain-HTTP host, such as an apt mirror, allow its address range with outbound_cidr_allowlist. A bare domain matches exactly; use *. for its subdomains.
To make creation safe to retry, give the sandbox a unique name. If a create request is interrupted and you can’t tell whether it succeeded, Sandbox.from_name finds the sandbox if it exists:
Queueing
Withqueue=True, a sandbox waits for capacity instead of failing with CapacityError. When creating with image=..., the image build or import is admitted without a capacity check too.
create returns once any image is ready, while the sandbox is still queued. Call sandbox.refresh() to update sandbox.queued. terminate() cancels a queued sandbox. With command arguments, create still waits for readiness within its timeout, so for long queue waits create a handle and call wait_until_ready(timeout=None).
Workspaces
Workspaces divide an organization’s sandboxes and images, and each member has a role in each workspace. Calls that create, list, or find sandboxes by name take aworkspace name or ws- ID and use the first of:
- The
workspaceargument. - The
TNR_WORKSPACEenvironment variable. main, which every organization has.
client.get_sandbox(id) finds a sandbox in any workspace you can access, and everything you then do with it follows that sandbox. Viewers can read a workspace’s sandboxes. Starting, stopping, and connecting to them, and building or importing images there, takes the developer role.
list_sandboxes covers one workspace. Names can change, so long-lived code such as a cleanup job should store workspace IDs.
Images
By default, commands run directly in the sandbox’s Ubuntu VM. Pass anImage to run them inside a container instead. The image is built or imported by Thunder before the sandbox is allocated.
Signature
username and password together for a private registry. from_dockerfile uploads the directory as the build context, honoring .dockerignore, up to 5 GiB.
An image-backed sandbox runs the image’s ENTRYPOINT followed by its CMD once at startup. After that command exits or fails, the sandbox stays available for exec and file transfers until you terminate it or its lifetime expires. exec, upload, and download operate inside the container. sandbox.image_id reports the managed image in use.
Images that start a server will start it, and inherited defaults count too. To keep an inherited command from running, clear both defaults:
Sandbox.create run as a separate exec command after the sandbox is ready. They don’t replace the image’s command, which may still be running or may already have exited. sandbox.wait() and sandbox.poll() don’t track the image’s command, and the SDK doesn’t expose its output or exit status.
To prepare an image ahead of time, call client.resolve_image. It returns a ResolvedImage once the image is ready and raises SandboxFailedError if the build or import fails.
Signature
spec to check capacity for them before building. The check doesn’t reserve anything. Sandbox.create passes its resources automatically. The image is built or imported in workspace, and ResolvedImage.workspace_id reports which workspace lists it. An organization builds each recipe once, so a Dockerfile or registry digest already built in another workspace comes back as the same image, ready at once.
Sandbox.update_network_policy
Replaces the complete outbound policy of a running sandbox. It accepts the same network options as Sandbox.create.
Signature
timeout defaults to 300 seconds; pass None to wait indefinitely. A timeout raises SandboxTimeoutError but doesn’t undo an accepted update. A superseded policy raises ConflictError. Existing user connections are revoked during a policy change; SSH stays available.
Sandbox.from_id
Retrieves a sandbox by its permanent API ID.
Signature
Sandbox.from_name
Finds the live sandbox with the given name. It raises NotFoundError when no live sandbox has that name and ConflictError if the name is ambiguous.
Signature
Sandbox.from_id when you know the ID.
Sandbox handles expose these properties:
sandbox.info carries timezone-aware lifecycle timestamps: created_at, ready_at, stopped_at (finished or failed), and expires_at. A timestamp is None until its transition happens. Values are as of the last fetch; call sandbox.refresh() to see newer transitions.
SSH authentication
The SDK authenticates your machine with one Ed25519 key and a short-lived SSH certificate for each workspace. The private key remains on your machine. The API signs its public half, and every sandbox in the workspace trusts that authority, so you can reconnect to existing sandboxes from any authenticated machine. The credential is created lazily on the first SSH operation, reused across sandboxes, and renewed automatically before expiry. The SDK caches it under~/.thunder/sandbox_keys/ by default:
main workspace. Each other workspace adds its own certificate beside the shared key, as id_ed25519-<hash>-cert.pub and id_ed25519-<hash>-cert.json.
Set TNR_HOME to move the cache. When the cache directory is unwritable, the SDK holds the key and certificate in memory for command execution and file transfers.
sandbox.ssh exposes the host, port, user, and cached credential paths. sandbox.ssh_command returns a tuple suitable for subprocess or shell display, including the private key, certificate, and options required for short-lived hosts:
exec, upload, or download before shelling out. Manual SSH requires a writable cache directory. Because nodes reuse forwarded ports, the generated command sends host-key records to /dev/null; the SDK pins a sandbox’s host key in memory for the life of the process.
Sandbox.wait_until_ready
Waits until the sandbox reports ready. Thunder holds each request open until the sandbox is ready, so it returns moments after startup finishes. Transient connection, service, and rate-limit errors are retried for a short grace period. It raises SandboxFailedError for a terminal sandbox and SandboxTimeoutError at the deadline.
timeout=None to wait indefinitely, for example for a queued sandbox.
Sandbox.refresh
Fetches the current sandbox state and updates the handle in place.
Sandbox.poll
Checks whether the sandbox or its main command has finished without blocking. It returns None while running, 0 when stopped successfully, or 1 when the sandbox failed.
Sandbox.wait
Waits for the command passed to Sandbox.create(*args) or, when no command was provided, for the sandbox to stop.
Signature
SandboxTimeoutError.
Sandbox.exec
Starts a command over SSH. Pass the executable and each argument separately.
Signature
Account to run the command as in an image-backed sandbox: a username or UID, optionally followed by a group or GID, such as
"root", "1000", or "1000:1000". Commands run as root by default, regardless of the image’s USER. Passing user without an image raises InvalidRequestError.Requests a pseudo-terminal for interactive commands. PTY commands stay attached to their SSH connection and are not durable.
Keeps the command’s output on the sandbox after it has been read, so it can be recovered later with
get_process. Call process.cleanup() when finished.Process exposes id, stdin, stdout, stderr, returncode, is_durable, poll(), wait(), cleanup(), and terminate(). stdout and stderr remain readable after the process finishes. A process wait deadline raises asyncio.TimeoutError; Sandbox.wait() converts a timeout from the command passed to Sandbox.create(*args) into SandboxTimeoutError.
By default, commands run as durable jobs that do not depend on the SSH connection that started them. If the connection drops, the SDK reconnects and keeps reading the same job’s output without gaps or duplicates, rather than starting the command again. An operation that keeps failing for 10 minutes raises ConnectionError; the job keeps running, and get_process can recover it. Once the job has finished and its captured output has been read, the SDK removes the job’s files from the sandbox unless retain=True.
In an image-backed sandbox, a command given by bare name runs the image’s own program, as under docker exec. So sh is the image’s shell, which is dash on Ubuntu and Debian and doesn’t support [[ ]], set -o pipefail, or source; call bash -c for scripts that need them. If the image has no such program, the sandbox’s BusyBox provides it when it can.
Each argument or environment value must be under 128 KiB, the Linux limit; exec raises InvalidRequestError above that. Upload larger input as a file.
Durable commands do not accept stdin: stdin.write() raises io.UnsupportedOperation. Pass input through arguments, environment variables, or uploaded files. Set pty=True for programs that need interactive input or terminal behavior; a PTY command whose SSH connection drops raises ConnectionError and its final state is unknown, so use the default mode for unattended or long-running work.
text=True, streams read and write strings. With text=False, they use bytes. terminate() stops the remote command while the sandbox continues running. For a durable command, it sends SIGTERM to the whole process group and then SIGKILL after five seconds, so wait() returns 143 or 137.
Sandbox.get_process
Recovers a durable command by its process.id, for example from another Python process or after a restart.
Signature
retain=True to exec when you plan to recover a command after its output has already been read. PTY commands cannot be recovered.
Sandbox.upload
Uploads a local file or directory over the sandbox’s authenticated SSH connection. Files are written to a staging path and renamed into place when complete, so partial data never appears at the destination. If the connection drops, the SDK reconnects and repeats the transfer.
Signature
recursive=True when uploading a directory. Pass timeout= in seconds to bound the transfer; it raises SandboxTimeoutError when it expires.
Sandbox.download
Downloads a remote file or directory over the sandbox’s authenticated SSH connection. Like upload, it writes to a staging path first and repeats the transfer after a dropped connection.
Signature
recursive=True when downloading a directory. timeout= works as it does for upload.
Sandbox.terminate
Stops the sandbox and destroys its filesystem. A queued sandbox is cancelled immediately. If the sandbox is still starting, timeout controls how long to wait for it to become stoppable. Use None to wait indefinitely.
Signature
Client.from_cli
Creates an authenticated client from the Thunder CLI configuration.
TNR_API_TOKEN. TNR_API_URL overrides the API endpoint, TNR_HOME overrides the directory containing CLI state and the shared sandbox SSH credential, and TNR_WORKSPACE sets the workspace for calls that don’t name one.
Use the client as a context manager to close it automatically:
Client.create_sandbox
Creates a sandbox using this client’s authentication and configuration. It accepts the same arguments as Sandbox.create.
Client.get_sandbox
Retrieves a sandbox by its permanent API ID.
Client.get_sandbox_by_name
Finds a live sandbox by its reusable name. Prefer Client.get_sandbox when you know the ID.
Client.list_sandboxes
Iterates over one workspace’s active sandboxes by default. Results are newest first, and pagination is automatic. Pass "all" for complete history or one of "created", "ready", "finished", or "failed" for a single lifecycle status.
Signature
Client.list_workspaces
Returns the workspaces your credential can reach, each with its id, name, and your role ("viewer" or "developer").
Client.get_pricing
Fetches the current USD per-hour rates as a Pricing object. memory_gb and storage_gb are per GiB, and gpu is keyed by GPUType.
sandbox.get_price_per_second() returns a sandbox’s total USD per second for all of its configured resources: the hourly rates divided by 3600. It is a rate, not accrued charges. Billing runs from sandbox.info.ready_at to sandbox.info.stopped_at or the end of the sandbox’s lifetime, whichever comes first. Time spent queued or starting isn’t billed.
Client.close
Closes the client. Later API calls through that client raise ConnectionError.
_async suffix.
Errors
Errors
All SDK errors inherit from
ThunderError. CapacityError, RateLimitError, and ServiceUnavailableError also inherit from RetryableError, so you can catch every transient condition at once.API-backed errors expose the HTTP
status, machine-readable code, and optional retry_after value.