> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sailresearch.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sailboxes

> The Sailbox class: create, operate, and tear down Sailboxes

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](/sailboxes); this page is the API reference. In a language
without a Sail SDK, reach for the [HTTP API](/sailboxes-http-api).

<CodeGroup>
  ```python Python theme={null}
  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)
  ```

  ```typescript TypeScript theme={null}
  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);
  ```

  ```rust Rust theme={null}
  use sail::{Client, CreateSailboxRequest};

  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());
  ```
</CodeGroup>

## 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 is [`shell`](#shell), 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`.

<CodeGroup>
  ```python Python theme={null}
  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())
  ```

  ```typescript TypeScript theme={null}
  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();
  ```

  ```rust Rust theme={null}
  use sail::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?;
  ```
</CodeGroup>

## 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. Every TypeScript static also accepts an explicitly constructed
`client` in its options object (see
[Configuration](/reference/sdk-configuration)); the signatures on this page omit it.

## Attributes

Every `Sailbox` carries its identity and lifecycle state: `sailbox_id`,
`name`, and `status` (`running`, `paused`, `sleeping`, `failed`,
`terminated`). A `Sailbox` returned by [`get`](#sailbox-get) or
[`list`](#sailbox-list) also carries the monitoring snapshot from that fetch:
owning app, image, configured and observed resource usage, timestamps, and its
network policy (see [snapshot fields](#sailboxinfo); absent means public).

Treat `sailbox_id` as the durable handle: store the id, and turn it back into
a usable `Sailbox` any time with [`Sailbox.get`](#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.

***

<h2 id="sailbox-create">
  Sailbox.create
</h2>

<CodeGroup>
  ```python Python theme={null}
  @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_limit_gib: int | None = None,
      disk_limit_gib: int | None = None,
      ingress_ports: Sequence[int | IngressPort] | None = (),
      volumes: Mapping[str, Volume | str] | None = None,
      visibility: Literal["org", "private"] = "org",
      network_policy: NetworkPolicy | NetworkAllowlist | Literal["public", "no_network"] = NetworkPolicy.PUBLIC,
  ) -> Sailbox
  ```

  ```typescript TypeScript theme={null}
  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" | "l";
    memoryLimitGib?: number;
    diskLimitGib?: number;
    ingressPorts?: (number | IngressPortInput)[];
    volumes?: Record<string, Volume | string>;
    visibility?: "org" | "private";
    networkPolicy?: NetworkPolicy;
  }): Promise<Sailbox>
  ```

  ```rust Rust theme={null}
  pub async fn create_sailbox(
      &self, // Client
      req: &CreateSailboxRequest,
      timeout: Option<Duration>,
  ) -> Result<Sailbox, SailError>
  ```
</CodeGroup>

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; ongoing Sailbox
billing uses your actual CPU, memory, and disk consumption. Each size also has
a one-time creation charge. `memory_limit_gib` and `disk_limit_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 Sailbox can use.
See [Sailbox pricing](/sailboxes-pricing).

| Parameter             | Default                | Description                                                                                                                                                                                                                                                                                                                                                                                     |
| --------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image`               | `None`                 | A `sail.Image` value or custom [image definition](/sailbox-sdk-images). Defaults to a prebuilt Debian base (no image build).                                                                                                                                                                                                                                                                    |
| `app`                 | required               | The owning [app](/sailbox-sdk-apps): 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"`, `"m"` (the platform default), or `"l"`. Each size sets the vCPU count plus default memory and disk; `"s"` gives the fastest cold starts and resumes, and caps what a runaway workload can consume.                                                                                                                                                                        |
| `memory_limit_gib`    | `None`                 | Memory ceiling in whole GiB, within the size's range: 2-64 for `"s"`, 8-128 for `"m"`, 16-256 for `"l"`. The size's default when omitted.                                                                                                                                                                                                                                                       |
| `disk_limit_gib`      | `None`                 | Disk ceiling in whole GiB, within the size's range: 8-128 for `"s"`, 32-512 for `"m"`, 64-1024 for `"l"`. The size's default when omitted.                                                                                                                                                                                                                                                      |
| `ingress_ports`       | empty                  | Guest ports to expose. Each entry is a bare `int` (HTTP shorthand) or an [`IngressPort`](#ingressport).                                                                                                                                                                                                                                                                                         |
| `volumes`             | `None`                 | Shared volumes to mount, mapping an absolute guest path to a [`Volume`](#volumes) (or its id).                                                                                                                                                                                                                                                                                                  |
| `visibility`          | `"org"`                | Who may operate the Sailbox: `"org"` (anyone in your org) or `"private"` (only you). See below.                                                                                                                                                                                                                                                                                                 |
| `network_policy`      | `NetworkPolicy.PUBLIC` | How the Sailbox may reach the network, chosen at creation and fixed for its life: `NetworkPolicy.PUBLIC` (the default), `NetworkPolicy.NO_NETWORK` (TypeScript `"no_network"`) for no network access at all, or a `NetworkAllowlist(...)` to allow only the listed destinations. `NO_NETWORK` cannot be combined with `ingress_ports`; an allowlist can. See [Network policy](#network-policy). |

<Note>
  Volumes are currently in Alpha. To pilot them, reach out in the [Sail
  Slack](https://join.slack.com/t/sailresearchcrew/shared_invite/zt-41pdcym9j-UU0Ey~A~r6n2H0DQVQsQHQ).
</Note>

**Returns** a running `Sailbox`.

**Raises** a creation error when Sail cannot create the Sailbox, a
permission error on 401/403, and an invalid-argument error for an unknown
`size` or invalid ingress ports and
volume mounts.

<Note>
  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](/sailbox-sdk-images).
</Note>

### SSH

For a quick shell, use [`shell()`](#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.

Enable SSH after create with [`enable_ssh`](#sailbox-enable-ssh), which trusts
your org's SSH certificate authority, starts `sshd`, and exposes guest port `22`
as raw TCP. See the [SSH access guide](/sailboxes-networking#ssh-access) for the
access model, how to connect, and source restrictions.

`visibility` chooses who may operate the Sailbox, fixed for its life. `"org"`,
the default, lets any credential in your org exec, copy files, SSH into, or run
lifecycle operations on it. `"private"` restricts all of that to you: only your
credential can operate the Sailbox (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,
setting a wake time, and the pause, sleep, resume, terminate, and upgrade
operations by setting `SAIL_OWNER_OVERRIDE_REASON` (or the
`X-Sail-Owner-Override-Reason` header on raw HTTP calls); exposing or removing
listeners, 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 Sailbox's SSH server
accepts only its creator's certificates. From the CLI, pass
`--visibility private` to `sail box create`.

***

<h2 id="sailbox-get">
  Sailbox.get
</h2>

<CodeGroup>
  ```python Python theme={null}
  @classmethod
  def get(sailbox_id: str) -> Sailbox
  ```

  ```typescript TypeScript theme={null}
  static get(sailboxId: string): Promise<Sailbox>
  ```

  ```rust Rust theme={null}
  pub fn sailbox(&self, sailbox_id: impl Into<String>) -> Sailbox // Client
  ```
</CodeGroup>

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 Sailbox. Operations that run inside it (commands, file
reads and writes, network traffic) wake a sleeping Sailbox on demand; a
paused Sailbox rejects them until you call `resume`. 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()`.

<CodeGroup>
  ```python Python theme={null}
  sb = sail.Sailbox.get("sb_...")
  result = sb.exec("echo hello").wait()
  ```

  ```typescript TypeScript theme={null}
  const sb = await Sailbox.get("sb_...");
  const result = await (await sb.exec("echo hello")).wait();
  ```

  ```rust Rust theme={null}
  use sail::ExecOptions;

  let sb = client.sailbox("sb_...");
  let result = sb
      .exec_shell("echo hello", ExecOptions::default())
      .await?
      .wait()
      .await?;
  ```
</CodeGroup>

***

<h2 id="sailbox-list">
  Sailbox.list
</h2>

<CodeGroup>
  ```python Python theme={null}
  @classmethod
  def list(
      *,
      app_id: str | None = None,
      status: str | None = None,
      search: str | None = None,
      order: Literal["newest_active", "newest_created"] | None = None,
      limit: int | None = None,
  ) -> list[Sailbox]
  ```

  ```typescript TypeScript theme={null}
  static list(params?: {
    appId?: string;
    status?: SailboxStatusFilter;
    search?: string;
    order?: SailboxListOrder;
    limit?: number;
  }): Promise<Sailbox[]>
  ```

  ```rust Rust theme={null}
  pub async fn list_sailboxes(
      &self, // Client
      query: &ListSailboxesQuery,
  ) -> Result<SailboxPage, SailError>
  ```
</CodeGroup>

Lists Sailboxes for the current org. Python and TypeScript fetch pages
internally until every match (or `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`](/sailbox-sdk-apps) 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`](#sailbox-list-page) to control paging yourself or to
read the pagination envelope.

***

<h2 id="sailbox-list-page">
  Sailbox.list\_page
</h2>

<CodeGroup>
  ```python Python theme={null}
  @classmethod
  def list_page(
      *,
      app_id: str | None = None,
      status: str | None = None,
      search: str | None = None,
      order: Literal["newest_active", "newest_created"] | None = None,
      limit: int = 50,
      offset: int = 0,
  ) -> SailboxPage
  ```

  ```typescript TypeScript theme={null}
  static listPage(query?: ListSailboxesQuery): Promise<SailboxPage>
  ```

  ```rust Rust theme={null}
  pub async fn list_sailboxes(&self, query: &ListSailboxesQuery) -> Result<SailboxPage, SailError>
  ```
</CodeGroup>

Same call as `Sailbox.list`, but returns a [`SailboxPage`](#sailboxpage) with
the Sailboxes plus the `limit`/`offset`/`total`/`has_more` pagination envelope.
(In Rust, `list_sailboxes` always returns the page.)

***

## exec

<CodeGroup>
  ```python Python theme={null}
  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 | PtyConfig = False,
      env: Mapping[str, str] | None = None,
      user: str | int | None = None,
      idempotency_key: str | None = None,
      output_mode: OutputMode | Literal["auto", "pipe", "tail"] = "auto",
      output_buffer_bytes: int = 1048576,
      kwargs: Mapping | None = None,
  ) -> ExecProcess | Any
  ```

  ```typescript TypeScript theme={null}
  exec(command: string | readonly string[], options?: {
    timeoutSeconds?: number;
    background?: boolean;
    cwd?: string;
    openStdin?: boolean;
    pty?: boolean | PtyConfig;
    env?: Record<string, string>;
    user?: string;
    idempotencyKey?: string;
    outputMode?: "auto" | "pipe" | "tail";
    outputBufferBytes?: number;
  }): Promise<ExecProcess>
  ```

  ```rust Rust theme={null}
  pub async fn exec_shell(&self, command: &str, options: ExecOptions)
      -> Result<ExecProcess, SailError>;
  pub async fn exec(&self, argv: impl IntoIterator<Item = impl Into<String>>, options: ExecOptions)
      -> Result<ExecProcess, SailError>;
  ```
</CodeGroup>

Runs a command in the Sailbox and returns a process handle. By default a stream
you are reading pauses the command when you fall behind, so nothing is lost
until a cancel or the exec `timeout` ends the pauses,
and a stream you are not reading keeps only its most recent 1 MiB. To get every
byte, start reading right after `exec` returns. `output_mode` and
`output_buffer_bytes` (`outputMode` and `outputBufferBytes` in TypeScript)
change that; what counts as reading a stream differs by
language. See [Exec process](#sailboxexecprocess).

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. For
a function, `output_mode` must stay `auto`, and the function's complete encoded
response (its serialized return value, captured stdout and stderr, and any
error details, as encoded on the wire) must fit `output_buffer_bytes`; a larger
response raises `SailboxFunctionSerializationError`. A second call with the same `idempotency_key` while the function runs takes over its output, and the earlier call may then fail to decode its result. See
[Images & Functions](/sailbox-sdk-images).

| 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: `True` for the default terminal, or a `PtyConfig(term=..., cols=..., rows=...)` (TS `{ term, cols, rows }`) to set `$TERM` and the initial window, which default to `xterm-256color` and 80x24. `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`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `env`                 | `None`    | Extra environment variables for the command. Entries override the guest defaults (including `LANG` and the `IS_SANDBOX=1` sandbox marker) 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.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `user`                | `None`    | Run the command as this user: a user name or numeric uid, optionally with a group after a colon (`"alice"`, `1000`, `"alice:staff"`, the Docker `USER` syntax). A named user or group must exist in the Sailbox's `/etc/passwd` / `/etc/group`; a numeric uid or gid need not. `HOME` (plus `USER` and `LOGNAME` when a name resolves) default to the account, with `env` entries still winning. The command starts in the image's working directory when this user can enter it, and in the filesystem root otherwise. When `user` is not given, commands run as the image's `USER` if the image sets one, root otherwise; pass `user="0:0"` to force root (`"root"` is a user name like any other, resolved through the Sailbox's `/etc/passwd`). Sailboxes created before user support shipped must call [`upgrade`](#upgrade) once first; the exact spelling `user="0:0"` needs no upgrade. |
| `idempotency_key`     | `None`    | Defaults to a generated key, so retrying the initial submission won't double-launch the command. An exec has one live handle at a time: a second handle started with the same key takes over the stream, and the first stops receiving live output and resolves from a bounded recorded result. A first handle reconnecting after a dropped connection can race a handle that attached meanwhile, and either handle's result may come back incomplete; avoid overlapping same-key handles.                                                                                                                                                                                                                                                                                                                                                                                                      |
| `output_mode`         | `"auto"`  | What happens when a stream's buffer fills: `auto`, `pipe`, or `tail`. See **Output limits** under [Exec process](#sailboxexecprocess). `pipe` is not available with `pty`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `output_buffer_bytes` | `1048576` | Size of each stream's buffer in bytes, from 64 KiB to 64 MiB. It is what `wait()` returns per stream and how far a reader can fall behind before the command pauses (`auto`, `pipe`) or output is dropped (`tail`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

<CodeGroup>
  ```python Python theme={null}
  result = sb.exec("echo hi", timeout=5).wait()
  print(result.stdout, result.exit_code)

  # Stream output as it arrives (a chunk can be a partial line); the loop
  # ends when the command finishes:
  proc = sb.exec("for i in 1 2 3; do echo $i; sleep 1; done")
  for chunk in proc.stdout:
      print(chunk, end="")

  # Piping stdin:
  proc = sb.exec("wc -l", open_stdin=True)
  proc.stdin.write("one\ntwo\n")
  proc.stdin.close()
  print(proc.wait().stdout)
  ```

  ```typescript TypeScript theme={null}
  const result = await (await sb.exec("echo hi", { timeoutSeconds: 5 })).wait();
  console.log(result.stdout, result.exitCode);

  // Stream output as it arrives (a chunk can be a partial line); the loop
  // ends when the command finishes:
  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);
  }

  // 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);
  ```

  ```rust Rust theme={null}
  use sail::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(
          ["wc", "-l"],
          ExecOptions {
              open_stdin: true,
              ..Default::default()
          },
      )
      .await?;
  wc.write_stdin(b"one\ntwo\n").await?;
  wc.close_stdin().await?;
  print!("{}", wc.wait().await?.stdout);
  ```
</CodeGroup>

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 sleeping
Sailbox wakes it. A paused Sailbox rejects commands until you call `resume`.

***

## run

<CodeGroup>
  ```python Python theme={null}
  def run(
      command: str | Sequence[str],
      *,
      timeout: int | None = None,
      cwd: str | None = None,
      env: Mapping[str, str] | None = None,
      user: str | int | None = None,
      check: bool = False,
      idempotency_key: str | None = None,
      output_buffer_bytes: int = 1048576,
  ) -> ExecResult
  ```

  ```typescript TypeScript theme={null}
  run(command: string | readonly string[], options?: {
    timeoutSeconds?: number;
    cwd?: string;
    env?: Record<string, string>;
    user?: string;
    check?: boolean;
    idempotencyKey?: string;
    outputBufferBytes?: number;
    signal?: AbortSignal;
  }): Promise<ExecResult>
  ```

  ```rust Rust theme={null}
  pub async fn run(&self, argv: impl IntoIterator<Item = impl Into<String>>, options: RunOptions)
      -> Result<ExecResult, SailError>
  pub async fn run_shell(&self, command: &str, options: RunOptions)
      -> Result<ExecResult, SailError>
  ```
</CodeGroup>

Runs a command to completion and returns its buffered result: a one-shot
convenience over [`exec`](#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).
`user` picks the user the command runs as (see [`exec`](#exec)).

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. While
the first call is still running, a second call with the same key takes over
its output stream, and the earlier call's result may come back truncated.
In TypeScript, aborting `signal` force-cancels the remote command and rejects.

The result's `stdout` and `stderr` hold only the most recent
`output_buffer_bytes` (`outputBufferBytes` in TypeScript) of each stream
(1 MiB by default, up to 64 MiB), with
`stdout_truncated` and `stderr_truncated` set when older output was dropped;
the command never pauses for unread output. To get every byte, use
[`exec`](#exec) and read the
stream (see [Exec process](#sailboxexecprocess)). The interactive and detached
`exec` options (`open_stdin`, `pty`, `background`) and the `pipe` output mode
are not available on `run`.

<CodeGroup>
  ```python Python theme={null}
  result = sb.run("echo hello")
  print(result.exit_code, result.stdout)
  ```

  ```typescript TypeScript theme={null}
  const result = await sb.run("echo hello");
  console.log(result.exitCode, result.stdout);
  ```

  ```rust Rust theme={null}
  use sail::RunOptions;

  let result = sb.run_shell("echo hello", RunOptions::default()).await?;
  println!("{} {}", result.exit_code, result.stdout);
  ```
</CodeGroup>

***

## shell

<CodeGroup>
  ```python Python theme={null}
  def shell(
      command: str | None = None,
      *,
      shell: str | None = None,
      term: str | None = None,
      cwd: str | None = None,
      user: str | int | None = None,
      timeout: int | None = None,
      env: Mapping[str, str] | None = None,
      no_forward: bool = False,
  ) -> int
  ```

  ```typescript TypeScript theme={null}
  shell(command?: string, options?: {
    shell?: string;
    term?: string;
    cwd?: string;
    user?: string;
    timeoutSeconds?: number;
    env?: Record<string, string>;
    noForward?: boolean;
  }): Promise<number>
  ```

  ```rust Rust theme={null}
  pub async fn shell(
      &self,
      command: Option<&str>,
      options: ShellOptions,
  ) -> Result<i32, SailError>
  ```
</CodeGroup>

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) 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 Sailbox, without running an SSH
server. `shell` overrides the login shell (default `$SHELL`, else
`/bin/bash`); it is ignored when `command` is given. `env` adds environment
variables to the session, with the same precedence and reserved names as for
[`exec`](#exec).

The session runs as the image's `USER` when the image sets one, root
otherwise: the same identity [`exec`](#exec) uses. `user` runs it as someone
else instead (a user name or numeric uid, optionally with a group after a
colon, like `"alice"`, `1000`, `"alice:staff"`); `user="0:0"` is always root.
A `user` other than `"0:0"` requires a Sailbox whose guest honors requested
users; on older Sailboxes the session fails until [`upgrade`](#upgrade) is
called.

While the session is open, browser opens and localhost servers in the Sailbox are
forwarded to your machine. When a program in the Sailbox 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 Sailbox starts on `localhost` keeps serving inside the Sailbox 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 Sailbox 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 Sailbox's clipboard, and text copied inside the Sailbox 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. Plain
`exec` forwards nothing; for the same forwarding on `sail box exec --tty`, see
the [CLI reference](/reference/cli#sail-box-shell).

<CodeGroup>
  ```python Python theme={null}
  sb.shell()
  ```

  ```typescript TypeScript theme={null}
  await sb.shell();
  ```

  ```rust Rust theme={null}
  use sail::shell::ShellOptions;

  sb.shell(/* command */ None, ShellOptions::default()).await?;
  ```
</CodeGroup>

From the CLI:

```bash theme={null}
sail box shell <sailbox-id>
```

***

<h2 id="fs">
  The fs namespace
</h2>

File and directory operations live under the `fs` 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`, and
`upload_dir` and `download_dir` transfer whole directories.

Writes give what they create to the image's `USER` by default, or to root
when the image sets none. That is the same identity [`exec`](#exec) runs
commands as, so an uploaded file is usable by the code in the Sailbox. Reads
and the directory helpers act as root by default, so they work on any path.
Each operation except the reads and the directory download takes an
optional `user` in the Docker
`USER` syntax: a user name or numeric uid, optionally with a group after a
colon (`"alice"`, `1000`, `"alice:staff"`). The other directory helpers
then run as that user, with its permissions enforced. The writes give what
they create that owner (like `COPY --chown`) while the write itself always
runs as root, so it works even where the owner cannot write. `"0:0"` is
always root, and the directory upload gives what it creates the same way.
Reads and the download take no `user`: a `user` only decides which paths an
operation may touch and who owns what it creates, a read creates nothing in
the Sailbox, and a download reads any path as root the way the reads do.
A `user` other than `"0:0"` requires a Sailbox whose guest honors requested
users; on older Sailboxes the operation fails until [`upgrade`](#upgrade)
is called.

***

<h2 id="read">
  fs.read
</h2>

<CodeGroup>
  ```python Python theme={null}
  def read(path: str | PurePosixPath) -> bytes
  ```

  ```typescript TypeScript theme={null}
  read(path: string): Promise<Buffer>
  ```

  ```rust Rust theme={null}
  pub async fn read(&self, path: &str) -> Result<Vec<u8>, SailError>
  ```
</CodeGroup>

Reads a regular file from the Sailbox as bytes. Loads the whole file into
memory; for very large files (checkpoints, datasets) prefer
[`read_stream`](#read-stream). Raises a file-not-found error if the path does
not exist.

<CodeGroup>
  ```python Python theme={null}
  data = sb.fs.read("/workspace/output.txt")
  print(data.decode())
  ```

  ```typescript TypeScript theme={null}
  const data = await sb.fs.read("/workspace/output.txt");
  console.log(data.toString());
  ```

  ```rust Rust theme={null}
  let data = sb.fs().read("/workspace/output.txt").await?;
  println!("{}", String::from_utf8_lossy(&data));
  ```
</CodeGroup>

***

<h2 id="read-stream">
  fs.read\_stream
</h2>

<CodeGroup>
  ```python Python theme={null}
  def read_stream(path: str | PurePosixPath) -> FileStream  # iterable, sync and async
  ```

  ```typescript TypeScript theme={null}
  readStream(path: string): Promise<FileStream> // async-iterable
  ```

  ```rust Rust theme={null}
  pub async fn read_stream(&self, path: &str) -> Result<FileReader, SailError>
  ```
</CodeGroup>

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`.

<CodeGroup>
  ```python Python theme={null}
  with open("local.bin", "wb") as f:
      for chunk in sb.fs.read_stream("/workspace/large.bin"):
          f.write(chunk)
  ```

  ```typescript TypeScript theme={null}
  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();
  ```

  ```rust Rust theme={null}
  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?)?;
  }
  ```
</CodeGroup>

***

<h2 id="write">
  fs.write
</h2>

<CodeGroup>
  ```python Python theme={null}
  def write(
      path: str | PurePosixPath,
      data: str | bytes | bytearray | memoryview | IOBase,
      *,
      create_parents: bool = True,
      mode: int = 0o644,
      user: str | int | None = None,
  ) -> None
  ```

  ```typescript TypeScript theme={null}
  write(path: string, data: Buffer | Uint8Array | string, options?: {
    createParents?: boolean;
    mode?: number;
    user?: string;
  }): Promise<void>
  ```

  ```rust Rust theme={null}
  pub async fn write(
      &self,
      path: &str,
      data: &[u8],
      options: WriteOptions,
  ) -> Result<(), SailError>
  ```
</CodeGroup>

Writes data to a regular file in the Sailbox. Missing parent directories are
created by default in every language. `path` must be absolute. To write
several files in one call, see [`fs.write_files`](#write-files).

| Parameter        | Default  | Description                                                                                                                                                                                                                                                                                                      |
| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `path`           | required | Absolute destination path.                                                                                                                                                                                                                                                                                       |
| `data`           | required | Bytes or a string. Python also accepts a file-like object, streamed from the source.                                                                                                                                                                                                                             |
| `create_parents` | `True`   | Create missing parent directories.                                                                                                                                                                                                                                                                               |
| `mode`           | `0o644`  | POSIX permission bits (0–`0o777`).                                                                                                                                                                                                                                                                               |
| `user`           | `None`   | Owner for the written file and any directories the write creates (`"alice"`, `1000`, `"alice:staff"`). The write itself always runs as root, like `COPY --chown`. Omitted follows the image's `USER`, else root; `"0:0"` forces root. Requires a guest that honors requested users; see [the fs namespace](#fs). |

<CodeGroup>
  ```python Python theme={null}
  sb.fs.write("/workspace/input.txt", "hello\n")
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.write("/workspace/input.txt", "hello\n");
  ```

  ```rust Rust theme={null}
  use sail::WriteOptions;

  sb.fs()
      .write("/workspace/input.txt", b"hello\n", WriteOptions::default())
      .await?;
  ```
</CodeGroup>

***

<h2 id="write-files">
  fs.write\_files
</h2>

<CodeGroup>
  ```python Python theme={null}
  def write_files(
      files: Mapping[str | PurePosixPath, str | bytes | bytearray | memoryview | IOBase],
      *,
      create_parents: bool = True,
      mode: int = 0o644,
      user: str | int | None = None,
  ) -> None
  ```

  ```typescript TypeScript theme={null}
  writeFiles(files: Record<string, Buffer | Uint8Array | string>, options?: {
    createParents?: boolean;
    mode?: number;
    user?: string;
  }): Promise<void>
  ```

  ```rust Rust theme={null}
  pub async fn write_files<P: AsRef<str>, D: AsRef<[u8]>>(
      &self,
      files: impl IntoIterator<Item = (P, D)>,
      options: WriteOptions,
  ) -> Result<(), SailError>
  ```
</CodeGroup>

Writes several complete files in one call. `files` maps each absolute guest
path to its contents; every file gets the same `create_parents`, `mode`, and
`user` as [`fs.write`](#write). In Rust, `files` is any iterator of path and
contents pairs, borrowed or owned: each entry is streamed from the buffer you
pass, not copied first. How a batch runs, what happens on a failure,
and when to stream instead are covered in
[Filesystem](/sailboxes-filesystem#write-files).

<CodeGroup>
  ```python Python theme={null}
  sb.fs.write_files({"/workspace/a.txt": "first\n", "/workspace/b.txt": b"second\n"})
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.writeFiles({
    "/workspace/a.txt": "first\n",
    "/workspace/b.txt": "second\n",
  });
  ```

  ```rust Rust theme={null}
  use sail::WriteOptions;

  sb.fs()
      .write_files(
          [("/workspace/a.txt", &b"first\n"[..]), ("/workspace/b.txt", &b"second\n"[..])],
          WriteOptions::default(),
      )
      .await?;
  ```
</CodeGroup>

***

<h2 id="write-stream">
  fs.write\_stream
</h2>

<CodeGroup>
  ```python Python theme={null}
  def write_stream(
      path: str | PurePosixPath,
      *,
      create_parents: bool = True,
      mode: int = 0o644,
      user: str | int | None = None,
  ) -> FileWriter
  ```

  ```typescript TypeScript theme={null}
  writeStream(path: string, options?: {
    createParents?: boolean;
    mode?: number;
    user?: string;
  }): Promise<FileWriter>
  ```

  ```rust Rust theme={null}
  pub async fn write_stream(
      &self,
      path: &str,
      options: WriteOptions,
  ) -> Result<FileWriter, SailError>
  ```
</CodeGroup>

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`](#write) can consume
whole. The options match [`write`](#write), including `user` for the written
file's owner. Python makes the file mode explicit and defaults it to `0o644`.

<CodeGroup>
  ```python Python theme={null}
  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.
  ```

  ```typescript TypeScript theme={null}
  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;
  }
  ```

  ```rust Rust theme={null}
  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.
  ```
</CodeGroup>

***

<h2 id="directory-helpers">
  fs.mkdir / fs.remove / fs.exists
</h2>

<CodeGroup>
  ```python Python theme={null}
  def mkdir(path: str | PurePosixPath, *, user: str | int | None = None) -> None
  def remove(path: str | PurePosixPath, *, user: str | int | None = None) -> None
  def exists(path: str | PurePosixPath, *, user: str | int | None = None) -> bool
  ```

  ```typescript TypeScript theme={null}
  mkdir(path: string, options?: { user?: string }): Promise<void>
  remove(path: string, options?: { user?: string }): Promise<void>
  exists(path: string, options?: { user?: string }): Promise<boolean>
  ```

  ```rust Rust theme={null}
  pub async fn mkdir(&self, path: &str, user: Option<&str>) -> Result<(), SailError>
  pub async fn remove(&self, path: &str, user: Option<&str>) -> Result<(), SailError>
  pub async fn exists(&self, path: &str, user: Option<&str>) -> Result<bool, SailError>
  ```
</CodeGroup>

`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`](#ls) lists it. A failure (for
example a permission error) raises with the guest's stderr in the message.

With `user`, the helper runs as that user with its permissions enforced:
`mkdir` leaves the created directories owned by it, `remove` can only delete
what that user may delete, and `exists` reports what that user can observe (a
path it lacks permission to reach reports false).

<CodeGroup>
  ```python Python theme={null}
  sb.fs.mkdir("/workspace/results")
  if not sb.fs.exists("/workspace/results/run.lock"):
      sb.fs.remove("/workspace/results/stale")
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.mkdir("/workspace/results");
  if (!(await sb.fs.exists("/workspace/results/run.lock"))) {
    await sb.fs.remove("/workspace/results/stale");
  }
  ```

  ```rust Rust theme={null}
  sb.fs().mkdir("/workspace/results").await?;
  if !sb.fs().exists("/workspace/results/run.lock").await? {
      sb.fs().remove("/workspace/results/stale").await?;
  }
  ```
</CodeGroup>

***

<h2 id="ls">
  fs.ls
</h2>

<CodeGroup>
  ```python Python theme={null}
  def ls(path: str | PurePosixPath, *, user: str | int | None = None) -> list[DirEntry]
  ```

  ```typescript TypeScript theme={null}
  ls(path: string, options?: { user?: string }): Promise<DirEntry[]>
  ```

  ```rust Rust theme={null}
  pub async fn ls(&self, path: &str, user: Option<&str>) -> Result<Vec<DirEntry>, SailError>
  ```
</CodeGroup>

Lists a directory's immediate entries (no recursion) as
[`DirEntry`](#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. With `user`, the listing runs as that user, so a directory it cannot read
raises a permission error.

<CodeGroup>
  ```python Python theme={null}
  for entry in sb.fs.ls("/workspace"):
      print(f"{entry.type:9} {entry.size:>8} {entry.name}")
  ```

  ```typescript TypeScript theme={null}
  for (const entry of await sb.fs.ls("/workspace")) {
    console.log(entry.type, entry.size, entry.name);
  }
  ```

  ```rust Rust theme={null}
  for entry in sb.fs().ls("/workspace").await? {
      println!("{:?} {} {}", entry.entry_type, entry.size, entry.name);
  }
  ```
</CodeGroup>

***

<h2 id="upload-dir">
  fs.upload\_dir
</h2>

<CodeGroup>
  ```python Python theme={null}
  def upload_dir(local_dir: str | Path, guest_dir: str | PurePosixPath, *, user: str | int | None = None) -> None
  ```

  ```typescript TypeScript theme={null}
  uploadDir(dirs: { localDir: string; guestDir: string; user?: string }): Promise<void>
  ```

  ```rust Rust theme={null}
  pub async fn upload_dir(&self, local_dir: &Path, guest_dir: &str, user: Option<&str>) -> Result<(), SailError>
  ```
</CodeGroup>

Uploads a local directory's contents into a Sailbox directory. The local
directory's entries land inside the Sailbox directory, which is created if
needed. Entries the upload does not name are left in place; a same-named
file is replaced. Uploaded files keep their permission bits (the setuid,
setgid, and sticky bits are cleared) and belong to
the image's `USER` (root when the image sets none or its identity cannot be
read). A `user` (the same syntax the other operations take) gives the
entries to that user instead, like `COPY --chown`; it must exist in the
Sailbox, and like the other operations' `user` it requires a Sailbox whose
guest honors requested users. The Sailbox's image must provide `tar` and
`gzip`, which the transfer uses to ship the directory as one compressed
archive; the default images do.

<CodeGroup>
  ```python Python theme={null}
  sb.fs.upload_dir("./local_data", "/workspace/data")
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.uploadDir({
    localDir: "./local_data",
    guestDir: "/workspace/data",
  });
  ```

  ```rust Rust theme={null}
  sb.fs().upload_dir(Path::new("./local_data"), "/workspace/data").await?;
  ```
</CodeGroup>

***

<h2 id="download-dir">
  fs.download\_dir
</h2>

<CodeGroup>
  ```python Python theme={null}
  def download_dir(guest_dir: str | PurePosixPath, local_dir: str | Path) -> None
  ```

  ```typescript TypeScript theme={null}
  downloadDir(dirs: { guestDir: string; localDir: string }): Promise<void>
  ```

  ```rust Rust theme={null}
  pub async fn download_dir(&self, guest_dir: &str, local_dir: &Path) -> Result<(), SailError>
  ```
</CodeGroup>

Downloads a Sailbox directory's contents to a local directory. The Sailbox
directory's entries land inside the local directory, which is created if
needed. Entries the download does not name are left in place; a same-named
file is replaced. The transfer reads every file in the tree, so download
directories of ordinary files: system trees like `/proc` or `/sys` hold files that cannot
be read as plain data, and downloading them fails. A file that is being
written while the download runs is captured as it is at that moment, the
way copying a live file would; download after writers finish for a
consistent copy. On Windows, a directory that contains symbolic links
cannot be downloaded, since Windows restricts creating them. The Sailbox's
image must provide `tar` and `gzip`, which the transfer uses to ship the
directory as one compressed archive; the default images do.

<CodeGroup>
  ```python Python theme={null}
  sb.fs.download_dir("/workspace/results", "./local_results")
  ```

  ```typescript TypeScript theme={null}
  await sb.fs.downloadDir({
    guestDir: "/workspace/results",
    localDir: "./local_results",
  });
  ```

  ```rust Rust theme={null}
  sb.fs().download_dir("/workspace/results", Path::new("./local_results")).await?;
  ```
</CodeGroup>

***

## listener / listeners

<CodeGroup>
  ```python Python theme={null}
  def listener(guest_port: int) -> Listener
  def listeners() -> list[Listener]
  def wait_for_listener(
      guest_port: int,
      *,
      timeout: float = 60.0,
  ) -> Listener
  ```

  ```typescript TypeScript theme={null}
  listener(guestPort: number): Promise<Listener>
  listeners(): Promise<Listener[]>
  waitForListener(guestPort: number, options?: {
    timeoutSeconds?: number; // 60
    signal?: AbortSignal;
  }): Promise<Listener>
  ```

  ```rust Rust theme={null}
  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,
      options: WaitForListenerOptions, // timeout (60s)
  ) -> Result<Listener, SailError>;
  ```
</CodeGroup>

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.

<CodeGroup>
  ```python Python theme={null}
  listener = sb.wait_for_listener(3000, timeout=60)
  print(listener.endpoint.url)
  ```

  ```typescript TypeScript theme={null}
  const listener = await sb.waitForListener(3000, { timeoutSeconds: 60 });
  if (listener.endpoint?.kind === "http") {
    console.log(listener.endpoint.url);
  }
  ```

  ```rust Rust theme={null}
  use sail::{ListenerEndpoint, WaitForListenerOptions};

  let listener = sb
      .wait_for_listener(3000, WaitForListenerOptions::default())
      .await?;
  if let Some(ListenerEndpoint::Http { url }) = listener.endpoint() {
      println!("{url}");
  }
  ```
</CodeGroup>

Ports are exposed at create time via `ingress_ports`, or at runtime with
`expose`/`unexpose` (see [Networking](/sailboxes-networking)).

***

<h2 id="sailbox-enable-ssh">
  enable\_ssh
</h2>

<CodeGroup>
  ```python Python theme={null}
  def enable_ssh(
      *,
      allowlist: list[str] | None = None,
      wait: bool = True,
      timeout: float = 60.0,
  ) -> TcpEndpoint | None
  ```

  ```typescript TypeScript theme={null}
  enableSsh(options?: {
    allowlist?: string[];
    wait?: boolean; // true
    timeoutSeconds?: number; // 60
  }): Promise<SshEndpoint | null>
  ```

  ```rust Rust theme={null}
  pub async fn enable_ssh(
      &self,
      options: EnableSshOptions, // { allowlist, wait, timeout }
  ) -> Result<Option<SshEndpoint>, SailError>
  ```
</CodeGroup>

Prepares this Sailbox 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](/sailboxes-networking#ssh-access) 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 addresses or ranges 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

<CodeGroup>
  ```python Python theme={null}
  def checkpoint(
      *,
      name: str | None = None,
      ttl_seconds: int | None = None,
  ) -> SailboxCheckpoint
  ```

  ```typescript TypeScript theme={null}
  checkpoint(options?: {
    name?: string;
    ttlSeconds?: number;
  }): Promise<SailboxCheckpoint>
  ```

  ```rust Rust theme={null}
  pub async fn checkpoint(
      &self,
      options: CheckpointOptions, // { name, ttl }
  ) -> Result<SailboxCheckpoint, SailError>
  ```
</CodeGroup>

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. Upgrade a Sailbox that uses an older guest
payload before you create a checkpoint handle.

`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 the checkpoint expires:
seven days out unless you asked for a different window. Starting a Sailbox from
it after that fails.

***

## from\_checkpoint

<CodeGroup>
  ```python Python theme={null}
  @classmethod
  def from_checkpoint(
      checkpoint_id: str,
      *,
      name: str,
      timeout: int | None = None,
  ) -> Sailbox
  ```

  ```typescript TypeScript theme={null}
  static fromCheckpoint(options: {
    checkpointId: string;
    name: string;
    timeoutSeconds?: number;
  }): Promise<Sailbox>
  ```

  ```rust Rust theme={null}
  pub async fn create_from_checkpoint(
      &self, // Client
      checkpoint_id: &str,
      name: &str,
      timeout: Option<Duration>,
  ) -> Result<Sailbox, SailError>
  ```
</CodeGroup>

Creates a new running Sailbox, called `name`, from a durable checkpoint
handle returned by [`checkpoint`](#checkpoint). The new Sailbox gets a fresh network identity;
existing TCP connections do not carry over, and ingress ports are not
inherited. `timeout` (seconds, `> 0` when set) bounds the call, since a
restore can block for many minutes while the new Sailbox queues for
capacity. A call that times out fails, and the restore may still finish
in the background; the new Sailbox then shows up in
[`Sailbox.list`](#sailbox-list). Omit `timeout` to wait without a client-side
bound.

The new Sailbox restores the memory saved in the checkpoint as well as the
writable disk, so processes the original was running carry on there. Commands
you started with `exec` stop in the new Sailbox, though their writes up to the
checkpoint are kept, and one you started with the background option keeps
running there. Start the other commands you need again.

Sometimes the new Sailbox comes up cold instead, with the disk intact and
nothing running. Write code that expects a cold start.

<Warning>
  `checkpoint()` does not support a Sailbox that has volume mounts. Create a
  separate Sailbox without volume mounts before you create a checkpoint handle.
</Warning>

***

## upgrade

<CodeGroup>
  ```python Python theme={null}
  def upgrade() -> UpgradeResult
  ```

  ```typescript TypeScript theme={null}
  upgrade(): Promise<UpgradeResult>
  ```

  ```rust Rust theme={null}
  pub async fn upgrade(&self) -> Result<UpgradeResult, SailError>
  ```
</CodeGroup>

Upgrades this Sailbox's runtime to the latest version, picking up new
Sailbox features, fixes, and performance improvements without recreating the
Sailbox. 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

<CodeGroup>
  ```python Python theme={null}
  def pause() -> None
  def sleep(wake_at: Optional[datetime] = None) -> Optional[datetime]
  def resume() -> Sailbox
  def terminate() -> None
  ```

  ```typescript TypeScript theme={null}
  pause(): Promise<void>
  sleep(wakeAt?: Date): Promise<Date | undefined>
  resume(): Promise<void>
  terminate(): Promise<void>
  ```

  ```rust Rust theme={null}
  pub async fn pause(&self) -> Result<(), SailError>;
  pub async fn sleep(&self, wake_at: Option<OffsetDateTime>) -> Result<Option<OffsetDateTime>, SailError>;
  pub async fn resume(&self) -> Result<(), SailError>;
  pub async fn terminate(&self) -> Result<(), SailError>;
  ```
</CodeGroup>

* **`pause`** checkpoints and pauses the Sailbox in memory until you
  explicitly resume it. Commands and network traffic do not wake a paused
  Sailbox.
* **`sleep`** checkpoints the Sailbox to disk; inbound traffic, an operation,
  or an explicit `resume` wakes it. An optional wake time schedules a
  wall-clock wake (see [Lifecycle](/sailboxes-lifecycle)).
* **`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](/sailboxes-lifecycle) for how these interact with
checkpoints, and [Pricing](/sailboxes-pricing) for billing.

***

<h2 id="volumes">
  Volumes
</h2>

A `Volume` 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`](#sailbox-create), keyed by the
absolute guest path to mount it at:

<CodeGroup>
  ```python Python theme={null}
  import sail

  vol = sail.Volume.find("shared-cache", mint_if_missing=True)

  sb = sail.Sailbox.create(
      app=app,
      name="worker-1",
      volumes={"/mnt/cache": vol},
  )
  ```

  ```typescript TypeScript theme={null}
  import { Sailbox, Volume } from "@sailresearch/sdk";

  const vol = await Volume.find("shared-cache", { mintIfMissing: true });

  const sb = await Sailbox.create({
    app,
    name: "worker-1",
    volumes: { "/mnt/cache": vol },
  });
  ```

  ```rust Rust theme={null}
  use sail::{CreateSailboxRequest, VolumeMount};

  let vol = client.get_volume("shared-cache", /* mint_if_missing */ true).await?;

  let sb = client
      .create_sailbox(
          &CreateSailboxRequest {
              app_id: app.id,
              name: "worker-1".into(),
              volume_mounts: vec![VolumeMount {
                  volume_id: vol.volume_id,
                  mount_path: "/mnt/cache".into(),
              }],
              ..Default::default()
          },
          /* timeout */ None,
      )
      .await?;
  ```
</CodeGroup>

The management surface:

<CodeGroup>
  ```python Python theme={null}
  @staticmethod
  def find(name: str, *, mint_if_missing: bool = False) -> Volume

  @staticmethod
  def list(*, max_objects: int | None = None) -> list[Volume]

  def delete(*, allow_missing: bool = False) -> Volume | None

  @staticmethod
  def delete_by_name(name: str, *, allow_missing: bool = False) -> Volume | None
  ```

  ```typescript TypeScript theme={null}
  static find(
    name: string,
    options?: ClientOptions & { mintIfMissing?: boolean }
  ): Promise<Volume>
  static list(options?: ClientOptions & { maxObjects?: number }): Promise<Volume[]>
  delete(options?: { allowMissing?: boolean }): Promise<boolean>
  ```

  ```rust Rust theme={null}
  pub async fn get_volume(&self, name: &str, mint_if_missing: bool) -> Result<VolumeInfo, SailError>
  pub async fn list_volumes(&self, max_objects: Option<i64>) -> Result<Vec<VolumeInfo>, SailError>
  pub async fn delete_volume(&self, volume_id: &str, allow_missing: bool) -> Result<Option<VolumeInfo>, SailError>
  ```
</CodeGroup>

* **`find`** looks up a volume by name; `mint_if_missing` creates it when no
  volume with that name exists.
* **`list`** returns the org's active volumes, newest first; `max_objects`
  caps the count.
* **`delete`** deletes the volume. With `allow_missing`, deleting an
  already-deleted volume succeeds instead of raising a not-found error.

Each handle carries `volume_id`, `name`, `backend`, `status`, and
`created_at` / `updated_at` timestamps.

***

<h2 id="http-policies">
  HTTP policies
</h2>

An [HTTP policy](/sailboxes-credentials#policies) shapes the HTTPS requests a Sailbox
sends; [credential injection](/sailboxes-credentials) is its main use.
Policies belong to your organization, and each Sailbox has at most one
attached at a time:

<CodeGroup>
  ```python Python theme={null}
  def set_http_policy(policy: HttpPolicy | HttpPolicySummary | str) -> None
  def http_policy() -> HttpPolicy | None
  def clear_http_policy() -> None
  ```

  ```typescript TypeScript theme={null}
  setHttpPolicy(policy: HttpPolicy | HttpPolicySummary | string): Promise<void>
  httpPolicy(): Promise<HttpPolicy | null>
  clearHttpPolicy(): Promise<void>
  ```

  ```rust Rust theme={null}
  pub async fn set_http_policy(&self, policy: &HttpPolicy) -> Result<(), SailError>;
  pub async fn http_policy(&self) -> Result<Option<HttpPolicy>, SailError>;
  pub async fn clear_http_policy(&self) -> Result<(), SailError>;
  ```
</CodeGroup>

* **`set_http_policy`** attaches a policy, replacing any policy already
  attached. In Python and TypeScript, pass a policy handle, a summary row
  from a listing, or a policy id string.
* **`http_policy`** returns the attached policy, or `None` / `null` when no
  policy is attached.
* **`clear_http_policy`** detaches the policy. It also succeeds when no policy
  is attached.

A policy change applies to HTTPS connections the Sailbox opens after the
call; connections already open keep the previous policy until they close.
Create policies and store the secrets they reference with the org-scoped
`HttpPolicy` and `Secret` surfaces (`Client` methods in Rust). The
[credential injection guide](/sailboxes-credentials) walks through the full
flow in every language.

***

## Supporting types

Field names below use the Python spelling; TypeScript exposes the same fields
in camelCase.

<h3 id="direntry">
  DirEntry
</h3>

One entry in a directory listing from [`fs.ls`](#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.        |

<h3 id="network-policy">
  Network policy
</h3>

The `network_policy` create argument (TypeScript `networkPolicy`) is one of:

* omit it (or `None`) for public networking, the default.
* `NetworkPolicy.NO_NETWORK` (TypeScript `"no_network"`, Rust
  `NetworkPolicy::NoNetwork`) for no network access at all. It cannot be
  combined with `ingress_ports`.
* a `NetworkAllowlist(hosts)` (TypeScript `{ mode: "allowlist", allowedHosts }`,
  Rust `NetworkPolicy::Allowlist(hosts)`) to allow only the listed
  destinations: hostnames, `*.` wildcard hostnames, IPv4 addresses, and IPv4
  ranges in CIDR form, at least one and at most 128. Only connections the
  Sailbox opens are limited, so it combines with `ingress_ports` and SSH. A
  list that breaks the [entry rules](/sailboxes-network-policy#entries) fails
  the call with `InvalidArgumentError` before a Sailbox is created.

The policy is fixed for the Sailbox's life, and `from_checkpoint` keeps the
policy of the source. `get` and `list` read it back as a `NetworkPolicyInfo`:
`mode` and, for an allowlist, the stored `allowed_hosts`. It is absent when
the Sailbox is public. The [network policy guide](/sailboxes-network-policy)
covers what each entry allows.

<h3 id="ingressport">
  IngressPort
</h3>

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`   | Addresses, ranges, or Sail app names allowed to connect. Empty means public.          |

Address and range 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 addresses and ranges). An entry that reads as an
address or a range is taken as one, so an app name cannot read as either, and
cannot contain a `/`. An address must not carry an IPv6 zone, such as
`fe80::1%eth0`, which names an interface on one machine rather than a source.

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"]`.

<CodeGroup>
  ```python Python theme={null}
  sb = sail.Sailbox.create(
      app=app,
      name="db-box",
      ingress_ports=[80, 443, sail.IngressPort(5432, "tcp", allowlist=["203.0.113.0/24"])],
  )
  ```

  ```typescript TypeScript theme={null}
  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"] },
    ],
  });
  ```

  ```rust Rust theme={null}
  use sail::{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?;
  ```
</CodeGroup>

<h3 id="sailboxexecprocess">
  Exec process
</h3>

A handle to a command running in the Sailbox (`ExecProcess` in Python,
`ExecProcess` in TypeScript and Rust). The command runs inside the Sailbox, and
a temporary network interruption does not kill it. Sail may reattach after an
interruption, but reattachment does not guarantee exact output replay. Closing
the handle abandons the live attachment without killing the command.

**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).

**Output limits.** Each stream has a buffer, 1 MiB by default.
`output_buffer_bytes` (`outputBufferBytes` in TypeScript) sets its size, from
64 KiB to 64 MiB. The `output_mode` option (`outputMode` in TypeScript) sets what happens when
a buffer fills.
This applies to commands started without a `pty`; a `pty` command always
behaves like `tail`.

* `auto`, the default. If you are reading a stream and fall behind, the
  command pauses when the buffer fills and resumes as you read, like a pipe.
  If you are not reading a stream, the command never pauses and the stream
  keeps only its most recent bytes. Reading a stream is how you get every
  byte, and it slows the command when you cannot keep up.
* `pipe`. The command pauses when either buffer fills and stays paused until
  you read that stream, so nothing is lost while you are late to start
  reading. Read both streams, or the command stays paused on the one you
  ignore; `2>&1` or `2>/dev/null` in a shell command is the easy way out.
  Once you release a reader, its stream goes back to keeping only its most
  recent bytes. `wait()` without a reader waits for as long as the command
  stays paused. Not available with a `pty`.
* `tail`. The command never pauses for you. Each stream keeps only its most
  recent bytes, even while you are reading it, so a slow reader skips output
  without notice (a Rust `StreamReader` can check `took_drop`).
  `stdout_truncated` and `stderr_truncated` say only that the result holds
  less than the command wrote, which is also the case after a reader consumed
  everything.

Stdout and stderr are handled separately. Sending `cancel()`, and the exec
`timeout`, end every pause: from then on each stream keeps only its most recent
bytes, so a reader more than a buffer behind skips ahead. A command that ignores
the cancel signal keeps running that way; cancel with `force` to stop it. The
command keeps its original timeout. If a handle attaches to a command launched
earlier under the same `idempotency_key`, that handle's pause deadline starts
when the attachment succeeds, so it can release the pauses one full timeout
after that; `cancel()` and `close()` release them at once. A command that
never pauses for you still runs no faster than your connection carries its
output; when output outruns the connection, the command pauses on the Sailbox
until the backlog drains.

With `auto`, start reading right after `exec()` returns to get every byte
(what starts a read differs by language; see below). You can read stdout
without holding stderr, or the reverse. The stream you are not holding keeps
its most recent bytes and never pauses the command when it fills, and a reader
that starts on it late begins with whatever is still held; with `pipe`, that
is everything. If you hold both readers, read them at the same time, each from
its own thread or task
(`asyncio.gather` in Python, `Promise.all` in TypeScript, two tasks in Rust).
The exit code is available from `poll()` (Python also has `exit_code`) as soon
as the streams end.

**Claiming and releasing a stream.** Each stream can be read once; a second
attempt fails (`InvalidArgumentError` in Python and TypeScript,
`SailError::InvalidArgument` in Rust), before or after the first reader is
released. After release, Sail again keeps only the stream's most recent bytes,
in every mode.

* **Python.** Accessing `proc.stdout` or `proc.stdout_bytes` (and the `stderr`
  twins) claims the stream and returns a generator; from that access on the
  command pauses rather than lose output (see **Output limits** for the
  exceptions). The stream is released when the
  generator ends, when you call `close()` on it (`await` its `aclose()` for an
  async generator), or when nothing references it any more (a `for` loop over
  `proc.stdout` drops it when the loop ends, including by `break`). To stop
  early on purpose, keep the generator in a variable and call `close()`.
* **TypeScript.** Accessing `proc.stdout` or `proc.stderr` claims nothing. The
  stream is claimed when iteration starts (`for await`, `.raw()`, `.text()`,
  `.bytes()`) or when you call `.toReadable()`. It is released when the
  iteration finishes or you leave it (`break`, `return`, or a thrown error
  inside `for await`), when `.text()` or `.bytes()` reaches the end, or when
  the `Readable` is destroyed (released at once, even while a read is waiting
  for output).
* **Rust.** `reader(OutputStream::Stdout)` or `reader_async(...)` claims the
  stream. It is held while the reader value exists and released when the
  reader is dropped.

In every language, `close()` on the handle, or your process exiting, releases
both streams. The command keeps running and never pauses. `wait()` fails after
`close()` unless it already resolved a result. If the connection to the
Sailbox is interrupted, the command keeps
running and does not pause while Sail reconnects; output produced in the
meantime can be missing, and `stdout_truncated` / `stderr_truncated` report
when that happened. Reattachment is best-effort, not an exact replay.

A `pty` command never pauses: Sail keeps only its most recent output, up to
the buffer size, so a reader further behind than that misses older output. Use
`resync()` to
ask the command to repaint its current screen.

**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()`** waits for the command to finish and returns its
[result](#sailboxexecresult), holding each stream's buffer, its most recent
output (see **Output limits** above). It never pauses the command itself and
can be called while a reader is still open; with `pipe`, it waits for as long
as an unread stream keeps the command paused. After `close()` it fails,
unless a result was already resolved; a repeat call returns that result. 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 once the output stream
has ended, else nothing. It never blocks and never drops output, so it is the
way to get the exit code after reading the streams yourself. If the connection
was lost for good mid-command, the stream ends early with the outcome still
unknown: `poll()` stays empty and `wait()` fetches the result Sail recorded.

**`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()`** abandons the handle without killing the command. It releases both
streams. The command keeps running and never pauses, and Sail keeps only the
most recent output of each stream. Call `cancel()` instead if the command
should stop. `wait()` fails after `close()` unless it already resolved a
result.

**`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.

<h3 id="sailboxexecresult">
  Exec result
</h3>

The output of a completed exec (`ExecResult` in Python, `ExecResult` in
TypeScript and Rust).

| Field              | Type   | Description                                                                       |
| ------------------ | ------ | --------------------------------------------------------------------------------- |
| `stdout`           | `str`  | The most recent standard output, up to the exec's buffer size (1 MiB by default). |
| `stderr`           | `str`  | The most recent standard error, up to the exec's buffer size (1 MiB by default).  |
| `exit_code`        | `int`  | Process exit code.                                                                |
| `timed_out`        | `bool` | The command hit its `timeout` budget and was killed.                              |
| `stdout_truncated` | `bool` | The command wrote more stdout than `stdout` holds, so older bytes are missing.    |
| `stderr_truncated` | `bool` | The command wrote more stderr than `stderr` holds, so older bytes are missing.    |

<h3 id="sailboxlistener">
  Listener
</h3>

An exposed guest port and how to reach it.

Every listener carries its guest port, `protocol`, route status, and a typed
`endpoint`: an [`HttpEndpoint`](#httpendpoint) (with `url`) or a
[`TcpEndpoint`](#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)`.

<h3 id="httpendpoint">
  HttpEndpoint
</h3>

The routable HTTPS address of an `"http"` listener.

| Field | Type  | Description                               |
| ----- | ----- | ----------------------------------------- |
| `url` | `str` | The HTTPS URL to reach the guest service. |

<h3 id="tcpendpoint">
  TcpEndpoint
</h3>

The host and port to connect to for a `"tcp"` listener.

| Field  | Type  | Description       |
| ------ | ----- | ----------------- |
| `host` | `str` | Hostname to dial. |
| `port` | `int` | Port to dial.     |

<h3 id="sailboxinfo">
  Sailbox snapshot fields
</h3>

The monitoring snapshot carried by every `Sailbox` returned from
[`Sailbox.get`](#sailbox-get) and [`Sailbox.list`](#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 maximums.                                                                                                                                                    |
| `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 managed runtime the Sailbox last booted with.                                                                                                            |
| `deprecation`                                       | `SailboxDeprecation \| None` | Set when this Sailbox's managed runtime should be upgraded and the caller can act on it: a `deadline` date and a `message` with upgrade instructions. `None` otherwise. |
| `error_message`                                     | `str \| None`                | Failure detail, when applicable.                                                                                                                                        |
| `checkpoint_generation`                             | `int`                        | Monotonic checkpoint counter.                                                                                                                                           |
| `started_at` / `last_checkpointed_at`               | `datetime \| None`           | Timestamps (`Date` in TypeScript, `OffsetDateTime` in Rust).                                                                                                            |
| `created_at` / `updated_at`                         | `datetime`                   | Timestamps (`Date` in TypeScript, `OffsetDateTime` in Rust).                                                                                                            |
| `created_by_user_id`                                | `str \| None`                | The user whose credential created the Sailbox; `None` for service-key creates.                                                                                          |
| `visibility`                                        | `str \| None`                | `"private"` for creator-restricted Sailboxes; `None`/`"org"` otherwise.                                                                                                 |
| `network_policy`                                    | `NetworkPolicyInfo \| None`  | The Sailbox's network policy: `None` means public; otherwise `mode` (`"no_network"` or `"allowlist"`) and, for an allowlist, the `allowed_hosts` in effect. Read-only.  |

<Note>
  Observed-usage fields reflect the latest live sample within roughly the last
  two minutes, falling back to zero when no recent sample is available.
</Note>

<h3 id="sailboxpage">
  SailboxPage
</h3>

One page of [`Sailbox.list_page`](#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. |
