Skip to main content

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. Set SAILBOX_API_URL=https://... to override the Sailbox lifecycle API host, and SAILBOX_INGRESS_URL=https://... to override the exact path-based listener ingress used when workerproxy does not return a public URL.

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(name=...) creates a Voyage and emits voyage.started. It also sets the process-global current Voyage used by module-level helpers. New Voyage creation requires a non-empty name; pass optional integer version when the harness, prompts, model mix, or other workflow definition changes. 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_API_URL Overrides the Sailbox lifecycle API URL directly.
SAILBOX_INGRESS_URL Overrides exact path-based Sailbox listener ingress fallback.
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",
    version=2,
    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; attach calls do not require name or version.

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 usually use sail.Image.debian_arm64 or sail.Image.debian_arm, which pin the image to your local Python version so function execution can deserialize local Python bytecode. Use sail.Image.builtin_debian_arm64 when you specifically need the stable builtin rootfs without a Python-versioned imagebuilder base.

Reconnect to an existing sailbox by id with Sailbox.connect():

sb = sail.Sailbox.connect("sb_...")

connect() returns a full Sailbox handle that can run commands, read and write files, manage listeners, and make network requests. It verifies the current placement for a running sailbox and resumes a paused or sleeping sailbox before returning.

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. Multiple exec requests can run on the same sailbox at the same time; coordinate access to shared files, ports, and processes in your own commands when they overlap.

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.

Read and write files

Use write() to upload bytes, strings, or file-like objects into the sailbox filesystem and read() to fetch regular files back as bytes. Paths must be absolute. Missing parent directories are created by default. Pass mode to set POSIX permission bits; when omitted, writes default to 0o644.

sb.write("/workspace/input.txt", "hello\n")

data = sb.read("/workspace/input.txt")
print(data.decode())

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.endpoint.url)
print(listener.route_status)

Use listeners() to list every exposed port:

for listener in sb.listeners():
    print(listener.port, listener.endpoint, listener.route_status)

Ports must be unique and between 1 and 65535. Port 10000 is reserved by the platform; port 22 is reserved for HTTP but available for raw TCP ingress.

Pass sail.IngressPort(port, "tcp") to expose a port as raw TCP (SSH, Postgres, or any TCP protocol); its listener resolves to a TcpEndpoint with a host and port any client dials directly.

A raw-TCP port is reachable from the public internet with no platform-side auth — the in-guest daemon is the only access control. Pass cidr_allowlist=["203.0.113.0/24", ...] to restrict which source IPs may connect (each port carries its own allowlist). Exposing a well-known service port (a database, cache, or search engine) with no allowlist is rejected; set cidr_allowlist, or pass allow_public=True to confirm you intend it reachable by the whole internet.

For SSH, pass ssh=True to create (or --enable-ssh on the CLI): it installs your key, exposes port 22, and starts the SSH server in one call. Read the port-22 listener's endpoint for the ssh -p <port> root@<host> command to connect. See the sailbox docs for details.

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
child = sb.fork(name="rollout-1")  # Branch memory and writable disk into a new sailbox
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.

fork() creates a separate running sailbox from the current in-memory process state and writable disk. The child gets new Sail identity and networking. Active TCP connections are reset in the child, while listening sockets can accept new connections after you expose routes for the child.

We recommend calling sb.checkpoint() after important setup or state changes. On host failure, Sail restores from the most recent completed checkpoint; it does not replay commands that ran after that checkpoint. Changes made after the latest checkpoint may be lost if the host fails before the next checkpoint completes.

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, debian_arm, and builtin_debian_arm64
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

The monitoring reads (Sailbox.get, Sailbox.list), the lifecycle methods (checkpoint, pause, sleep, resume, terminate), and the listener methods raise builtin exceptions — LookupError (unknown or terminated sailbox, or an unexposed port), PermissionError (auth failure), ValueError (malformed argument), and RuntimeError (transient backend failure) — so they compose with ordinary except blocks. The Sail* exceptions below cover creation, exec(), and image builds:

Exception Raised when
sail.SailError Base SDK error
sail.SailboxError Base sailbox-specific error
sail.SailboxCreationError Sailbox.create() fails
sail.SailboxExecutionError An exec() or request() operation fails
sail.SailboxExecAlreadyRunningError Legacy error returned by older backends when 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sail_sdk-0.1.36.tar.gz (223.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sail_sdk-0.1.36-py3-none-any.whl (132.9 kB view details)

Uploaded Python 3

File details

Details for the file sail_sdk-0.1.36.tar.gz.

File metadata

  • Download URL: sail_sdk-0.1.36.tar.gz
  • Upload date:
  • Size: 223.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","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

Hashes for sail_sdk-0.1.36.tar.gz
Algorithm Hash digest
SHA256 bb052140b12452fcefe38682007c77ad19928eb29f00551a04a3bc88896fc1e6
MD5 222b72d4c944fabf50657019336a0378
BLAKE2b-256 30b71b5fb3c9fcd289bcc70de1521c5296e60de5e96b9476c3547b3b2a154481

See more details on using hashes here.

File details

Details for the file sail_sdk-0.1.36-py3-none-any.whl.

File metadata

  • Download URL: sail_sdk-0.1.36-py3-none-any.whl
  • Upload date:
  • Size: 132.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","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

Hashes for sail_sdk-0.1.36-py3-none-any.whl
Algorithm Hash digest
SHA256 5bb1145db6c45e63e935e50025f26441e3d65fcda29060bace41c98fa6075f5c
MD5 e4669ca227df85509b75e6500b58614a
BLAKE2b-256 b651a315620de035e47020a0887f1f5cbe32c0c1ae35bd40d259ee8eaee4b217

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page