A 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.
import sail
sb = sail.Sailbox.create(
app=sail.App.find(name="example-app", mint_if_missing=True),
name="sandbox-1",
)
print(sb.sailbox_id, sb.status)
import { App, Sailbox } from "@sailresearch/sdk";
const app = await App.find("example-app", { mintIfMissing: true });
const sb = await Sailbox.create({ app, name: "sandbox-1" });
console.log(sb.sailboxId, sb.status);
use sail::sailbox::types::CreateSailboxRequest;
use sail::Client;
let client = Client::from_env()?;
let app = client.find_app("example-app", /* mint_if_missing */ true).await?;
let sb = client
.create_sailbox(
&CreateSailboxRequest {
app_id: app.id,
name: "sandbox-1".into(),
..Default::default()
},
/* timeout */ None,
)
.await?;
println!("{}", sb.sailbox_id());
Sync and async
- Python methods are synchronous, and every method that does I/O has an
async twin under
.aio (await sb.exec.aio(...)), with two exceptions:
await sb.exec.aio(...) returns an async process whose own methods
(wait, cancel, resize) are awaited directly, and
shell 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.
import asyncio
import sail
async def main():
app = await sail.App.find.aio(name="example-app", mint_if_missing=True)
sb = await sail.Sailbox.create.aio(app=app, name="sandbox-1")
proc = await sb.exec.aio("echo hi")
result = await proc.wait()
print(result.stdout)
await sb.terminate.aio()
asyncio.run(main())
const sb = await Sailbox.create({ app, name: "sandbox-1" });
const proc = await sb.exec("echo hi");
const result = await proc.wait();
console.log(result.stdout);
await sb.terminate();
use sail::exec::ExecOptions;
let sb = client.sailbox("sb_...");
let proc = sb.exec_shell("echo hi", ExecOptions::default()).await?;
let result = proc.wait().await?;
print!("{}", result.stdout);
sb.terminate().await?;
Naming
The three SDKs expose the same operations with each language’s conventions:
Python is snake_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.
Attributes
Every Sailbox carries its identity and lifecycle state: sailbox_id,
name, and status (running, paused, sleeping). 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. The returned
object reflects the sailbox at the time of the call; call get again for
fresh state.
Sailbox.create
@classmethod
def create(
*,
app: App | str,
image: ImageDefinition | None = None,
name: str,
image_build_timeout: int = 1800,
timeout: int = 600,
size: SailboxSize | None = None,
memory_gib: int | None = None,
disk_gib: int | None = None,
ingress_ports: list[int | IngressPort] | None = None,
volumes: Mapping[str, Volume | str] | None = None,
ssh: bool = False,
) -> Sailbox
static create(options: {
app: App | string;
name: string;
image?: ImageSpec | Image; // defaults to a plain Debian base
imageBuildTimeoutSeconds?: number; // 1800
timeoutSeconds?: number; // 600 per create attempt; 0 = unbounded
size?: "s" | "m";
memoryGib?: number;
diskGib?: number;
ingressPorts?: (number | IngressPortInput)[];
volumes?: Record<string, Volume | string>;
ssh?: boolean;
}): Promise<Sailbox>
pub async fn create_sailbox(
&self, // Client
req: &CreateSailboxRequest,
timeout: Option<Duration>,
) -> Result<Sailbox, SailError>
Creates a new sailbox. The SDK builds any custom image first, then blocks
until the VM is running or creation fails.
size is a resource ceiling, not a billing reservation; Sailbox billing uses
your actual CPU, memory, and disk consumption. memory_gib and disk_gib
optionally tune the size’s ceilings, in whole GiB within the size’s range;
raising a ceiling costs nothing on its own, and lowering one caps what the box
can consume. See Sailbox pricing.
| Parameter | Default | Description |
|---|
image | None | A sail.Image value or custom image definition. Defaults to a prebuilt Debian base (instant create). For @sail.function, pin your interpreter with image=sail.Image.debian_arm64. |
app | required | The owning app: an App from App.find(), or its id. |
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. |
timeout | 600 | Seconds to bound each create attempt while the sailbox waits for capacity and boots. Pass 0 to wait without a client-side bound. |
size | None | Resource size: "s" or "m" (the platform default). Each size sets the vCPU count plus default memory and disk; "s" gives the fastest cold starts, forks, and resumes, and caps what a runaway workload can consume. |
memory_gib | None | Memory ceiling in whole GiB, within the size’s range: 2-64 for "s", 8-128 for "m". The size’s default when omitted. |
disk_gib | None | Disk size in whole GiB, within the size’s range: 8-128 for "s", 32-512 for "m". The size’s default when omitted. |
ingress_ports | None | Guest ports to expose. Each entry is a bare int (HTTP shorthand) or an IngressPort. |
volumes | None | Shared volumes to mount, mapping an absolute guest path to a Volume (or its id). |
ssh | False | True to make the box SSH-ready in one call (trusts your org’s SSH CA and starts sshd). See below. |
Returns a running 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, use shell() 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.
Sailbox.get
@classmethod
def get(sailbox_id: str) -> Sailbox
static get(sailboxId: string): Promise<Sailbox>
pub fn sailbox(&self, sailbox_id: impl Into<String>) -> Sailbox // Client
Fetches a sailbox by id and returns a fully usable 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().
sb = sail.Sailbox.get("sb_...")
result = sb.exec("echo hello").wait()
const sb = await Sailbox.get("sb_...");
const result = await (await sb.exec("echo hello")).wait();
use sail::exec::ExecOptions;
let sb = client.sailbox("sb_...");
let result = sb
.exec_shell("echo hello", ExecOptions::default())
.await?
.wait()
.await?;
Sailbox.list
@classmethod
def list(
*,
app_id: 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[Sailbox]
static list(params?: {
appId?: string;
status?: SailboxStatusFilter;
search?: string;
maxGuestSchemaVersion?: number;
limit?: number;
offset?: number;
}): Promise<Sailbox[]>
pub async fn list_sailboxes(
&self, // Client
query: &ListSailboxesQuery,
) -> Result<SailboxPage, SailError>
Lists sailboxes for the current org. app_id filters by the owning app id (resolve
a name through App.find first). search filters by name
substring. Returns just the sailboxes; use
Sailbox.list_page for the pagination envelope.
Sailbox.list_page
@classmethod
def list_page(*, ...same filters..., limit: int = 50, offset: int = 0) -> SailboxPage
static listPage(query?: ListSailboxesQuery): Promise<SailboxPage>
pub async fn list_sailboxes(&self, query: &ListSailboxesQuery) -> Result<SailboxPage, SailError>
Same call as 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
def exec(
command: str | Sequence[str] | SailFunction,
*function_args,
timeout: int | None = None,
background: bool = False,
cwd: str | None = None,
open_stdin: bool = False,
pty: bool = False,
term: str | None = None,
cols: int = 0,
rows: int = 0,
env: Mapping[str, str] | None = None,
idempotency_key: str | None = None,
retry_timeout: float = 30.0,
args: list | tuple | None = None,
kwargs: Mapping | None = None,
) -> ExecProcess | Any
exec(command: string | string[], options?: {
timeoutSeconds?: number;
background?: boolean;
cwd?: string;
openStdin?: boolean;
pty?: boolean;
term?: string;
cols?: number;
rows?: number;
env?: Record<string, string>;
idempotencyKey?: string;
retryTimeoutSeconds?: number;
}): Promise<ExecProcess>
pub async fn exec_shell(&self, command: &str, options: ExecOptions)
-> Result<ExecProcess, SailError>;
pub async fn exec(&self, argv: Vec<String>, options: ExecOptions)
-> Result<ExecProcess, SailError>;
Runs a command in the sailbox and returns a process handle: iterate its
stdout/stderr for live output and call 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.
| Parameter | Default | Description |
|---|
command | required | The command to run. |
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 that returns immediately (string commands only). The command’s output is discarded, so live output stays empty and wait() only confirms the launcher started it. |
cwd | None | Working directory to run the command from (string commands only). |
open_stdin | False | Open the command’s stdin for writing. When False, stdin reads as instant EOF. |
pty | False | Run the command under a pseudo-terminal: isatty() is true, control bytes written to stdin become signals, and resize(cols, rows) adjusts the window. stdout and stderr merge onto one output stream. Implies open_stdin. |
term | None | $TERM for a pty command. Defaults to xterm-256color. |
cols / rows | 0 | Initial pty window. Default 80x24. |
env | None | Extra environment variables for the command. Entries override the guest defaults (including LANG) and the image environment. A few reserved variables that identify the sailbox (such as SAILBOX_ID) cannot be overridden. For pty commands the local COLORTERM, LANG, LC_*, and TERM_PROGRAM are forwarded automatically for keys not set here. |
idempotency_key | None | Defaults to a generated key, so retrying the initial submission won’t double-launch the command. |
retry_timeout | 30 | Seconds to retry transient submission failures, for example while a sleeping sailbox wakes. |
result = sb.exec("echo hi", timeout=5).wait()
print(result.stdout, result.exit_code)
# Live output (chunks are arrival-sized, not line-split):
proc = sb.exec("for i in 1 2 3; do echo $i; sleep 1; done")
for chunk in proc.stdout:
print(chunk, end="")
proc.wait()
# Piping stdin:
proc = sb.exec("wc -l", open_stdin=True)
proc.stdin.write("one\ntwo\n")
proc.stdin.close()
print(proc.wait().stdout)
const result = await (await sb.exec("echo hi", { timeoutSeconds: 5 })).wait();
console.log(result.stdout, result.exitCode);
// Live output (chunks are arrival-sized, not line-split):
const proc = await sb.exec("for i in 1 2 3; do echo $i; sleep 1; done");
for await (const chunk of proc.stdout) {
process.stdout.write(chunk);
}
await proc.wait();
// Piping stdin:
const wc = await sb.exec(["wc", "-l"], { openStdin: true });
await wc.writeStdin("one\ntwo\n");
await wc.closeStdin();
console.log((await wc.wait()).stdout);
use sail::exec::ExecOptions;
use std::time::Duration;
let result = sb
.exec_shell(
"echo hi",
ExecOptions {
timeout: Some(Duration::from_secs(5)),
..Default::default()
},
)
.await?
.wait()
.await?;
print!("{} {}", result.stdout, result.exit_code);
// Piping stdin:
let wc = sb
.exec(
vec!["wc".into(), "-l".into()],
ExecOptions {
open_stdin: true,
..Default::default()
},
)
.await?;
wc.write_stdin(b"one\ntwo\n").await?;
wc.close_stdin().await?;
print!("{}", wc.wait().await?.stdout);
Multiple execs can run on the same sailbox concurrently; coordinate access to
shared files and ports in your own commands. Running a command on a paused or
sleeping sailbox wakes it.
run
def run(
command: str | Sequence[str],
*,
timeout: int | None = None,
cwd: str | None = None,
env: Mapping[str, str] | None = None,
) -> ExecResult
run(command: string | string[], options?: {
timeoutSeconds?: number;
cwd?: string;
env?: Record<string, string>;
}): Promise<ExecResult>
// One-shot runs compose the two primitives:
let result = sb.exec_shell(command, ExecOptions::default()).await?.wait().await?;
Runs a command to completion and returns its buffered result: a one-shot
convenience over 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). A
nonzero exit code returns normally on the result rather than raising; check
exit_code.
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.
result = sb.run("echo hello")
print(result.exit_code, result.stdout)
const result = await sb.run("echo hello");
console.log(result.exitCode, result.stdout);
use sail::exec::ExecOptions;
let proc = sb.exec_shell("echo hello", ExecOptions::default()).await?;
let result = proc.wait().await?;
println!("{} {}", result.exit_code, result.stdout);
shell
def shell(
command: str | None = None,
*,
shell: str | None = None,
term: str | None = None,
cwd: str | None = None,
timeout: int | None = None,
) -> int
shell(command?: string, options?: {
shell?: string;
term?: string;
cwd?: string;
timeoutSeconds?: number;
}): Promise<number>
pub async fn shell(
&self,
command: Option<&str>,
options: ShellOptions,
) -> Result<i32, SailError>
Opens an interactive pty session on the sailbox and bridges it to your local
terminal. With no 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), 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.
use sail::shell::ShellOptions;
sb.shell(/* command */ None, ShellOptions::default()).await?;
From the CLI:
sail box shell <sailbox-id>
The fs namespace
File and directory operations live under the fs namespace: sb.fs in Python
and TypeScript, sb.fs() in Rust. Reads and writes stream bytes over the file
service; the directory helpers (mkdir, remove, exists, ls) run
standard coreutils in the guest.
fs.read
def read(path: str) -> bytes
read(path: string): Promise<Buffer>
pub async fn read(&self, path: &str) -> Result<Vec<u8>, SailError>
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 a file-not-found error if the path does
not exist.
data = sb.fs.read("/workspace/output.txt")
print(data.decode())
const data = await sb.fs.read("/workspace/output.txt");
console.log(data.toString());
let data = sb.fs().read("/workspace/output.txt").await?;
println!("{}", String::from_utf8_lossy(&data));
fs.read_stream
def read_stream(path: str) -> FileStream # iterable, sync and async
readStream(path: string): Promise<FileStream> // async-iterable
pub async fn read_stream(&self, path: &str) -> Result<FileReader, SailError>
Yields a regular file’s contents in chunks without buffering the whole file in
memory. Iterate to completion (or close the stream) so it is released. The
Python iterator supports both for and async for.
with open("local.bin", "wb") as f:
for chunk in sb.fs.read_stream("/workspace/large.bin"):
f.write(chunk)
import { createWriteStream } from "node:fs";
const out = createWriteStream("local.bin");
for await (const chunk of await sb.fs.readStream("/workspace/large.bin")) {
out.write(chunk);
}
out.end();
use std::io::Write;
let mut out = std::fs::File::create("local.bin")?;
let reader = sb.fs().read_stream("/workspace/large.bin").await?;
while let Some(chunk) = reader.next().await {
out.write_all(&chunk?)?;
}
fs.write
def write(
path: str,
data: str | bytes | bytearray | memoryview | IOBase,
*,
create_parents: bool = True,
mode: int | None = None,
) -> None
write(path: string, data: Buffer | Uint8Array | string, options?: {
createParents?: boolean;
mode?: number;
}): Promise<void>
pub async fn write(
&self,
path: &str,
data: &[u8],
options: WriteOptions,
) -> Result<(), SailError>
Writes data to a regular file in the sailbox. Missing parent directories are
created by default in every language. path must be absolute.
| Parameter | Default | Description |
|---|
path | required | Absolute destination path. |
data | required | Bytes or a string. Python also streams file-like objects in chunks. |
create_parents | True | Create missing parent directories. |
mode | None | POSIX permission bits (0–0o777). Defaults to 0o644 when omitted. |
sb.fs.write("/workspace/input.txt", "hello\n")
await sb.fs.write("/workspace/input.txt", "hello\n");
use sail::WriteOptions;
sb.fs()
.write("/workspace/input.txt", b"hello\n", WriteOptions::default())
.await?;
fs.write_stream
def write_stream(
path: str,
*,
create_parents: bool = True,
mode: int | None = None,
) -> FileWriter
writeStream(path: string, options?: {
createParents?: boolean;
mode?: number;
}): Promise<FileWriter>
pub async fn write_stream(
&self,
path: &str,
options: WriteOptions,
) -> Result<FileWriter, SailError>
Opens a streaming write and returns a writer: push chunks with 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.
with sb.fs.write_stream("/logs/run.log") as writer:
for line in ["step 1 ok\n", "step 2 ok\n"]:
writer.write(line)
# A clean exit finishes (commits); an exception aborts and propagates.
const writer = await sb.fs.writeStream("/logs/run.log");
try {
for (const line of ["step 1 ok\n", "step 2 ok\n"]) {
await writer.write(line);
}
await writer.finish();
} catch (err) {
await writer.abort();
throw err;
}
use sail::WriteOptions;
let mut writer = sb
.fs()
.write_stream("/logs/run.log", WriteOptions::default())
.await?;
for line in ["step 1 ok\n", "step 2 ok\n"] {
writer.write(line.as_bytes()).await?;
}
writer.finish().await?;
// Dropping an unfinished writer aborts the transfer.
fs.mkdir / fs.remove / fs.exists
def mkdir(path: str) -> None
def remove(path: str) -> None
def exists(path: str) -> bool
mkdir(path: string): Promise<void>
remove(path: string): Promise<void>
exists(path: string): Promise<boolean>
pub async fn mkdir(&self, path: &str) -> Result<(), SailError>
pub async fn remove(&self, path: &str) -> Result<(), SailError>
pub async fn exists(&self, path: &str) -> Result<bool, SailError>
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.
sb.fs.mkdir("/workspace/results")
if not sb.fs.exists("/workspace/results/run.lock"):
sb.fs.remove("/workspace/results/stale")
await sb.fs.mkdir("/workspace/results");
if (!(await sb.fs.exists("/workspace/results/run.lock"))) {
await sb.fs.remove("/workspace/results/stale");
}
sb.fs().mkdir("/workspace/results").await?;
if !sb.fs().exists("/workspace/results/run.lock").await? {
sb.fs().remove("/workspace/results/stale").await?;
}
fs.ls
def ls(path: str) -> list[DirEntry]
ls(path: string): Promise<DirEntry[]>
pub async fn ls(&self, path: &str) -> Result<Vec<DirEntry>, SailError>
Lists a directory’s immediate entries (no recursion) as
DirEntry records. Runs GNU find in the guest, which the
default Debian image ships. 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.
for entry in sb.fs.ls("/workspace"):
print(f"{entry.type:9} {entry.size:>8} {entry.name}")
for (const entry of await sb.fs.ls("/workspace")) {
console.log(entry.type, entry.size, entry.name);
}
for entry in sb.fs().ls("/workspace").await? {
println!("{:?} {} {}", entry.entry_type, entry.size, entry.name);
}
listener / listeners
def listener(guest_port: int) -> Listener
def listeners() -> list[Listener]
def wait_for_listener(
guest_port: int,
*,
timeout: float = 60.0,
poll_interval: float = 1.0,
) -> Listener
listener(guestPort: number): Promise<Listener>
listeners(): Promise<Listener[]>
waitForListener(guestPort: number, options?: {
timeoutSeconds?: number; // 60
pollIntervalSeconds?: number; // 1
}): Promise<Listener>
pub async fn listener(&self, guest_port: u32) -> Result<Listener, SailError>;
pub async fn listeners(&self) -> Result<Vec<Listener>, SailError>;
pub async fn wait_for_listener(
&self,
guest_port: u32,
timeout: Duration,
poll_interval: Duration,
) -> Result<Listener, SailError>;
Look up the exposed guest ports and how to reach them. Waiting 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; it raises a timeout error otherwise.
listener = sb.wait_for_listener(3000, timeout=60)
print(listener.endpoint.url)
const listener = await sb.waitForListener(3000, { timeoutSeconds: 60 });
if (listener.endpoint?.kind === "http") {
console.log(listener.endpoint.url);
}
use std::time::Duration;
use sail::sailbox::types::ListenerEndpoint;
let listener = sb
.wait_for_listener(3000, Duration::from_secs(60), Duration::from_secs(1))
.await?;
if let Some(ListenerEndpoint::Http { url }) = listener.endpoint() {
println!("{url}");
}
Ports are exposed at create time via ingress_ports, or at runtime with
expose/unexpose (see Networking).
enable_ssh
def enable_ssh(
*,
allowlist: list[str] | None = None,
wait: bool = True,
timeout: float = 60.0,
) -> TcpEndpoint | None
enableSsh(options?: {
allowlist?: string[];
wait?: boolean; // true
timeoutSeconds?: number; // 60
}): Promise<SshEndpoint | null>
pub async fn enable_ssh(
&self,
options: EnableSshOptions, // { allowlist, wait, timeout }
) -> Result<Option<SshEndpoint>, SailError>
Prepares this box for SSH (idempotent): installs your org’s SSH certificate
authority as trusted, starts 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
def checkpoint(
*,
name: str | None = None,
ttl_seconds: int | None = None,
) -> SailboxCheckpoint
checkpoint(options?: {
name?: string;
ttlSeconds?: number;
}): Promise<SailboxCheckpoint>
pub async fn checkpoint(
&self,
options: CheckpointOptions, // { name, ttl }
) -> Result<SailboxCheckpoint, SailError>
Creates a durable checkpoint handle for this sailbox. Running sailboxes are
snapshotted first. Paused and sleeping sailboxes return a handle to their
existing checkpoint without waking.
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
@classmethod
def from_checkpoint(
checkpoint_id: str,
*,
name: str | None = None,
timeout: int | None = None,
) -> Sailbox
static fromCheckpoint(options: {
checkpointId: string;
name?: string;
timeoutSeconds?: number;
}): Promise<Sailbox>
pub async fn create_from_checkpoint(
&self, // Client
checkpoint_id: &str,
name: Option<&str>,
timeout: Option<Duration>,
) -> Result<Sailbox, SailError>
Creates a new running sailbox from a durable checkpoint handle returned by
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.
upgrade
def upgrade() -> UpgradeResult
upgrade(): Promise<UpgradeResult>
pub async fn upgrade(&self) -> Result<UpgradeResult, SailError>
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
(any application state not yet written to disk is lost, as after a sudden
power loss). A paused or sleeping sailbox is upgraded without waking; the
upgrade is recorded and applied at the next wake.
Returns an 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
def pause() -> None
def sleep() -> None
def resume() -> Sailbox
def terminate() -> None
pause(): Promise<void>
sleep(): Promise<void>
resume(): Promise<void>
terminate(): Promise<void>
pub async fn pause(&self) -> Result<(), SailError>;
pub async fn sleep(&self) -> Result<(), SailError>;
pub async fn resume(&self) -> Result<(), SailError>;
pub async fn terminate(&self) -> Result<(), SailError>;
pause checkpoints and pauses the sailbox in memory until it is
explicitly resumed or an operation wakes it.
sleep checkpoints the sailbox to disk; inbound traffic, an operation,
or an explicit resume wakes it.
resume wakes a paused or sleeping sailbox. Raises a not-found error if
the sailbox is terminated.
terminate permanently ends the sailbox. Idempotent: terminating an
already-terminated sailbox succeeds.
Sail may also sleep a Sailbox on its own, but only when nothing would notice:
no CPU or network activity, no process waiting on a timer, and no open
connections a sleep would break. A slept Sailbox wakes transparently on
traffic or the next operation.
See Lifecycle for how these interact with
checkpoints, and Pricing for billing.
Supporting types
Field names below use the Python spelling; TypeScript exposes the same fields
in camelCase.
DirEntry
One entry in a directory listing from fs.ls. Reported for the entry
itself, so a symlink’s type is "symlink" regardless of what it points at.
| Field | Description |
|---|
name | The entry’s base name, with no directory prefix. |
type | "file", "directory", "symlink", or "other" (device, FIFO, socket, …). |
size | Size in bytes as reported by the guest. |
modified_time | Last-modified time as a Unix timestamp in seconds, with a fractional part. |
mode | Unix permission bits, e.g. 0o644. The file-type bits are not included. |
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).
| 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. |
allowlist | None | CIDR prefixes or Sail app names allowed to connect. Empty means public. |
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"].
sb = sail.Sailbox.create(
app=app,
name="db-box",
ingress_ports=[80, 443, sail.IngressPort(5432, "tcp", allowlist=["203.0.113.0/24"])],
)
const sb = await Sailbox.create({
app,
name: "db-box",
ingressPorts: [
{ guestPort: 80, protocol: "http" },
{ guestPort: 443, protocol: "http" },
{ guestPort: 5432, protocol: "tcp", allowlist: ["203.0.113.0/24"] },
],
});
use sail::sailbox::types::{IngressPort, IngressProtocol};
let sb = client
.create_sailbox(
&CreateSailboxRequest {
app_id: app.id,
name: "db-box".into(),
ingress_ports: vec![
IngressPort {
guest_port: 80,
protocol: IngressProtocol::Http,
allowlist: Vec::new(),
},
IngressPort {
guest_port: 5432,
protocol: IngressProtocol::Tcp,
allowlist: vec!["203.0.113.0/24".to_string()],
},
],
..Default::default()
},
/* timeout */ None,
)
.await?;
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 worker-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() 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).
| Field | Type | Description |
|---|
stdout | str | Captured standard output. |
stderr | str | Captured standard error. |
exit_code | int | Process exit code. |
timed_out | bool | The command hit its timeout budget and was killed. |
stdout_truncated | bool | stdout exceeded the buffer cap; only the tail was kept. |
stderr_truncated | bool | stderr exceeded the buffer cap; only the tail was kept. |
stdout_complete | bool | The live stream delivered stdout through to the exit, so a live reader already holds the full output even if the buffered copy was truncated. |
stderr_complete | bool | Same, for stderr. |
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.
| 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. |
Sailbox snapshot fields
The monitoring snapshot carried by every Sailbox returned from
Sailbox.get and Sailbox.list.
| 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 | Version of the in-guest agent the sailbox last booted with. |
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 | datetime | Timestamps (Date in TypeScript, OffsetDateTime in Rust). |
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[Sailbox] | The sailboxes on this page. |
limit | int | Page size used. |
offset | int | Page offset used. |
total | int | Total matching sailboxes. |
has_more | bool | Whether more sailboxes exist beyond this page. |