Skip to main content

Install

pip install sail-sdk
Set your API key before using the SDK:
export SAIL_API_KEY=sk_...

Create a sailbox

Create a Sail app namespace, then start a sailbox from the Debian arm64 image:
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. Pass cpu, memory_mib, and disk_gib to size the sailbox, and ingress_ports to expose services.
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.
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 durable exec request. Call wait() to read stdout, stderr, and the return code.
result = sb.exec("echo hi", timeout=5).wait()

print(result.stdout)
print(result.stderr)
print(result.returncode)
Use cwd to run from a working directory. Use background=True for long-lived processes such as web servers.
sb.exec("python3 -m http.server 3000", background=True, cwd="/srv/app").wait()
Multiple exec requests can be active on a sailbox at the same time. Coordinate shared files, ports, and processes in your own commands when they overlap.

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 supported only 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, start a service inside the VM, then fetch the listener’s endpoint. A bare port number is exposed over HTTP and resolves to an HttpEndpoint with a routable 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)
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 (below).

Raw TCP and SSH

Pass sail.IngressPort(port, "tcp") to expose a port as raw TCP instead of HTTP — for SSH, Postgres, or any other TCP protocol. Its listener resolves to a TcpEndpoint with a host and port that any client dials directly — a raw byte stream, so no TLS wrapping or client-side config is needed. Each org can hold a limited number of concurrent raw-TCP endpoints (32 by default); a create beyond that limit fails with a clear error. Tear down endpoints you no longer need, or contact us to raise your limit. The default image ships no SSH server, so a DIY SSH box installs one, exposes port 22 as TCP, injects your public key, and starts sshd:
import os, sail

pubkey = open(os.path.expanduser("~/.ssh/id_ed25519.pub")).read().strip()

sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64.apt_install("openssh-server"),
    name="ssh-demo",
    ingress_ports=[sail.IngressPort(22, "tcp")],
)

# Install your key and enable key-only root login. Debian ships root with a
# locked password, which blocks key auth through PAM until it is cleared;
# password login stays disabled, so the empty password is not a login path.
sb.exec(
    "install -d -m700 /root/.ssh && "
    f"echo '{pubkey}' > /root/.ssh/authorized_keys && "
    "chmod 600 /root/.ssh/authorized_keys && "
    "passwd -d root && "
    "sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config && "
    "sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && "
    "mkdir -p /run/sshd"
).wait()
sb.exec("/usr/sbin/sshd -D -e", background=True)

listener = sb.listener(22).wait(timeout=60)
host, port = listener.endpoint.host, listener.endpoint.port
print(f"ssh root@{host} -p {port}")
listener.wait() returns once sshd is accepting connections — for SSH it waits for the server banner — after which ssh root@<host> -p <port> connects with no extra flags.
A raw-TCP port is reachable from the public internet with no platform-side authentication — the in-guest daemon (such as sshd) is the only access control, so make sure it requires credentials.
To restrict which source IPs may connect, pass cidr_allowlist:
ingress_ports=[
    sail.IngressPort(22, "tcp", cidr_allowlist=["203.0.113.0/24", "198.51.100.7/32"]),
]
Connections from outside the listed CIDR prefixes are dropped before reaching the guest. An empty or omitted cidr_allowlist means any source may connect. cidr_allowlist is a TCP-only control; it is not accepted on HTTP ports. Exposing a well-known unauthenticated service port (such as Postgres, MySQL, or Redis) as raw TCP with neither a cidr_allowlist nor allow_public=True is rejected, since publishing one of these to the whole internet with no source restriction is rarely intended. Set a cidr_allowlist, or pass allow_public=True to confirm you want it publicly reachable:
ingress_ports=[
    sail.IngressPort(5432, "tcp", allow_public=True),
]

Lifecycle

Sailboxes preserve their writable disk, in-memory state, and in-flight network requests across checkpoints and resumes.
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
Call checkpoint() after important setup, such as installing packages or fetching remote data. On host failure, Sail restores from the most recent completed checkpoint and does not replay commands that ran after that checkpoint. 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.

Sleep during inference

To automatically sleep a sailbox 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 = client.responses.create(
    model="zai-org/GLM-5",
    input="Summarize the current workspace state.",
    background=False,
    extra_headers={"X-SailboxId": sb.sailbox_id},
)

Custom images

Start from the arm64 Debian base image and add build steps:
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, pip_install, run_commands, env, and build.