Skip to main content
Install thunder-sandbox, then import it under the short thunder alias.
The same 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
string
Optional command and arguments to start once the sandbox is running.
string
Optional label, unique among live sandboxes. Use the returned id to address the sandbox.
Mapping[str, str | None]
Environment variables set for SSH sessions. Entries whose value is None are omitted. Names must match [A-Za-z_][A-Za-z0-9_]*.
int | None
default:"300"
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.
boolean
default:"false"
Waits for capacity instead of failing with CapacityError. Without command arguments, create returns right away while the sandbox waits. See Queueing.
integer
default:"4"
Number of vCPUs.
integer
default:"32"
Memory in GiB.
integer
default:"50"
Ephemeral disk size in GiB.
GPUType
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.
integer
Number of GPUs. Currently, sandboxes support one GPU. Supply it together with gpu_type. Contact us if you are interested in larger configurations.
Image
Optional container image to run commands in. See Images.
boolean
default:"false"
Blocks outbound internet access. Choose either this option or an allowlist.
Sequence[str]
IPv4 CIDRs the sandbox may reach.
Sequence[str]
Domains the sandbox may reach over TLS. Use *.example.com for subdomains.
string
Workspace name or ws- ID to create the sandbox in. Defaults to TNR_WORKSPACE, then the main workspace. See Workspaces.
Client
Reuse an existing authenticated client.
CPU, memory, and storage are whole numbers. The API’s resource catalog sets the allowed range for each and rejects a value outside it with 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.
Passing either allowlist restricts outbound traffic to what you list, and an allowlist you leave out allows nothing. The two lists add together: a connection is allowed when either list permits it. Supply both when a workload needs direct IP and domain access. To leave one dimension open, pass its open value explicitly. 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

With queue=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.
Without command arguments, 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 a workspace name or ws- ID and use the first of:
  1. The workspace argument.
  2. The TNR_WORKSPACE environment variable.
  3. main, which every organization has.
A sandbox ID needs no workspace: 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 an Image to run them inside a container instead. The image is built or imported by Thunder before the sandbox is allocated.
Signature
Supply 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:
Command arguments passed to 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
Pass the planned resources as 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
Allowlists follow the same rules as at creation: an allowlist you leave out allows nothing, unless you pass neither. Each call replaces the complete policy rather than merging with the previous one. The call waits until Thunder confirms the new policy is enforced. 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
Use IDs when storing or reconnecting to a sandbox. Names are reusable labels.

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
Names are unique only while a sandbox is live and may be reused after it stops. Prefer 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:
These are the files for the 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:
The command uses the cached files, so run at least one SDK SSH operation such as 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.
Pass 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
A deadline raises SandboxTimeoutError.

Sandbox.exec

Starts a command over SSH. Pass the executable and each argument separately.
Signature
string
required
Command followed by its arguments.
float | None
Default timeout used by process.wait().
string
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.
string
Remote working directory.
Mapping[str, str | None]
Variables for this command. A None value unsets a variable.
boolean
default:"true"
Returns text streams when true and byte streams when false.
boolean
default:"false"
Requests a pseudo-terminal for interactive commands. PTY commands stay attached to their SSH connection and are not durable.
"capture" | "discard"
default:"capture"
Captures standard output, or sends it to /dev/null.
"capture" | "discard"
default:"capture"
Captures standard error, or sends it to /dev/null.
boolean
default:"false"
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.
The returned 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.
With 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
Pass 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
Set 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
Set 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.
For headless use, set 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 methods have the same behavior and options as their synchronous twins and return the same public object types. Stream operations also use the _async suffix.
Async stdin follows the same pattern:
Clients support both synchronous and asynchronous context managers. Async sandbox listings are async iterators:
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.