Python SDK for the Sail sandbox platform
Project description
sail-sdk
Python SDK for Sail sailboxes, inference, and Voyages.
Sailboxes are persistent Linux sandboxes managed by Sail. Use the sail-sdk
Python package to create a VM, run shell commands, expose ports, checkpoint
state, pause, resume, and terminate it. The SDK also includes Voyage
observability helpers and thin inference wrappers for Sail's public REST API.
Sailboxes are currently in beta. APIs and operational behavior may change as we stabilize the product.
Install
pip install sail-sdk
uv add sail-sdk
Set your API key before using the SDK:
export SAIL_API_KEY=sk_...
Set SAIL_API_URL=https://... to override the REST API endpoint directly.
Voyages
Voyages are a flight recorder for long-running agent, evaluation, and background-task trajectories. Your harness owns the work loop. Sail records timeline events, spans, agent metadata, and correlated Sail inference calls.
import sail
sail.voyage.init(name="overnight eval")
sail.voyage.event("planner.started", message="planning")
with sail.voyage.agent("agent_solver", name="Solver", role="executor"):
with sail.voyage.span("call model"):
resp = sail.inference.responses.create(
model="openai/gpt-oss-20b",
input="Say hello in one sentence.",
)
sail.voyage.complete()
print(sail.voyage.id())
print(sail.voyage.dashboard_url())
sail.voyage.init() creates a Voyage and emits voyage.started. It also sets
the process-global current Voyage used by module-level helpers. If
SAIL_API_KEY is absent, sail.voyage becomes a safe no-op for local scripts:
no Voyage is created, no network calls are made, and id() /
dashboard_url() return None. Sailbox and inference APIs still require
SAIL_API_KEY.
Voyage Environment
| Variable | Purpose |
|---|---|
SAIL_API_KEY |
Required for real Voyage events and inference. Use an org-bearing sk_... key. |
SAIL_MODE |
Selects SDK routing mode. |
SAIL_API_URL |
Overrides the REST API URL directly. |
SAILBOX_ID |
Default sailbox_id attached to new Voyages. |
SAIL_VOYAGE_ID |
Attaches to an existing Voyage instead of creating one. |
SAIL_AGENT_ID |
Default event agent_id. |
SAIL_AGENT_NAME |
Default event agent_name. |
SAIL_AGENT_ROLE |
Default event agent_role. |
SAIL_VOYAGE_DEBUG |
Enables warnings for no-op mode, swallowed background flush failures, and dropped events. |
Events And Spans
voyage = sail.voyage.init(name="batch run", metadata={"suite": "nightly"})
voyage.event("planner.started", message="Building plan")
with voyage.span("solve task", agent_id="agent_1", name="solver"):
voyage.event("tool.called", payload={"tool": "search"})
voyage.complete(message="done")
Events are buffered locally. event() validates local input, enqueues quickly,
and does not raise network errors. A background flusher sends batches
periodically. flush(), complete(), and fail() block and raise delivery
errors. Process exit does a bounded best-effort flush only, so call
complete() or fail() for product-critical terminal state.
Span and agent contexts use Python contextvars, so they follow lexical
execution context. Raw threading.Thread does not inherit active span or agent
contexts automatically. The current Voyage itself is process-global, so raw
threads can still use the current Voyage for inference correlation.
Attach From Child Processes
A parent process can create the Voyage and pass SAIL_VOYAGE_ID plus
SAIL_API_KEY to a child process or Sailbox command.
import os
import subprocess
import sail
voyage = sail.voyage.init(name="parent run")
env = dict(os.environ)
env["SAIL_VOYAGE_ID"] = voyage.id
env["SAIL_API_KEY"] = os.environ["SAIL_API_KEY"]
subprocess.run(["python", "child.py"], env=env, check=True)
voyage.complete()
In the child:
import sail
sail.voyage.init()
sail.voyage.event("child.started")
sail.voyage.flush()
Calling init() with voyage_id=... or with SAIL_VOYAGE_ID set attaches to
the existing Voyage and does not emit a second voyage.started.
Inference
The SDK includes thin wrappers for Sail's inference endpoints. They POST the JSON payload as given and return the raw JSON response dict.
resp = sail.inference.responses.create(
model="openai/gpt-oss-20b",
input="hello",
)
chat = sail.inference.chat.completions.create(
model="openai/gpt-oss-20b",
messages=[{"role": "user", "content": "hello"}],
)
If a current Voyage exists, the wrappers add X-Sail-Voyage-Id so the
dashboard can correlate model calls. You can pass voyage=... to correlate
with an explicit Voyage, or call inference without any Voyage for normal
uncorrelated inference.
stream=True is not supported by the v0 wrapper. Use a raw client with
sail.voyage.headers() when you need streaming or a custom HTTP/OpenAI client.
import json
import os
import urllib.request
import sail
sail.voyage.init(name="raw-client-smoke")
api_urls = {
"prod": "https://api.sailresearch.com",
"dev": "https://dev.sailresearch.com",
"staging": "https://staging.sailresearch.com",
"local": "https://dev.sailresearch.com",
}
mode = os.environ.get("SAIL_MODE", "prod").strip().lower() or "prod"
api_url = os.environ.get("SAIL_API_URL", "").strip() or api_urls[mode]
headers = sail.voyage.headers({"Content-Type": "application/json"})
headers["Authorization"] = "Bearer " + os.environ["SAIL_API_KEY"]
req = urllib.request.Request(
api_url.rstrip("/") + "/v1/responses",
data=json.dumps({"model": "openai/gpt-oss-20b", "input": "hello"}).encode(),
headers=headers,
method="POST",
)
Voyage Limitations
The v0 Voyage SDK does not include native async helpers, streaming inference wrappers, an agent framework, orchestration, swarms, memory graphs, tool abstractions, an OpenAI client factory, or Sailbox auto-binding.
Create a sailbox
Sailboxes are persistent sandboxes that can run indefinitely, pause and resume their writable disk, memory, and open network connections, and automatically sleep while Sail inference calls are in flight. You are only charged while a sailbox is active, making them efficient for bursty agent and tool workloads.
import sail
app = sail.App.find(name="example-app", mint_if_missing=True)
sb = sail.Sailbox.create(
app=app,
image=sail.Image.debian_arm64,
name="sandbox-1",
cpu=1,
memory_mib=1024,
disk_gib=8,
)
print(sb.sailbox_id)
print(sb.status)
Sailbox.create() returns after the VM is running. The main create arguments
are:
| Argument | Default | Description |
|---|---|---|
app |
Required | A sail.App, usually from sail.App.find() |
image |
Required | A sail.Image value or custom image definition |
name |
Required | Human-readable sailbox name |
image_build_timeout |
1800 |
Seconds to wait for custom image builds before creating the VM |
cpu |
1 |
vCPU count; must be greater than 0 |
memory_mib |
1024 |
Memory in MiB; must be between 1024 and 65536 |
disk_gib |
8 |
Writable state disk in GiB; must be between 1 and 64 |
ingress_ports |
None |
Guest ports to expose publicly |
Sailboxes should use sail.Image.debian_arm64 or sail.Image.debian_arm. We
have plans to support AMD64 images soon - please contact us if you would like
us to prioritise this.
Run commands
exec() starts a shell command and returns a SailboxExecRequest. Call wait()
to retrieve stdout, stderr, and the return code.
result = sb.exec("echo hi", timeout=5).wait()
print(result.stdout)
print(result.stderr)
print(result.returncode)
timeout is the command runtime budget in seconds. Omit it to run without an
SDK-provided runtime limit. Only one exec request can be active on a sailbox at
a time; if another command is running, the SDK raises
sail.SailboxExecAlreadyRunningError.
Pass cwd to run the command from a specific working directory. With
background=True, Sail launches the process through a detached shell and
wait() only waits for that launcher to succeed.
Run Python functions
Decorate a Python function with @sail.function() and pass it to exec() to
run it inside the sailbox. For functions, exec() waits for completion and
returns the function's return value directly. Sail runs the function with the
image's python3.
@sail.function()
def add(x: int, y: int) -> int:
return x + y
value = sb.exec(add, 2, 3, timeout=30)
print(value) # 5
Python functions are currently only supported for sailboxes running custom images. We plan to remove this limitation shortly.
Function execution is synchronous. background=True is not supported for
functions. Remote exceptions are raised as sail.SailboxFunctionError with the
remote traceback attached.
This beta path sends serialized function payloads and return values through the existing exec RPC. Keep arguments and return values small; for large dataframes or artifacts, write data from inside the sailbox and return a small reference.
Expose ports
Pass ingress_ports when creating the sailbox, then start a service inside the
VM and fetch its listener URL.
sb = sail.Sailbox.create(
app=app,
image=sail.Image.debian_arm64,
name="web-demo",
ingress_ports=[3000],
)
sb.exec("python3 -m http.server 3000", background=True, cwd="/srv/app").wait()
listener = sb.listener(3000)
listener.wait(timeout=60)
print(listener.url)
print(listener.route_status)
Use listeners() to list every exposed port:
for listener in sb.listeners():
print(listener.port, listener.url, listener.route_status)
Ports must be unique and between 1 and 65535. Ports 22 and 10000 are
reserved by the platform.
Long-running services
Create a normal sailbox and start long-running processes with background exec.
There is no separate daemon creation API. Existing guest-to-egress connections can
remain open while background processes run. Sailboxes are checkpointed only when
you call checkpoint(), pause(), or sleep(), or when Sail receives an AWS
preemption signal for the host.
sb = sail.Sailbox.create(
app=app,
image=sail.Image.debian_arm64,
name="web-daemon",
ingress_ports=[3000],
)
sb.exec("python3 -m http.server 3000", background=True, cwd="/srv/app").wait()
sb.checkpoint()
Lifecycle
sb.checkpoint() # Snapshot while keeping the sailbox running
sb.pause() # Checkpoint and pause until explicit resume
sb.sleep() # Checkpoint and sleep until network ingress, exec, or resume
sb.resume() # Resume a paused or sleeping sailbox
sb.terminate() # Permanently destroy the sailbox
After pause(), exec() raises sail.SailboxExecutionError until resume()
succeeds. Network traffic does not wake a paused sailbox; only resume() does.
After sleep(), network ingress, exec(), or resume() wakes the sailbox from
its latest checkpoint.
terminate() is permanent.
Sailboxes preserve their writable disk, in-memory state, and in-flight network requests across checkpoints and resumes.
We highly recommend calling sb.checkpoint() after any non-deterministic
command. On host failure, Sail can provide best-effort recovery by replaying
commands from the latest checkpoint. Checkpointing immediately after a
non-deterministic command allows you to avoid diverging behaviour on replays.
To sleep a sailbox automatically while a foreground Sail inference call is in
flight, include its ID in the request with the X-SailboxId header. Sail will
resume the sailbox after the inference call completes.
response = sail.inference.responses.create(
model="zai-org/GLM-5",
input="Summarize the current workspace state.",
background=False,
headers={"X-SailboxId": sb.sailbox_id},
)
Custom images
Start from the arm64 Debian base image, add build steps, and pass the image
definition to Sailbox.create():
image = (
sail.Image.debian_arm64
.apt_install("git", "curl")
.pip_install("requests")
.run_commands("python3 -m pip show requests >/tmp/requests.txt")
.env({"APP_ENV": "demo"})
)
sb = sail.Sailbox.create(
app=app,
image=image,
name="custom-image-demo",
image_build_timeout=1800,
)
Image definitions are immutable; each helper returns a new definition. Supported
helpers are apt_install(*packages), pip_install(*packages),
run_commands(*commands), env(dict[str, str]), and build(timeout=1800). The
SDK retries short-lived imagebuilder socket-close transport races during
BuildImage and status polling, but retry sleeps are capped by the same
timeout or image_build_timeout budget.
Calling build() eagerly builds the image before you create a sailbox. If the
same image was already built, the cached image is returned. This step is
optional because Sailbox.create() will build custom image definitions before
creating the VM.
API surface
Common exported SDK types:
| Type | Description |
|---|---|
sail.App |
Sail application namespace; use App.find(name=..., mint_if_missing=True) |
sail.Image |
Image namespace with debian_arm64 and debian_arm |
sail.SailFunction |
Decorated Python function accepted by Sailbox.exec() |
sail.Sailbox |
Standard sailbox handle |
sail.SailboxExecRequest |
Durable exec request returned by exec() |
sail.SailboxExecResult |
stdout, stderr, and returncode from wait() |
sail.SailboxListener |
Public listener metadata for an exposed guest port |
sail.voyage |
Module-level Voyage helpers for init, events, spans, agents, terminal state, and headers |
sail.Voyage |
Object API returned by sail.voyage.init() |
sail.inference |
Thin raw JSON wrappers for /v1/responses and /v1/chat/completions |
Common exceptions:
| Exception | Raised when |
|---|---|
sail.SailError |
Base SDK error |
sail.SailboxError |
Base sailbox-specific error |
sail.SailboxCreationError |
Creation, checkpoint, stop, start, or scheduler lifecycle operation fails |
sail.SailboxExecutionError |
Exec or listener operation fails |
sail.SailboxExecAlreadyRunningError |
Another exec request is already active |
sail.SailboxExecRequestNotFoundError |
A durable exec request cannot be found |
sail.SailboxFunctionError |
A decorated Python function raised while running in a sailbox |
sail.SailboxFunctionSerializationError |
Function payload, runtime setup, or return-value serialization fails |
sail.SailboxTerminatedError |
The sailbox no longer exists on the worker |
sail.SailboxWorkerLostError |
The assigned worker was lost during exec wait |
sail.ImageBuildError |
Custom image build fails |
sail.VoyageError |
Voyage validation, buffering, or delivery fails |
sail.VoyageHTTPError |
Voyage API returns an HTTP error |
sail.VoyageNotFoundError |
Attaching to a Voyage fails because it is not visible to the API key |
sail.InferenceError |
Inference wrapper config or v0 feature validation fails |
sail.InferenceHTTPError |
Inference endpoint returns an HTTP error |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sail_sdk-0.1.13.tar.gz.
File metadata
- Download URL: sail_sdk-0.1.13.tar.gz
- Upload date:
- Size: 81.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8180199916e61009459f631fe74ff9488015841faeee601f711d2f188d9f8cc0
|
|
| MD5 |
c687d8240524082e75fbe84d214eb252
|
|
| BLAKE2b-256 |
deb10276b8d4ccdb2ec627ce92be65e2575e098b6b72af9c4aa457d6c906614c
|
File details
Details for the file sail_sdk-0.1.13-py3-none-any.whl.
File metadata
- Download URL: sail_sdk-0.1.13-py3-none-any.whl
- Upload date:
- Size: 58.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5fc520ba113bc4bcc3af27bb1aafa7f5e6502f710e85aff72205e5c6b49deccf
|
|
| MD5 |
2aa3e876bd06cfc5e584e35e687e57ad
|
|
| BLAKE2b-256 |
b594b9c0976d9368c93b4f9e3b75d9cb2e38ab351dea85d1933effabb56d6fd3
|