Sailbox is a Linux VM on the Sail platform: a full cloud environment
designed for long-horizon agents. Create one, 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.
Sync and async
- Python methods are synchronous, and every method that does I/O has an
async twin under
.aio(await sb.exec.aio(...)). Handles returned by async calls are already async:await sb.exec.aio(...)returns a process whose own methods (wait,cancel,resize) are awaited directly, with no.aio. The one exception to the rule isshell, which is sync-only since it drives your local terminal. - TypeScript is async-only: every operation returns a
Promise. - Rust is async-only, on a Tokio runtime. Synchronous code can drive any
call with
sail::block_on.
Naming
The three SDKs expose the same operations with each language’s conventions: Python issnake_case with keyword arguments, TypeScript is camelCase with
options objects and unit-suffixed durations (timeoutSeconds), and Rust pairs
a Sailbox object with explicit argument structs. Parameter tables on this
page use the Python spelling; each section’s TypeScript signature shows the
real names. Every TypeScript static also accepts an explicitly constructed
client in its options object (see
Configuration); the signatures on this page omit it.
Attributes
EverySailbox carries its identity and lifecycle state: sailbox_id,
name, and status (running, paused, sleeping, failed,
terminated). A Sailbox returned by get or
list also carries the monitoring snapshot from that fetch:
owning app, image, configured and observed resource usage, and timestamps (see
snapshot fields).
Treat sailbox_id as the durable handle: store the id, and turn it back into
a usable Sailbox any time with Sailbox.get (which fetches
fresh state), or without a network call via Sailbox.from_id /
Sailbox.fromId / client.sailbox(id). The get result reflects the
Sailbox at the time of the call; call it again for fresh state.
Sailbox.create
size is a resource ceiling, not a billing reservation; ongoing Sailbox
billing uses your actual CPU, memory, and disk consumption. Each size also has
a one-time creation charge. memory_gib and disk_gib optionally tune the
size’s ceilings, in whole GiB within the size’s range; raising a ceiling does
not increase the ongoing rate, and lowering one caps what the box can consume.
See Sailbox pricing.
Volumes are currently in Alpha. To pilot them, reach out in the Sail
Slack.
Sailbox.
Raises a creation error when Sail cannot create the box, a
permission error on 401/403, and an invalid-argument error for an unknown
size or invalid ingress ports and
volume mounts.
In Python, pin
image=sail.Image.debian_arm64 or debian_amd64 when using
@sail.function; those images match your local Python version so function
bytecode can deserialize in the guest. See Images &
Functions.SSH
For a quick shell, useshell() or sail box shell: it streams a
PTY over the same channel as exec and uses no ingress port. SSH is opt-in,
for when you want a standard SSH endpoint: a devbox, your own client, scp,
or port forwarding.
ssh=True makes the box SSH-ready in one call: it runs
enable_ssh on the new box, which trusts your org’s SSH
certificate authority, starts sshd, and exposes guest port 22 as raw TCP.
See the SSH access guide for the access
model, how to connect, and source restrictions.
By default a box is org-wide: any credential in your org can exec, copy files,
SSH into, or run lifecycle operations on it. private=True restricts all of
that to you: only your credential can operate the box (your org can still see
it in listings, but not act on it). It requires an API key minted by your user,
not a service key. Org admins can override the restriction for exec, file, and
pause/resume/terminate/upgrade operations by setting SAIL_OWNER_OVERRIDE_REASON (or
the X-Sail-Owner-Override-Reason header on raw HTTP calls); exposing or
removing listeners, and fork, checkpoint, and restore stay creator-only. Every
override requires that reason, and your
org’s audit log records it. SSH has no override:
a private box’s sshd accepts only its creator’s certificates.
From the CLI, pass --private to sail box create.
Sailbox.get
Sailbox: run commands,
read and write files, and manage listeners on it directly. get never wakes a
paused or sleeping box; operations resume it on demand. The returned object
reflects the Sailbox at the time of the call, so call get again for fresh
state. Unknown and wrong-org ids both raise a not-found error, so you cannot
tell an unknown id from one owned by another org.
In Rust, client.sailbox(id) binds the id without a network call; fetch
current state with sb.info().
Sailbox.list
limit of them) is collected; limit caps
the total returned, bounding the fetch for large orgs. (Rust’s
list_sailboxes returns one page with its envelope.) app_id filters by the owning
app id (resolve a name through App.find first).
search filters by name substring. order is "newest_active" (most recently
active first, the default) or "newest_created" (newest-created first). Use
Sailbox.list_page to control paging yourself or to
read the pagination envelope.
Sailbox.list_page
Sailbox.list, but returns a SailboxPage with
the Sailboxes plus the limit/offset/total/has_more pagination envelope.
(In Rust, list_sailboxes always returns the page.)
exec
wait() for the buffered result.
A string command runs via /bin/sh -lc, so shell syntax (pipes, redirects,
&&) works. Python and TypeScript also accept an argv list, which execs the
program directly with no shell interpretation, and Rust separates the two as
exec_shell (string) and exec (argv).
Python’s exec additionally accepts a @sail.function-decorated Python
callable; it then blocks and returns the function’s return value directly. See
Images & Functions.
run
exec followed by wait(). A string command runs
via /bin/sh -lc; a list is exec’d directly. cwd sets the working directory
for string commands only (like exec, an argv command with cwd raises).
By default a nonzero exit code returns normally on the result; check
exit_code. With check set, a nonzero exit or a timeout raises
CommandFailedError carrying the completed result instead. (Rust reports
everything through the returned ExecResult.) A command that exceeds
timeout is killed and reports timed_out on the result; without check,
a timeout alone does not raise.
idempotency_key makes a retried run wait on the original command instead
of launching it again, so a control-loop retry cannot double-execute.
In TypeScript, aborting signal force-cancels the remote command and rejects.
The result’s stdout and stderr are the buffered output, capped with the oldest
bytes dropped first. For unbounded output, or to read output as it happens,
stream it live with exec. The interactive and detached exec
options (open_stdin, pty, background) are not available on run.
shell
command, runs a login shell; pass command to run that
under a pty instead (e.g. a REPL or vim). Keystrokes (including Ctrl-C,
Ctrl-Z, and Ctrl-D) reach the remote process, its output renders locally, and
terminal resizes propagate. Blocks until the remote process exits and returns
its exit code. Requires an interactive local terminal (stdin and stdout must
be TTYs) on a Unix machine, so it suits CLIs and dev tools rather than
server-side harnesses.
This is the equivalent of ssh-ing into the box, without running an SSH
server. shell overrides the login shell (default $SHELL, else
/bin/bash); it is ignored when command is given.
While the session is open, browser opens and localhost servers in the box are
forwarded to your machine. When a program in the box opens a browser (a login
like claude login or gh auth login), the page opens in your local browser,
and a login that redirects to a localhost callback completes end to end. A
server the box starts on localhost keeps serving inside the box the whole
time (code and agents there reach it as usual); while the shell is open it is
also mirrored to the same port on your machine, unless that port is already in
use locally. The mirror lasts only for the session. Files dragged onto the terminal upload
into the box and paste as their guest paths, and Ctrl+V forwards your
clipboard. On devbox images the clipboard is two-way: a pasted image or text
lands on the box’s clipboard, and text copied inside the box comes back to
yours. Other images upload a pasted image as a file and paste its path. Pass
no_forward=True (TS noForward) to turn all of it off, for example for an
untrusted or automated session. Pass no_forward_browser=True (TS
noForwardBrowser) to keep everything forwarded except browser opens. Plain
exec forwards nothing; for the same forwarding on sail box exec --tty, see
the CLI reference.
The fs namespace
File and directory operations live under thefs namespace: sb.fs in Python
and TypeScript, sb.fs() in Rust. Reads and writes stream bytes to and from
the guest, and Python paths accept str or PurePosixPath. The directory
helpers mkdir (creates missing parents), remove (deletes recursively),
and exists behave like mkdir -p, rm -rf, and test -e.
fs.read
read_stream. Raises a file-not-found error if the path does
not exist.
fs.read_stream
for and async for.
fs.write
path must be absolute.
fs.write_stream
write and
confirm with finish. Only finish commits the write; a writer that goes
away without finishing (an explicit abort, an exception, or dropping it)
cancels the transfer instead, and the guest file state is then unspecified.
Use it when your data arrives incrementally (streaming logs, assembling an
archive on the fly) rather than from a source write can consume
whole.
fs.mkdir / fs.remove / fs.exists
mkdir creates a directory and any missing parents (like mkdir -p) and is a
no-op if it already exists. remove deletes a file or a whole directory tree
(like rm -rf) and is a no-op if the path is already absent. exists reports
whether a path exists; it follows symlinks (like test -e), so a dangling
symlink reports false even though fs.ls lists it. A failure (for
example a permission error) raises with the guest’s stderr in the message.
fs.ls
DirEntry records. A missing path raises, as does a path that is not
a directory and a listing too large for the exec output cap. An entry whose
name is not valid UTF-8 fails the listing, since the path API cannot address
it.
listener / listeners
ingress_ports, or at runtime with
expose/unexpose (see Networking).
enable_ssh
sshd, and exposes guest port 22 as raw TCP
once the CA-only server owns it. See the
SSH access guide for the access model and
how to connect. By default it blocks until SSH is reachable, up to timeout
seconds, and returns the endpoint to dial; pass wait=False to skip the probe
and return nothing.
allowlist restricts which source CIDRs may connect to port 22, replacing
any existing restriction. Left empty, a first enable opens the port to any
source, and a re-enable keeps the existing restriction. Disabling SSH removes
the port-22 listener along with its restriction, so enabling again starts
fresh.
checkpoint
name sets a display name for the handle. ttl_seconds (ttl in Rust),
when set, must be > 0 and overrides the server’s default retention window. Set it when you
keep a checkpoint to reuse as a template, so the handle does not expire while
you still need it.
The returned handle carries expires_at, a timestamp (datetime in Python,
Date in TypeScript, OffsetDateTime in Rust) for when it becomes
eligible for garbage collection (or None for a handle to an existing
checkpoint that carries no fresh retention bound).
from_checkpoint
checkpoint. The new Sailbox gets a fresh network identity;
existing TCP connections do not carry over, and ingress ports are not
inherited. timeout, when set, must be > 0.
A command still running when the checkpoint was taken does not resume in the
new Sailbox. Its filesystem changes up to the checkpoint are kept, but the
command itself does not continue. Start any commands you need again on the new
Sailbox.
fork
checkpoint and start children from it with
from_checkpoint. A common use is fan-out: prepare one Sailbox (install
dependencies, warm caches, load a repository), then fork it once per task.
from_checkpoint, the child gets a fresh network
identity: TCP connections are reset, and ingress ports are not inherited. A
command still running in the parent does not continue in the child. The
parent keeps running unchanged.
Use checkpoint plus from_checkpoint
instead when you want a durable snapshot to create Sailboxes from later;
fork requires the parent to exist at the moment of the call.
upgrade
UpgradeResult: applied is true when the upgrade happened
immediately (the Sailbox was running) and false when it will apply at the next
wake, and status is the Sailbox’s lifecycle status after the call.
Already-up-to-date Sailboxes report immediate success without rebooting.
pause / sleep / resume / terminate
pausecheckpoints and pauses the Sailbox in memory until it is explicitly resumed or an operation wakes it.sleepcheckpoints the Sailbox to disk; inbound traffic, an operation, or an explicitresumewakes it. An optional wake time schedules a wall-clock wake (see Lifecycle).resumewakes a paused or sleeping Sailbox. Raises a not-found error if the Sailbox is terminated.terminatepermanently ends the Sailbox. Idempotent: terminating an already-terminated Sailbox succeeds.
Volumes
AVolume is an org-scoped shared filesystem (NFS) that can be mounted into
one or more Sailboxes. Resolve one by name, then pass it (or its id) in the
volumes mapping of Sailbox.create, keyed by the
absolute guest path to mount it at:
findlooks up a volume by name;mint_if_missingcreates it when no volume with that name exists.listreturns the org’s active volumes, newest first;max_objectscaps the count.deletedeletes the volume. Withallow_missing, deleting an already-deleted volume succeeds instead of raising a not-found error.
volume_id, name, backend, status, and
created_at / updated_at timestamps.
Supporting types
Field names below use the Python spelling; TypeScript exposes the same fields in camelCase.DirEntry
One entry in a directory listing fromfs.ls. Reported for the entry
itself, so a symlink’s type is "symlink" regardless of what it points at.
IngressPort
A guest port to expose for ingress. In Python,Sailbox.create
also accepts a bare int as shorthand for IngressPort(port) (an HTTP port).
CIDR entries work for HTTP and TCP listeners. App-name entries match
authenticated traffic from another Sailbox and work on HTTP listeners only,
because raw-TCP connections carry no source app identity (so a
"tcp"
allowlist must contain only CIDR prefixes).
To send that authenticated traffic, pass headers=sail.ingress_auth_headers()
on the request when calling from inside a Sailbox. From the host, fetch a
specific Sailbox’s headers with sb.ingress_auth_headers() (this needs an
organization-scoped API key).
Reserved ports: guest port 22 cannot be an HTTP port (expose it as tcp
for SSH) and 10000/10001/15001/15002 are reserved for Sail’s
in-guest services. Leaving allowlist empty normally makes the port publicly
reachable, with one exception: for well-known database, cache, and search
ports (e.g. 5432, 6379), a raw-TCP expose with no allowlist is rejected,
so you can’t accidentally publish an unprotected Postgres or Redis to the
whole internet. To make one of these ports public on purpose, say so
explicitly with allowlist=["0.0.0.0/0", "::/0"].
Exec process
A handle to a command running in the Sailbox (ExecProcess in Python,
ExecProcess in TypeScript and Rust). The command runs inside the Sailbox,
independent of this handle and the connection that launched it. Closing the
handle or losing the network leaves the command running, and its result stays
retrievable through wait().
Live output. stdout and stderr are iterators (for in Python,
for await in TypeScript, next().await in Rust). The Python and TypeScript
iterators yield text, incrementally decoded from the raw stream (a multibyte
character split across chunks arrives whole); the raw byte stream is available
as stdout_bytes / stderr_bytes in Python, .raw() on the stream in
TypeScript, and is what the Rust reader yields directly. Bytes travel exactly
as the command wrote them (escape sequences and binary payloads included). A
reader that falls more than ~1 MiB behind skips the dropped head, keeping the
tail. If the output stream breaks mid-run, live iteration ends early; wait()
still returns the full result by reattaching to the command, but the live tail
does not resume.
stdin. With open_stdin=True, write to the command’s stdin and deliver EOF
by closing it (Python proc.stdin.write/close, TypeScript
writeStdin/closeStdin, Rust write_stdin/close_stdin). Writes block
(like a pipe write) while the command is not reading. Writing to a completed
command, or after the command closed its stdin, raises a broken-pipe error.
wait() resolves the exec and returns its result
with the full (capped) output, independent of how much was consumed via live
iteration. For foreground execs it waits for the command to finish; for
background execs it waits only for the detached launcher. An exec that ended
without a real exit code (the machine hosting the Sailbox was lost before the
command finished) raises a host-lost error instead of returning a result.
In Python and the sail CLI, Ctrl-C during a wait additionally sends
SIGINT to the remote command and resumes waiting, and a second Ctrl-C
escalates to SIGKILL; terminal-facing surfaces forward the interrupt like
a local foreground job. The TypeScript and Rust libraries leave process
signal handling to your application; wire the same behavior with
cancel if you want it.
poll() (Rust: try_wait) returns the exit code if it already arrived
on the output stream,
else nothing. A broken stream never sees the exit, so wait() is the
authoritative answer.
cancel() signals the guest command: SIGINT by default, SIGKILL with
force. Idempotent on the server. If the Sailbox is sleeping, cancel wakes it
to deliver the signal; if you paused the Sailbox, resume it first (cancel
raises rather than waiting, since the guest cannot receive the signal while
paused).
close() releases the output stream without touching the remote run. The
command keeps going, and a later wait() reattaches to it.
resize(cols, rows) adjusts a pty command’s window.
resync() asks a pty command to repaint its current screen on the output
stream. A command runs at full speed and never waits for a slow reader, so if
you render its output yourself and fall far behind, the oldest output is dropped
and the screen can end up garbled. Call resync() to receive the current screen
instead of a broken, partial one. It does nothing for a command with no pty.
The interactive shell helper calls it for you.
Exec result
The output of a completed exec (ExecResult in Python, ExecResult in
TypeScript and Rust).
Listener
An exposed guest port and how to reach it. Every listener carries its guest port,protocol, route status, and a typed
endpoint: an HttpEndpoint (with url) or a
TcpEndpoint (with host/port), absent until routable. In
TypeScript the endpoint is a union discriminated on kind; in Rust it is the
ListenerEndpoint enum returned by listener.endpoint(). Listeners are
snapshots; re-fetch with sb.listener(guest_port).
HttpEndpoint
The routable HTTPS address of an"http" listener.
TcpEndpoint
The host and port to connect to for a"tcp" listener.
Sailbox snapshot fields
The monitoring snapshot carried by everySailbox returned from
Sailbox.get and Sailbox.list.
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 ofSailbox.list_page results.