sail.Sailbox is a sandbox instance on the Sail platform. You create one with
Sailbox.create(), then run commands, transfer files, expose ports, and
checkpoint or pause it. For a guided walkthrough, see the
Sailboxes guide; this page is the API reference.
import sail
sb = sail.Sailbox.create(
app=sail.App.find(name="example-app", mint_if_missing=True),
image=sail.Image.debian_arm64,
name="sandbox-1",
)
print(sb.sailbox_id, sb.status)
Attributes
| Attribute | Type | Description |
|---|
sailbox_id | str | Stable control-plane identifier. The primary external handle for the sandbox. |
name | str | Human-readable name supplied at create time. |
status | str | Current lifecycle status (e.g. running, paused, sleeping). |
exec_endpoint | str | Endpoint the SDK dials for exec, file, listener, and network calls. Treat it as opaque. |
Treat sailbox_id as the durable handle. Sailbox.get and Sailbox.list
return a trimmed SailboxInfo instead of a Sailbox because
they intentionally omit exec_endpoint; use
Sailbox.connect to get a fully operable handle from an id.
Sailbox.create
@classmethod
def create(
*,
image: ImageDefinition,
app: App,
name: str,
image_build_timeout: int = 1800,
max_cpu: int | None = None,
max_memory_mib: int | None = None,
max_disk_gib: int | None = None,
ingress_ports: list[int | IngressPort] | None = None,
ssh: bool | str = False,
) -> Sailbox
Creates a new sailbox. Custom image definitions are built first, then a
synchronous request returns once the VM is running (or creation has failed).
The max_* parameters are resource-control limits, not billing reservations:
Sailbox billing uses observed CPU, memory, and disk usage.
| Parameter | Default | Description |
|---|
image | required | A sail.Image value or custom image definition. |
app | required | The owning sail.App, usually from App.find(). |
name | required | Human-readable sailbox name. |
image_build_timeout | 1800 | Seconds to wait for a custom image build before creating the VM. Must be > 0. |
max_cpu | 4 | Maximum vCPU count for resource control. Must be between 1 and 4. |
max_memory_mib | 8192 | Maximum memory in MiB for resource control. Must be between 1024 and 8192. |
max_disk_gib | 32 | Maximum writable state disk in GiB for resource control. Must be between 1 and 32. |
ingress_ports | None | Guest ports to expose. Each entry is a bare int (HTTP shorthand) or an IngressPort. |
ssh | False | True to get an SSH-ready box in one call; a key string or .pub path to override the key. See below. |
Returns a running Sailbox.
Raises sail.SailboxCreationError on creation failure, PermissionError
on 401/403, TypeError if image is not a sail.Image value, and
ValueError for out-of-range max_cpu/max_memory_mib/max_disk_gib or invalid
ingress_ports.
The older cpu, memory_mib, and disk_gib keyword arguments are accepted
as compatibility aliases for max_cpu, max_memory_mib, and max_disk_gib.
Use sail.Image.debian_arm64 or sail.Image.debian_arm for most workloads;
they pin the image to your local Python version so @sail.function can
deserialize local bytecode. See Images & Functions.
SSH in one call
Passing ssh=True adds an SSH server to the image, exposes guest port 22 as
raw TCP, installs your local public key (~/.ssh/id_ed25519.pub then
id_rsa.pub, or pass a key string / .pub path via ssh="..."), and starts
sshd. Read the port-22 listener’s endpoint for the connect command:
sb = sail.Sailbox.create(
app=app,
image=sail.Image.debian_arm64,
name="ssh-box",
ssh=True,
)
endpoint = sb.listener(22).wait().endpoint
print(f"ssh -p {endpoint.port} root@{endpoint.host}")
SSH must be set up at create time: guest port 22 ingress cannot be added to a
running sailbox, so start_sshd only works on a box
created with ssh=True.
Sailbox.connect
@classmethod
def connect(sailbox_id: str) -> Sailbox
Returns a fully operable handle for an existing sailbox by id. Unlike
Sailbox.get, this goes through the lifecycle resume route,
which validates access and returns the current exec_endpoint. If the sailbox
is paused or sleeping it is resumed first; if it is already running the backend
just verifies the current placement.
Raises LookupError if the sailbox cannot be resumed (e.g. terminated),
PermissionError on auth failures.
sb = sail.Sailbox.connect("sb_...")
Sailbox.get
@classmethod
def get(sailbox_id: str) -> SailboxInfo
Fetches a single sailbox owned by the current org as read-only monitoring
state. Returns a SailboxInfo (no exec_endpoint), so use
Sailbox.connect when you need to run commands. Unknown and wrong-org ids both
raise LookupError (the server returns 404 for both to avoid leaking ownership
across orgs).
Sailbox.list
@classmethod
def list(
*,
app: str | None = None,
status: str | None = None,
search: str | None = None,
max_guest_schema_version: int | None = None,
limit: int = 50,
offset: int = 0,
) -> list[SailboxInfo]
Lists sailboxes for the current org. Filters are applied server-side; app
matches against app_id (resolve a name through App.find
first). max_guest_schema_version keeps only sailboxes whose in-guest agent
version is at or below the given version (rows that predate version stamping
always match). Use it to find sailboxes that need
upgrade. Returns just the rows. Use
Sailbox.list_page for the pagination envelope.
Sailbox.list_page
@classmethod
def list_page(
*,
app: str | None = None,
status: str | None = None,
search: str | None = None,
max_guest_schema_version: int | None = None,
limit: int = 50,
offset: int = 0,
) -> SailboxPage
Same call as Sailbox.list, but returns a SailboxPage with
the rows plus the server’s limit/offset/total/has_more envelope.
exec
def exec(
command: str | SailFunction,
*function_args,
timeout: int | None = None,
background: bool = False,
cwd: str | None = None,
idempotency_key: str | None = None,
args: list | tuple | None = None,
kwargs: Mapping | None = None,
) -> SailboxExecRequest | Any
Runs a shell command or a decorated Python function in
the sailbox.
- For a shell command (
str), returns a SailboxExecRequest
immediately; call .wait() to collect output.
- For a
SailFunction, exec blocks and returns the function’s return
value directly.
| Parameter | Default | Description |
|---|
command | required | A shell command string, or a @sail.function-decorated callable. |
*function_args | — | Positional args for a SailFunction (alternatively pass args=). |
timeout | None | Command runtime budget in seconds. Omit for no SDK-imposed limit. Must be > 0 when set. |
background | False | Launch through a detached shell; wait() only waits for the launcher to succeed. Not supported for functions. |
cwd | None | Working directory to run the command from. |
idempotency_key | None | Defaults to a generated key, which makes retrying initial submission safe. |
args / kwargs | None | Function arguments, as an alternative to positional *function_args. |
result = sb.exec("echo hi", timeout=5).wait()
print(result.stdout, result.returncode)
Multiple execs can run on the same sailbox concurrently; coordinate access to
shared files and ports in your own commands. Exec is unavailable while the
sailbox is paused (call resume() first), which raises
sail.SailboxExecutionError.
read
def read(path: str) -> bytes
Reads a regular file from the sailbox as bytes. Loads the whole file into
memory; for very large files (checkpoints, datasets) prefer
read_stream. Raises FileNotFoundError if the path does not
exist, ValueError if path is empty.
data = sb.read("/workspace/output.txt")
print(data.decode())
read_stream
def read_stream(path: str) -> Iterator[bytes]
Yields a regular file’s contents as chunks (currently ~1 MiB each) without
buffering the whole file. The underlying channel stays open until the iterator
is exhausted or closed, so iterate to completion rather than holding it open.
with open("local.bin", "wb") as f:
for chunk in sb.read_stream("/workspace/large.bin"):
f.write(chunk)
write
def write(
path: str,
data: str | bytes | bytearray | memoryview | IOBase,
*,
create_parents: bool = True,
mode: int | None = None,
) -> None
Writes data to a regular file in the sailbox. Missing parent directories are
created by default. path must be absolute.
| Parameter | Default | Description |
|---|
path | required | Absolute destination path. |
data | required | Bytes, string, or a file-like object (streamed in chunks). |
create_parents | True | Create missing parent directories. |
mode | None | POSIX permission bits (0–0o777). Defaults to 0o644 when omitted. |
sb.write("/workspace/input.txt", "hello\n")
request
def request(
method: str,
url: str,
*,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
data: bytes | str | None = None,
json: Any | None = None,
timeout: int | None = None,
durability: str = "tracked",
idempotency_key: str | None = None,
) -> SailboxRequest
Issues a tracked HTTP request from inside the sailbox and returns a
SailboxRequest handle. data and json are mutually
exclusive; idempotency_key is required (must be non-empty). timeout
defaults to 30 seconds server-side when omitted.
Sailboxes have transparent outbound egress, so for most use cases you can make
ordinary HTTP calls from inside an exec. request() exists for cases that
need a durably tracked request record.
listener
def listener(port: int) -> SailboxListener
Returns the SailboxListener for a single exposed guest
port. Raises LookupError if the port was not exposed via ingress_ports at
create time (with a message telling you to expose it),
PermissionError/RuntimeError on other failures.
listener = sb.listener(3000)
listener.wait(timeout=60)
print(listener.endpoint.url)
listeners
def listeners() -> list[SailboxListener]
Returns one SailboxListener per exposed guest port.
for listener in sb.listeners():
print(listener.port, listener.protocol, listener.route_status, listener.endpoint)
start_sshd
def start_sshd(
public_key: str | None = None,
*,
wait: bool = True,
timeout: float = 60.0,
) -> TcpEndpoint | None
Installs your SSH public key and (re)starts sshd. create(ssh=True) runs
this for you, so you normally only reach for it to bring sshd back up if the
daemon stops inside a running box. Safe to re-run: it generates the box’s host
key once and never rotates it, so a caller’s known_hosts stays valid.
public_key defaults to ~/.ssh/id_ed25519.pub then ~/.ssh/id_rsa.pub; pass
a key string or a path to a .pub file to override. By default this blocks
until the port-22 listener is reachable and returns its
TcpEndpoint; pass wait=False to skip the probe and return
None.
Raises LookupError if guest port 22 is not exposed (the box was not
created with ssh=True), since the ingress port cannot be added to a running
sailbox.
checkpoint
Durably checkpoints this running sailbox without stopping it.
upgrade
Upgrades this sailbox’s in-guest agent to the latest version, picking up new
sailbox features, fixes, and performance improvements without recreating the
box. A running sailbox reboots in place on its current disk: all filesystem
state is preserved, but processes restart as they would after a machine reboot
(unflushed application state gets power-loss semantics). A paused or sleeping
sailbox is upgraded without waking. The upgrade is recorded and applies
automatically at the next wake.
Returns True when the upgrade was applied immediately (the sailbox was
running) and False when it will apply at the next wake. Already-up-to-date
sailboxes return True without rebooting.
fork
def fork(
*,
name: str | None = None,
timeout_seconds: int | None = None,
) -> Sailbox
Creates a new running sailbox from this sailbox’s current memory and disk
state and returns its Sailbox handle. Running parents are checkpointed
first; sleeping and paused parents fork from their last checkpoint without
waking. timeout_seconds, when set, must be > 0.
pause
Checkpoints and pauses this sailbox until it is explicitly resumed. After
pause(), status becomes paused and exec is rejected until resume().
sleep
Checkpoints and sleeps this sailbox until traffic or an explicit resume()
wakes it. After sleep(), status becomes sleeping.
resume
Resumes this sailbox, refreshing status and exec_endpoint on the handle.
Raises LookupError if the sailbox is terminated and cannot be resumed.
terminate
Permanently terminates this sailbox. Idempotent: terminating an
already-terminated sailbox succeeds.
Supporting types
IngressPort
A guest port to expose through the platform edge. Sailbox.create also accepts
a bare int as shorthand for IngressPort(port) (an HTTP port).
| Field | Default | Description |
|---|
guest_port | required | Guest port to expose (1–65535). |
protocol | "http" | "http" for a stable HTTPS URL, or "tcp" for a byte-transparent raw-TCP host/port. |
cidr_allowlist | None | Source-IP CIDR prefixes allowed to connect. "tcp" ports only. |
allow_public | False | For "tcp": confirm the port is intentionally reachable by the whole internet. |
Reserved ports: guest port 22 cannot be an HTTP port (expose it as tcp for
SSH) and 10000/10001/15001/15002 are reserved for in-guest platform
services. Exposing a well-known
unauthenticated service port (database/cache/search, e.g. 5432, 6379) as
raw TCP with neither cidr_allowlist nor allow_public=True is rejected.
sb = sail.Sailbox.create(
app=app,
image=sail.Image.debian_arm64,
name="db-box",
ingress_ports=[80, 443, sail.IngressPort(5432, "tcp", cidr_allowlist=["203.0.113.0/24"])],
)
SailboxExecRequest
A handle to a submitted shell-command exec.
| Field | Description |
|---|
sailbox_id | Owning sailbox id. |
exec_request_id | Durable id for this exec. |
exec_endpoint | Worker-proxy endpoint the exec runs through. |
background | Whether the exec was launched in the background. |
idempotency_key | The idempotency key used for submission. |
def wait(*, retry_timeout: float = 30.0) -> SailboxExecResult
Waits for the exec to complete and returns a SailboxExecResult.
For foreground execs this waits for the command itself; for background execs it
waits only for the detached launcher. Ctrl-C sends SIGINT and resumes
waiting; a second Ctrl-C escalates to SIGKILL. Raises
sail.SailboxWorkerLostError if the assigned worker was lost, or
sail.SailboxExecInterruptedError if the exec was interrupted before completion
and is retryable.
def cancel(*, force: bool = False, retry_timeout: float = 0.0) -> None
Signals the guest command: SIGINT by default, SIGKILL if force=True.
Idempotent on the server.
SailboxExecResult
The output of a completed exec.
| Field | Type | Description |
|---|
stdout | str | Captured standard output. |
stderr | str | Captured standard error. |
returncode | int | Process exit code. |
SailboxListener
An exposed guest port and how to reach it.
| Field | Type | Description |
|---|
sailbox_id | str | Owning sailbox id. |
port | int | The exposed guest port. |
protocol | str | "http" or "tcp". |
route_status | str | Route lifecycle status (e.g. LISTENER_ROUTE_STATUS_ACTIVE). |
endpoint | HttpEndpoint | TcpEndpoint | None | How to reach the port; None until routable or when ingress for the protocol is disabled. |
def status() -> SailboxListener
def wait(timeout: float = 60.0, poll_interval: float = 1.0) -> SailboxListener
status() re-fetches the listener. wait() blocks until the route is active
and the endpoint is reachable (an HTTP probe to the url, or a TCP
connectivity check to the host/port), then returns the ready listener.
Raises TimeoutError if it does not become reachable within timeout.
HttpEndpoint
The routable HTTPS address of an "http" listener.
| Field | Type | Description |
|---|
url | str | The HTTPS URL to reach the guest service. |
TcpEndpoint
The host and port to connect to for a "tcp" listener.
| Field | Type | Description |
|---|
host | str | Hostname to dial. |
port | int | Port to dial. |
SailboxRequest
A handle to a tracked HTTP request issued via request.
| Field | Type | Description |
|---|
sailbox_id | str | Owning sailbox id. |
request_id | str | Durable request id (also available as .id). |
exec_endpoint | str | Worker-proxy endpoint. |
status | str | Request status. |
response | SailboxResponse | None | The response once completed. |
error_message | str | Error detail if the request failed. |
Methods: refresh() re-fetches current state, wait(timeout=None) blocks
until completion, and cancel() cancels the request. Each returns the updated
SailboxRequest.
SailboxResponse
The response of a completed SailboxRequest.
| Member | Type | Description |
|---|
status_code | int | HTTP status code. |
headers | dict[str, str] | Response headers. |
content | bytes | Raw response body. |
text | str (property) | Body decoded as UTF-8. |
json() | Any | Body parsed as JSON. |
SailboxInfo
A read-only snapshot returned by Sailbox.get and
Sailbox.list. It deliberately omits exec_endpoint; use
Sailbox.connect for an operable handle.
| Field | Type | Description |
|---|
sailbox_id | str | Sailbox id. |
app_id / app_name | str | Owning app. |
image_id | str | Image the sailbox runs. |
name | str | Sailbox name. |
status | str | Lifecycle status. |
memory_mib / vcpu_count / state_disk_size_gib | int | Configured resource-control maxima. |
cpu_requested_vcpu | int | Configured vCPU maximum. |
cpu_used_vcpu | float | Latest observed vCPU usage. |
memory_requested_bytes / memory_used_bytes | int | Configured max vs observed memory. |
disk_requested_bytes / disk_used_bytes | int | Configured max vs observed disk. |
architecture | str | CPU architecture. |
guest_schema_version | int | None | In-guest agent version the sailbox last booted with. None means it predates version stamping (the oldest possible version). |
error_message | str | None | Failure detail, when applicable. |
checkpoint_generation | int | Monotonic checkpoint counter. |
started_at / last_checkpointed_at | str | None | Timestamps. |
created_at / updated_at | str | Timestamps. |
Observed-usage fields reflect the latest live sample within roughly the last
two minutes, falling back to zero when no recent sample is available.
SailboxPage
One page of Sailbox.list_page results.
| Field | Type | Description |
|---|
items | list[SailboxInfo] | The rows on this page. |
limit | int | Page size used. |
offset | int | Page offset used. |
total | int | Total matching sailboxes. |
has_more | bool | Whether more rows exist beyond this page. |