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

# Python SDK

> Python SDK installation and full reference

The Sail Python SDK (`sail` on PyPI) supports Python 3.9+. It shares one
engine with the [TypeScript](/reference/typescript-sdk) and
[Rust](/reference/rust-sdk) SDKs, so behavior matches across languages.

## Install

<CodeGroup>
  ```bash pip theme={null}
  pip install sail
  ```

  ```bash uv theme={null}
  uv add sail
  ```
</CodeGroup>

Installing the Python SDK also puts the `sail` CLI on your `PATH`. To install
the CLI on its own, see [Install the CLI](/reference/cli).

The Sail API warns when your SDK version is nearing the end of its support
window. The SDK emits it as a `SailDeprecationWarning` through Python's
`warnings` module, once per process. A version past the end of its support
window is rejected with an upgrade error before any operation runs. Upgrade
with `pip install -U sail`.

## Configure

Set `SAIL_API_KEY` in the environment; the SDK also reads the credential
`sail auth login` stores under `~/.sail`. See
[Configuration](/reference/sdk-configuration).

## Quickstart

```python theme={null}
import sail

# Look up (or create) the app your sandboxes belong to.
app = sail.App.find(name="example-app", mint_if_missing=True)

# Boot a sandbox.
sb = sail.Sailbox.create(app=app, name="worker-1")

# Run a command and stream its output.
proc = sb.exec("echo hello && ls /")
for chunk in proc.stdout:
    print(chunk, end="")
result = proc.wait()
print("exit code:", result.exit_code)

# Move files.
sb.fs.write("/tmp/note.txt", "hi\n")
contents = sb.fs.read("/tmp/note.txt")

# Clean up (see also pause / sleep / resume / checkpoint).
sb.terminate()
```

## Sync and async

Every method that does I/O has an async twin under `.aio` (the interactive
`shell` is sync-only), so the same code works from scripts and from `asyncio`.
You choose sync or async once, at the call. A handle returned by an `.aio` call
is already async (`await proc.wait()`, `async for chunk in proc.stdout`), with
no further `.aio`:

```python theme={null}
sb = sail.Sailbox.create(app=app, name="box")
sb = await sail.Sailbox.create.aio(app=app, name="box")
```

See [Sailbox → Sync and async](/sailbox-sdk#sync-and-async) for streaming and
end-to-end examples.

## Python-only features

* [`@sail.function`](/sailbox-sdk-images#sail-function): run a local Python
  function inside a Sailbox.
* [Voyages](/voyages-sdk) and [Inference](/voyages-sdk-inference): record
  agent runs and attribute model calls to them.

## Errors

Product and transport failures derive from `sail.SailError`, and the error
classes that match a Python builtin also inherit it (`sail.NotFoundError` is a
`LookupError`), so both `except sail.SailError` and idiomatic builtin handlers
work. A few argument mistakes raise plain `ValueError`/`TypeError`. See
[Errors](/sailbox-sdk-errors).

## Reference

The docs below are auto-generated.

<div className="reference-fold prose prose-gray dark:prose-invert">
  <a id="sail.sailbox.Sailbox" />

  ## Sailbox

  A sandbox instance on the Sail platform: the operable handle plus the
  monitoring snapshot from the call that produced it.

  `sailbox_id` is the stable identifier and the durable external handle.
  Equality and hashing follow it: two handles for the same Sailbox compare
  equal, regardless of when their snapshots were taken. The remaining fields
  are the read-only snapshot as of `get`/`list`
  (`status`, resource usage, image, timestamps); a handle born from
  `create` carries only what the create response returns. Fetch a fresh
  snapshot with `Sailbox.get(sailbox_id)`. Every operation resolves the
  live endpoint on demand, waking a sleeping box only when the operation
  needs it.

  **Attributes:**

  | Attribute                | Type                           | Description                                                                                                                                                                                                  |
  | ------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | `sailbox_id`             | `str`                          | The Sailbox id: the stable, durable external handle.                                                                                                                                                         |
  | `name`                   | `str`                          | The Sailbox name.                                                                                                                                                                                            |
  | `status`                 | `str`                          | Lifecycle status (for example `"running"`).                                                                                                                                                                  |
  | `app_id`                 | `Optional[str]`                | Identifier of the owning app.                                                                                                                                                                                |
  | `app_name`               | `Optional[str]`                | Name of the owning app.                                                                                                                                                                                      |
  | `image_id`               | `Optional[str]`                | Identifier of the image the Sailbox was created from.                                                                                                                                                        |
  | `memory_mib`             | `Optional[int]`                | Configured memory, in MiB.                                                                                                                                                                                   |
  | `vcpu_count`             | `Optional[int]`                | Configured number of vCPUs.                                                                                                                                                                                  |
  | `state_disk_size_gib`    | `Optional[int]`                | Configured state-disk size, in GiB.                                                                                                                                                                          |
  | `cpu_requested_vcpu`     | `Optional[int]`                | Requested CPU, in vCPUs.                                                                                                                                                                                     |
  | `cpu_used_vcpu`          | `Optional[float]`              | Current CPU usage, in vCPUs.                                                                                                                                                                                 |
  | `memory_requested_bytes` | `Optional[int]`                | Requested memory, in bytes.                                                                                                                                                                                  |
  | `memory_used_bytes`      | `Optional[int]`                | Current memory usage, in bytes.                                                                                                                                                                              |
  | `disk_requested_bytes`   | `Optional[int]`                | Requested disk, in bytes.                                                                                                                                                                                    |
  | `disk_used_bytes`        | `Optional[int]`                | Current disk usage, in bytes.                                                                                                                                                                                |
  | `architecture`           | `Optional[str]`                | CPU architecture (for example `"arm64"`).                                                                                                                                                                    |
  | `guest_schema_version`   | `Optional[int]`                | Version of the managed Sailbox runtime the Sailbox last booted (or was created) with; the platform updates the runtime over time. `None` for handles born from `create`, which carry no monitoring snapshot. |
  | `deprecation`            | `Optional[SailboxDeprecation]` | Actionable runtime deprecation notice, when an upgrade is needed.                                                                                                                                            |
  | `error_message`          | `Optional[str]`                | Human-readable error detail when the box is in an error state.                                                                                                                                               |
  | `checkpoint_generation`  | `Optional[int]`                | Monotonic checkpoint generation counter.                                                                                                                                                                     |
  | `started_at`             | `Optional[datetime]`           | When the box last started, if it ever has.                                                                                                                                                                   |
  | `last_checkpointed_at`   | `Optional[datetime]`           | When the most recent checkpoint was taken, if any.                                                                                                                                                           |
  | `created_at`             | `Optional[datetime]`           | When the Sailbox was created.                                                                                                                                                                                |
  | `updated_at`             | `Optional[datetime]`           | When the Sailbox was last updated.                                                                                                                                                                           |
  | `created_by_user_id`     | `Optional[str]`                | The user whose credential created this box (for a fork or restore, the user who ran it). `None` for service-key creates.                                                                                     |
  | `visibility`             | `Optional[str]`                | `"private"` when access is restricted to the creator; `None`/`"org"` is the default org-wide access.                                                                                                         |

  <a id="sail.sailbox.Sailbox.create" />

  ### create

  Create a new Sailbox.

  ```python theme={null}
  @classmethod
  def create(
      *,
      app: Union[App, str],
      image: Optional[ImageDefinition] = None,
      name: str,
      image_build_timeout: int = 1800,
      timeout: int = 600,
      size: Optional[SailboxSize] = None,
      memory_gib: Optional[int] = None,
      disk_gib: Optional[int] = None,
      ingress_ports: Optional[List[Union[int, IngressPort]]] = None,
      volumes: Optional[Mapping[str, Any]] = None,
      ssh: bool = False,
      private: bool = False,
  ) -> Sailbox
  ```

  Custom image definitions are built first; the call then returns once
  the new box is running or creation has failed. Building follows the
  image contract (see `ImageDefinition.build`): the image may
  boot hidden at build time and periodically while in active use,
  sharing those boots' state with every Sailbox created from it via a
  start snapshot, so generate per-instance identity at runtime, not
  in boot-time jobs.

  `timeout` (seconds) bounds each create attempt, since creating a
  Sailbox can block for many minutes while it queues for
  capacity and boots the VM. The create is retried (reattaching to the same
  box), so it returns as soon as the box is ready and gives up after roughly
  three attempts. If the budget is exhausted it raises rather than hanging;
  the box may still come up server-side. To recover it, find its id with
  `Sailbox.list(search=name)`, then bind it with `from_id` or
  terminate it. Pass `0` to leave each attempt unbounded.

  `ingress_ports` exposes guest ports for ingress. Each
  entry is either a bare `int` (shorthand for an HTTP port) or an
  `IngressPort` carrying an explicit protocol, e.g.
  `ingress_ports=[80, 443, IngressPort(22, "tcp")]`. Call
  `listener` / `listeners` on the returned Sailbox for the
  public address of each exposed port: an HTTP listener's `endpoint` is
  an `HttpEndpoint` with a routable `url` and a TCP listener's is a
  `TcpEndpoint` with a `host`/`port` any TCP client can dial (for
  example `psql -h <host> -p <port>`).

  Pass `ssh=True` to get an SSH-ready box in one call. It calls
  `enable_ssh` on the new box, which trusts your org's SSH
  certificate authority, starts `sshd`, and exposes guest port 22 as
  `tcp`. A port-22 entry in `ingress_ports` is then not exposed at
  create; only its `allowlist` is kept, applied when `enable_ssh`
  exposes the port. To connect, wire up your machine with `sail box ssh
    alias <id>` (or `sail box ssh enable <id>`) and then `ssh <name>.sail`; a plain `ssh` to the raw port will not present your
  certificate.

  By default a box is org-wide: any credential in your org can exec,
  copy files, SSH, or run lifecycle operations on it. Pass
  `private=True` to restrict all of that to you. An org admin can
  override that with a recorded reason for exec, files, and
  pause/resume/terminate/upgrade. SSH, exposing or removing listeners, and
  fork/checkpoint/restore stay creator-only. Requires an API key
  minted by your user (not a service key).

  `volumes` mounts shared persistent NFS storage into the guest. Pass a
  mapping from absolute guest mount path to a `sail.Volume` returned
  by `sail.Volume.find()`, e.g. `volumes={"/mnt/shared": volume}`.
  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](https://join.slack.com/t/sailresearchcrew/shared_invite/zt-41pdcym9j-UU0Ey~A~r6n2H0DQVQsQHQ).

  `size` selects the resource size: `"s"`, `"m"` (the default),
  or `"l"`.
  Each size sets the vCPU count plus default memory and disk.
  Ongoing billing is based on observed usage, so a bigger size does not
  reserve CPU, memory, or disk. Each size has a separate one-time
  creation charge. Choose `"s"` when you want the fastest cold starts,
  forks, and resumes. Its lower ceilings also cap what a runaway workload
  can consume, so you don't accidentally use more than you need.
  `memory_gib` and `disk_gib` tune that size's default memory and
  disk ceilings in whole GiB, within its range.

  `image` is the image to boot; omit it for the prebuilt Debian base (an
  instant create with no build). A custom image is built at create if it
  is not already cached; `image_build_timeout` (seconds) bounds that
  build.

  Sail may sleep a fully idle box; it wakes transparently on traffic
  or the next operation.

  `await Sailbox.create.aio(...)` is the async form, building the image
  and provisioning the VM without blocking the event loop.

  <a id="sail.sailbox.Sailbox.list" />

  ### list

  List the Sailboxes for the current org that match the filters,
  fetching pages until every match (or `limit` of them) is collected.
  Use `list_page` to page through results manually instead.

  ```python theme={null}
  @classmethod
  def list(
      *,
      app_id: Optional[Union[App, str]] = None,
      status: Optional[SailboxStatus] = None,
      search: Optional[str] = None,
      order: Optional[SailboxListOrder] = None,
      limit: Optional[int] = None,
  ) -> List[Sailbox]
  ```

  Filters are server-side. `app_id` filters by the owning app: a
  `sail.App` value or an app id string (resolve an app name through
  `App.find` first if needed).
  `order` sorts the results: `"newest_active"` returns the most
  recently active first (the default the server applies),
  `"newest_created"` the newest-created first. `limit` caps the
  total returned, bounding the fetch for large orgs; `None` returns
  every match.

  <a id="sail.sailbox.Sailbox.list_page" />

  ### list\_page

  List one page of Sailboxes alongside the pagination envelope
  (`limit`/`offset`/`total`/`has_more`).

  ```python theme={null}
  @classmethod
  def list_page(
      *,
      app_id: Optional[Union[App, str]] = None,
      status: Optional[SailboxStatus] = None,
      search: Optional[str] = None,
      limit: int = DEFAULT_LIST_LIMIT,
      offset: int = 0,
      order: Optional[SailboxListOrder] = None,
  ) -> SailboxPage
  ```

  Takes the same filters as `list`, plus `limit` and `offset` to
  select the page. `order` sorts the results: `"newest_active"`
  returns the most recently active first (the default the server
  applies), `"newest_created"` the newest-created first.

  <a id="sail.sailbox.Sailbox.get" />

  ### get

  Fetch a Sailbox by id: the operable handle plus a fresh snapshot.

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

  Validates access first: wrong-org ids and unknown ids both surface as
  `LookupError` (the server returns 404 for both to avoid leaking
  ownership across orgs). Nothing wakes here; operations resume a paused
  or sleeping box on demand. Call `get` again for a fresh snapshot.

  <a id="sail.sailbox.Sailbox.from_id" />

  ### from\_id

  Bind a handle to an existing Sailbox id without a network call.

  ```python theme={null}
  @classmethod
  def from_id(sailbox_id: str) -> Sailbox
  ```

  The returned handle carries no snapshot fields (its `name` and
  `status` are empty), just the operable surface. The id is not
  verified to exist: operations on an unknown or inaccessible id fail
  with `NotFoundError`. Use `get` to validate the id and fetch
  a fresh snapshot instead.

  <a id="sail.sailbox.Sailbox.from_checkpoint" />

  ### from\_checkpoint

  Create a new running Sailbox from a durable checkpoint handle.

  ```python theme={null}
  @classmethod
  def from_checkpoint(
      checkpoint_id: str,
      *,
      name: Optional[str] = None,
      timeout: Optional[int] = None,
  ) -> Sailbox
  ```

  This is a new box branched from the checkpoint's state, not a resumption
  of the original. In-flight `Sailbox.exec()` sessions are reaped in the
  child: a command still running when the checkpoint was taken does not
  resume here. Its on-disk effects up to the checkpoint are preserved, but
  the command itself is not continued. Start fresh execs on the new box.
  `name` sets the new box's display name; `timeout` (seconds) bounds
  the call, must be positive when given, and defaults to the server's
  bound.

  <a id="sail.sailbox.Sailbox.fork" />

  ### fork

  Create a new running Sailbox from this one's live state.

  ```python theme={null}
  def fork(*, name: Optional[str] = None, timeout: Optional[int] = None) -> Sailbox
  ```

  The child copies this box's memory and writable disk as they are now,
  in one call, so it branches from the parent's current state while the
  parent keeps running. The copy is transient: no durable checkpoint is
  taken. To branch from a saved point in time instead, take a durable
  `checkpoint` and start boxes from it with `from_checkpoint`,
  which works even after the parent is gone.

  Like `from_checkpoint`, the child is a new independent box:
  commands still running in the parent do not continue in the child
  (their on-disk effects up to the fork are preserved); start fresh
  execs on the child. `name` sets the child's display name;
  `timeout` (seconds) bounds the call, must be positive when given,
  and defaults to the server's bound.

  <a id="sail.sailbox.Sailbox.terminate" />

  ### terminate

  Permanently terminate this Sailbox.

  ```python theme={null}
  def terminate() -> None
  ```

  Idempotent: terminating a box that is already terminated succeeds, so
  cleanup paths can call it unconditionally.

  <a id="sail.sailbox.Sailbox.pause" />

  ### pause

  Checkpoint and pause this Sailbox until it is explicitly resumed.

  ```python theme={null}
  def pause() -> None
  ```

  <a id="sail.sailbox.Sailbox.sleep" />

  ### sleep

  Checkpoint and sleep this Sailbox until traffic or a wake restores it.

  ```python theme={null}
  def sleep(wake_at: Optional[datetime] = None) -> Optional[datetime]
  ```

  `wake_at`, when given, schedules a wall-clock wake before the sleep
  starts and returns the effective wake time: the sooner of this
  request and any wake already scheduled. If the Sailbox is sleeping
  when that moment arrives, Sail restores it. The wake can fire a
  little after the time you set, so treat it as approximate. A naive
  `wake_at` is interpreted as local time. Calling `sleep` on an
  already-sleeping Sailbox succeeds and just updates the scheduled
  wake.

  <a id="sail.sailbox.Sailbox.checkpoint" />

  ### checkpoint

  Create a durable checkpoint handle for this Sailbox.

  ```python theme={null}
  def checkpoint(
      *,
      name: Optional[str] = None,
      ttl_seconds: Optional[int] = None,
  ) -> SailboxCheckpoint
  ```

  Running Sailboxes are snapshotted first. Paused and sleeping Sailboxes
  return a handle to their existing checkpoint.

  `name` sets a display name for the handle. `ttl_seconds`, when set,
  must be positive and overrides the server's default retention window;
  use it to keep a checkpoint you intend to reuse as a template alive
  longer than the default. The returned handle's `expires_at` reports
  when it will be garbage collected.

  <a id="sail.sailbox.Sailbox.upgrade" />

  ### upgrade

  Upgrade this Sailbox's runtime to the latest version.

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

  Upgrading picks up new Sailbox features, fixes, and performance
  improvements without recreating the box. A running Sailbox reboots
  in place on its current disk: all filesystem state is preserved, but
  processes restart as they would after a machine reboot (unflushed
  application state gets power-loss semantics). A paused or sleeping
  Sailbox is upgraded without waking. The upgrade is recorded and
  applies automatically at the next wake.

  Returns an `UpgradeResult`: `applied` is `True` when the
  upgrade happened immediately (the Sailbox was running) and `False`
  when it will apply at the next wake; already-up-to-date Sailboxes
  report `True` without rebooting.

  <a id="sail.sailbox.Sailbox.resume" />

  ### resume

  Resume this Sailbox through the public API.

  ```python theme={null}
  def resume() -> Sailbox
  ```

  <a id="sail.sailbox.Sailbox.listener" />

  ### listener

  Fetch one listener by guest port without waking the box.

  ```python theme={null}
  def listener(guest_port: int) -> Listener
  ```

  <a id="sail.sailbox.Sailbox.listeners" />

  ### listeners

  List this Sailbox's listeners without waking it.

  ```python theme={null}
  def listeners() -> list[Listener]
  ```

  <a id="sail.sailbox.Sailbox.wait_for_listener" />

  ### wait\_for\_listener

  Block until the listener on `guest_port` is reachable end to end.

  ```python theme={null}
  def wait_for_listener(guest_port: int, *, timeout: float = 60.0) -> Listener
  ```

  For an HTTP listener this probes the `url`, so a successful return
  means your guest HTTP server answered. For a TCP listener it opens a
  connection through the ingress edge and treats it as ready once the
  guest sends bytes (e.g. an SSH banner) or holds the connection open.
  This is a connectivity check, not an application-level health check.
  Raises `TimeoutError` if the listener does not become reachable
  within `timeout` seconds; `float("inf")` waits indefinitely.

  <a id="sail.sailbox.Sailbox.expose" />

  ### expose

  Expose an additional ingress port on this Sailbox at runtime.

  ```python theme={null}
  def expose(
      guest_port: int,
      protocol: IngressProtocol = "http",
      allowlist: Optional[List[str]] = None,
  ) -> Listener
  ```

  `protocol` is `"http"` (a routable URL, the default) or `"tcp"`
  (a public `host`/`port` for raw TCP: ssh, Postgres, etc.).
  `allowlist` restricts which source IP CIDRs (e.g. `["203.0.113.0/24"]`)
  or Sail app names may connect (app names on `"http"` listeners only;
  `"tcp"` allowlists must be CIDR prefixes). Re-exposing a port under the same protocol updates its
  `allowlist` to the value you pass. A raw-TCP port reclaims its previous
  address while your org still holds it idle; if another of your org's
  Sailboxes took the address over, a new one is allocated, so read the
  endpoint from the returned `Listener`. Changing an exposed port's
  protocol is rejected: `unexpose` an HTTP port and re-expose it, or use
  a different guest port for a raw-TCP one.
  Returns the `Listener` (its `route_status` is
  `"unknown"`: the expose response does not report reachability).

  This works on a paused or sleeping Sailbox without waking it; a later
  resume serves the new listener.
  Probing reachability with `wait_for_listener` needs a running,
  connected box, so wait only once the box is running.

  <a id="sail.sailbox.Sailbox.unexpose" />

  ### unexpose

  Stop serving an exposed ingress port on this Sailbox.

  ```python theme={null}
  def unexpose(guest_port: int) -> None
  ```

  A `"tcp"` port stops counting against your org's raw-TCP quota once
  removed, but its public `host`/`port` stays owned by your org:
  another of your org's Sailboxes may reuse the idle address, and it is
  never given to a different org. Re-`expose`-ing the same guest port
  reclaims the exact address while your org still holds it idle; after a
  reuse you get a new one. An `"http"` port carries no such reservation
  and is removed outright. Removing a port that is not exposed raises a
  `LookupError`.

  <a id="sail.sailbox.Sailbox.ingress_auth_headers" />

  ### ingress\_auth\_headers

  Fetch the ingress-identity headers for *this* Sailbox via the API.

  ```python theme={null}
  def ingress_auth_headers() -> Dict[str, str]
  ```

  Attach the returned headers to HTTP requests so they authenticate as
  this Sailbox against another listener whose `allowlist` contains
  this Sailbox's app name, useful for host-side orchestrators and tests
  that drive Sailboxes from outside. Requires an organization-scoped API
  key and a live (non-terminated) Sailbox.

  Inside a Sailbox guest, prefer the module-level
  `sail.ingress_auth_headers`, which reads the same values from
  the guest environment without an API call.

  <a id="sail.sailbox.Sailbox.enable_ssh" />

  ### enable\_ssh

  Make this Sailbox reachable over SSH, returning its endpoint.

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

  Installs your org's SSH certificate authority as trusted, (re)starts
  `sshd`, and exposes guest port 22 as `tcp` ingress once the CA-only
  daemon verifiably owns it (a failed enable never leaves port 22 newly
  exposed). Works on any running Sailbox; `create(ssh=True)` is sugar
  for calling this right after create. Safe to re-run: the box's host key
  is generated once and never rotated, so a caller's `known_hosts`
  stays valid; re-run it to bring `sshd` back up if the (unsupervised)
  daemon stops. `sshd` survives sleep and checkpoint→resume.

  `allowlist` restricts which source CIDRs may connect to port 22,
  replacing any existing restriction. Left empty, a first enable opens
  the port to any source, and a re-enable leaves an existing restriction
  unchanged. Disabling SSH (`sail box ssh disable`) unexposes port 22
  together with its restriction, so a later enable is a first enable.

  Anyone in the org connects with a short-lived certificate signed for
  their key, rather than installed keys (a private box is the exception,
  accepting only its creator's certificates). The `sail box ssh` CLI
  fetches that certificate and writes the local SSH config; this method only
  prepares the box. By default it blocks until the port-22 listener is
  reachable and returns its `TcpEndpoint`; pass `wait=False` to
  skip the readiness probe and return `None`.

  <a id="sail.sailbox.Sailbox.fs" />

  ### fs

  Filesystem operations on this Sailbox's guest: read and write files
  (buffered or streaming), and directory helpers.

  ```python theme={null}
  fs: SailboxFs
  ```

  <a id="sail.sailbox.Sailbox.run" />

  ### run

  Run a command to completion and return its buffered result.

  ```python theme={null}
  def run(
      command: Union[str, Sequence[str]],
      *,
      timeout: Optional[int] = None,
      cwd: Optional[str] = None,
      env: Optional[Mapping[str, str]] = None,
      check: bool = False,
      idempotency_key: Optional[str] = None,
  ) -> ExecResult
  ```

  A one-shot convenience over `exec` followed by `wait()`. A
  `str` runs via `/bin/sh -lc`; a sequence is exec'd directly. `env`
  adds environment variables for the command; `cwd` sets the working
  directory (string commands only, like `exec`). The result's
  stdout/stderr are the buffered output (capped, drop-oldest); for
  unbounded output, stream it live via `exec` instead.

  `check=True` raises `sail.CommandFailedError` (carrying the
  completed result as `result`) when the command exits nonzero or
  times out.

  When `timeout` (seconds) elapses, the command is killed and `run`
  returns an `ExecResult` with `timed_out=True`, raising only when
  `check=True`.

  `idempotency_key` deduplicates retries: calling `run` again with
  the same key returns the original command's result instead of
  launching it a second time.

  <a id="sail.sailbox.Sailbox.exec" />

  ### exec

  Run a shell command or decorated Python function in the Sailbox.

  ```python theme={null}
  def exec(
      command: Union[str, Sequence[str], SailFunction],
      *function_args: Any,
      timeout: Optional[int] = None,
      background: bool = False,
      cwd: Optional[str] = None,
      idempotency_key: Optional[str] = None,
      open_stdin: bool = False,
      pty: bool = False,
      term: Optional[str] = None,
      cols: int = 0,
      rows: int = 0,
      env: Optional[Mapping[str, str]] = None,
      args: Optional[Union[List[Any], Tuple[Any, ...]]] = None,
      kwargs: Optional[Mapping[str, Any]] = None,
  ) -> Union[ExecProcess, Any]
  ```

  For shell commands, returns a `ExecProcess` immediately
  after the backend accepts the command: iterate `proc.stdout` /
  `proc.stderr` for live output and call `proc.wait()` for the
  result. `open_stdin=True` opens the command's stdin for `proc.stdin`
  writes; by default stdin is `/dev/null` so stdin-reading commands
  see immediate EOF instead of blocking.

  `pty=True` runs the command under a pseudo-terminal: `isatty()` is
  true, control bytes written to `proc.stdin` become signals (Ctrl-C is
  `b"\x03"`), and `proc.resize(cols, rows)` adjusts the window.
  stdout and stderr merge onto `proc.output` (`proc.stderr` stays
  empty). `pty` implies `open_stdin`. For a full interactive shell that
  drives the local terminal, use `shell` instead.

  `env` adds environment variables for the command. Entries override
  the guest defaults (including `LANG`) and the image environment. A few
  reserved variables that identify the Sailbox (such as `SAILBOX_ID`)
  cannot be overridden. For pty execs the local terminal environment
  (`COLORTERM`, `LANG`, `LC_*`, `TERM_PROGRAM`) is forwarded
  automatically for keys not set here.

  `background=True` launches the command through a detached shell that
  returns immediately. Its output is discarded, so `proc.stdout` /
  `proc.stderr` stay empty and `proc.wait()` only confirms the
  launcher started it.

  `await sb.exec.aio(...)` is the async form: a shell command resolves
  to an `AsyncExecProcess`, a function to its return value.

  <a id="sail.sailbox.Sailbox.shell" />

  ### shell

  Open an interactive pty session on the Sailbox, driving the local terminal.

  ```python theme={null}
  def shell(
      command: Optional[str] = None,
      *,
      shell: Optional[str] = None,
      term: Optional[str] = None,
      cwd: Optional[str] = None,
      timeout: Optional[int] = None,
      no_forward: bool = False,
      no_forward_browser: bool = False,
  ) -> int
  ```

  With no `command`, runs an interactive login shell. Pass `command`
  to run that under a pty instead (e.g. a REPL or `vim`). Either way the
  session is bridged to the local terminal: raw-mode 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.

  This is the equivalent of `ssh`-ing into the box, without a separate
  SSH server. `shell` overrides the login shell (default `$SHELL` or
  `/bin/bash`); it is ignored when `command` is given.

  While attached, several local conveniences are forwarded: the box's
  browser opens and localhost servers reach your machine, files dragged
  onto the terminal upload into the box and paste as guest paths, and
  Ctrl+V forwards your clipboard. On devbox images the clipboard is
  two-way: pasted images and text land on the box's clipboard, and text
  copied inside the box comes back to yours. Other images upload a pasted
  image as a file and paste its path instead. Pass `no_forward=True` to
  turn all of it off (for example for an untrusted or automated session),
  or `no_forward_browser=True` to keep everything forwarded except
  browser opens.

  <a id="sail.app.App" />

  ## App

  A Sail application.

  **Attributes:**

  | Attribute    | Type       | Description                                           |
  | ------------ | ---------- | ----------------------------------------------------- |
  | `id`         | `str`      | Stable server-assigned app identifier.                |
  | `name`       | `str`      | Human-readable app name unique within the owning org. |
  | `created_at` | `datetime` | App creation time.                                    |

  <a id="sail.app.App.find" />

  ### find

  Find an app by name, optionally creating it if it doesn't exist.

  ```python theme={null}
  @classmethod
  def find(name: str, *, mint_if_missing: bool = False) -> App
  ```

  <a id="sail.app.App.list" />

  ### list

  Return every app the current org owns, newest first.

  ```python theme={null}
  @classmethod
  def list() -> list[App]
  ```

  Apps with no Sailboxes yet are included. The response is not paginated;
  the per-org app count is small.

  <a id="sail.image.ImageDefinition" />

  ## ImageDefinition

  <a id="sail.image.ImageDefinition.apt_install" />

  ### apt\_install

  Add an `apt-get install` step for `packages`.

  ```python theme={null}
  def apt_install(*packages: str) -> ImageDefinition
  ```

  <a id="sail.image.ImageDefinition.pip_install" />

  ### pip\_install

  Add a `pip install` step for `packages`.

  ```python theme={null}
  def pip_install(*packages: str) -> ImageDefinition
  ```

  <a id="sail.image.ImageDefinition.run_commands" />

  ### run\_commands

  Add shell commands to the build, each as its own step.

  ```python theme={null}
  def run_commands(*cmd: str) -> ImageDefinition
  ```

  <a id="sail.image.ImageDefinition.add_local_file" />

  ### add\_local\_file

  Bake the contents of one local file into the image at remote\_path.

  ```python theme={null}
  def add_local_file(
      local_path: Union[str, Path],
      remote_path: str,
      *,
      mode: Optional[int] = None,
  ) -> ImageDefinition
  ```

  The local file is hashed (sha256) and uploaded to Sail's
  content-addressed asset store; only the hash, target path, and mode
  flow into the image spec. A one-byte change to the local file
  therefore changes the resulting image\_id and forces a rebuild.

  `remote_path` must be an absolute POSIX path. If it ends with a
  slash, the basename of `local_path` is appended.
  `mode` is the POSIX permission bits (low 9 bits, max 0o777). When
  omitted (`None`) or 0 the default 0o644 applies; an explicit
  `mode=0` is treated the same as omitting the argument.

  <a id="sail.image.ImageDefinition.add_local_dir" />

  ### add\_local\_dir

  Bake a local directory into the image at remote\_path.

  ```python theme={null}
  def add_local_dir(
      local_path: Union[str, Path],
      remote_path: str,
      *,
      ignore: Optional[Union[Sequence[str], Path, str]] = None,
  ) -> ImageDefinition
  ```

  Each regular file under `local_path` is hashed and uploaded;
  per-file modes come from the local stat(). Symlinks are skipped.
  `ignore` accepts a sequence of gitignore patterns or a Path to a
  file containing them (e.g. `.dockerignore`); pass a list to use
  patterns directly. `remote_path` must be an absolute POSIX path.

  <a id="sail.image.ImageDefinition.env" />

  ### env

  Bake environment variables into the image.

  ```python theme={null}
  def env(env: Dict[str, str]) -> ImageDefinition
  ```

  <a id="sail.image.ImageDefinition.build" />

  ### build

  Build the image now and return a handle pinned to its image\_id.

  ```python theme={null}
  def build(*, timeout: int = 1800) -> ImageDefinition
  ```

  Submits the spec to Sail and polls until the build is ready or fails,
  raising `TimeoutError` if the build does not finish within
  `timeout` seconds.

  Sail may boot the image outside of any Sailbox -- once as the final
  stage of the build, and again periodically while the image is in
  active use -- to capture and refresh a start snapshot so Sailboxes
  created from it skip the cold boot. Boot-time initialization
  therefore runs at times you don't control, and anything it writes
  becomes part of the snapshot shared by every Sailbox created from
  this image -- generate per-instance identity (machine IDs, nonces,
  cached credentials) at runtime, not during boot. Per-Sailbox
  environment, networking, and credentials are injected at create
  time either way. See the "Hidden boots and start snapshots" section
  of the Sailbox images guide.

  <a id="sail.image.ImageNamespace" />

  ## ImageNamespace

  Base images a Sailbox can build on.

  Access these through the module-level `sail.Image` singleton, for example
  `sail.Image.debian_arm64` or `sail.Image.devbox_arm64`. See the
  [Images guide](https://docs.sailresearch.com/sailboxes-images) for how to
  choose between the Debian and devbox bases and the CPU architectures.

  <a id="sail.image.ImageNamespace.builtin_debian" />

  ### builtin\_debian

  Prebuilt Debian base, matching the Sailbox host architecture.

  ```python theme={null}
  builtin_debian: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.debian_amd64" />

  ### debian\_amd64

  Debian base for x86-64, pinned to your local Python version.

  ```python theme={null}
  debian_amd64: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.debian_amd" />

  ### debian\_amd

  Alias for `debian_amd64`.

  ```python theme={null}
  debian_amd: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.builtin_debian_amd64" />

  ### builtin\_debian\_amd64

  Debian base for x86-64, without Python-version pinning.

  ```python theme={null}
  builtin_debian_amd64: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.debian_arm64" />

  ### debian\_arm64

  Debian base for arm64, pinned to your local Python version.

  ```python theme={null}
  debian_arm64: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.debian_arm" />

  ### debian\_arm

  Alias for `debian_arm64`.

  ```python theme={null}
  debian_arm: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.builtin_debian_arm64" />

  ### builtin\_debian\_arm64

  Debian base for arm64, without Python-version pinning.

  ```python theme={null}
  builtin_debian_arm64: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.devbox_amd64" />

  ### devbox\_amd64

  Devbox base for x86-64: Debian plus a baked development toolchain.

  ```python theme={null}
  devbox_amd64: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.devbox_arm64" />

  ### devbox\_arm64

  Devbox base for arm64: Debian plus a baked development toolchain.

  ```python theme={null}
  devbox_arm64: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.builtin_devbox_amd64" />

  ### builtin\_devbox\_amd64

  Alias for `devbox_amd64`.

  ```python theme={null}
  builtin_devbox_amd64: ImageDefinition
  ```

  <a id="sail.image.ImageNamespace.builtin_devbox_arm64" />

  ### builtin\_devbox\_arm64

  Alias for `devbox_arm64`.

  ```python theme={null}
  builtin_devbox_arm64: ImageDefinition
  ```

  <a id="sail.exec_process.ExecProcess" />

  ## ExecProcess

  A command running in a Sailbox.

  Returned by `Sailbox.exec`. `stdout`/`stderr` iterate live output as
  it arrives, streamed into bounded buffers and resuming if the stream
  breaks; `wait()` returns the authoritative final result even when the
  stream cannot be resumed. The command runs inside the Sailbox independent
  of this handle and the connection that launched it, so closing the handle
  or losing the network leaves it running and its result retrievable through
  `wait()`.

  <a id="sail.exec_process.ExecProcess.exec_request_id" />

  ### exec\_request\_id

  Stable server-assigned identifier of this exec.

  ```python theme={null}
  exec_request_id: str
  ```

  <a id="sail.exec_process.ExecProcess.stdout" />

  ### stdout

  Live stdout iterator yielding `str` chunks (incrementally decoded
  UTF-8; use `stdout_bytes` for the raw byte stream). Ends when the
  stream ends; a reader that falls more than the buffer cap behind skips
  the dropped head. Each access returns a fresh iterator that replays the
  retained output from the beginning before following live.

  ```python theme={null}
  stdout: Iterator[str]
  ```

  <a id="sail.exec_process.ExecProcess.stderr" />

  ### stderr

  Live stderr iterator; same semantics as `stdout`. Empty for a pty
  exec, which merges stderr onto stdout.

  ```python theme={null}
  stderr: Iterator[str]
  ```

  <a id="sail.exec_process.ExecProcess.output" />

  ### output

  Live merged terminal output for a pty exec (alias of `stdout`).

  ```python theme={null}
  output: Iterator[str]
  ```

  <a id="sail.exec_process.ExecProcess.stdout_bytes" />

  ### stdout\_bytes

  Live stdout iterator yielding raw `bytes` chunks, exactly as the
  command wrote them (escape sequences and binary payloads included).

  ```python theme={null}
  stdout_bytes: Iterator[bytes]
  ```

  <a id="sail.exec_process.ExecProcess.stderr_bytes" />

  ### stderr\_bytes

  Raw `bytes` twin of `stderr`.

  ```python theme={null}
  stderr_bytes: Iterator[bytes]
  ```

  <a id="sail.exec_process.ExecProcess.output_bytes" />

  ### output\_bytes

  Raw `bytes` twin of `output` (alias of `stdout_bytes`).

  ```python theme={null}
  output_bytes: Iterator[bytes]
  ```

  <a id="sail.exec_process.ExecProcess.stdin" />

  ### stdin

  Stdin writer for an `open_stdin=True` exec; raises
  `sail.InvalidArgumentError` otherwise.

  ```python theme={null}
  stdin: StdinWriter
  ```

  <a id="sail.exec_process.ExecProcess.exit_code" />

  ### exit\_code

  Exit code if the live stream delivered it, else None.

  ```python theme={null}
  exit_code: Optional[int]
  ```

  None does not mean still-running: if the stream broke the exit code may
  not arrive here, so `wait()` is authoritative. An exec whose host was
  lost before it produced a real exit code raises `SailboxHostLostError`
  here, exactly as `wait()` does.

  <a id="sail.exec_process.ExecProcess.poll" />

  ### poll

  Alias of `exit_code`; never blocks.

  ```python theme={null}
  def poll() -> Optional[int]
  ```

  <a id="sail.exec_process.ExecProcess.cancel" />

  ### cancel

  Signal the guest command: SIGINT by default, SIGKILL if force=True.

  ```python theme={null}
  def cancel(*, force: bool = False) -> None
  ```

  Idempotent on the server. Transient failures are retried briefly,
  covering the window right after the command starts when the guest
  cannot accept signals for it yet.

  <a id="sail.exec_process.ExecProcess.resize" />

  ### resize

  Set the pty window (cols x rows) for a `pty=True` exec.

  ```python theme={null}
  def resize(cols: int, rows: int) -> None
  ```

  Advisory and best-effort: an unknown, finished, or not-yet-placed exec is
  a server no-op, and transient transport errors are swallowed since the
  next resize resends. A no-op for a non-pty exec.

  <a id="sail.exec_process.ExecProcess.resync" />

  ### resync

  Ask a `pty=True` exec to repaint its current screen.

  ```python theme={null}
  def resync() -> None
  ```

  A command runs at full speed and never waits for a slow reader, so if
  you fall far behind the oldest output is dropped. Call this after that
  happens to receive the current screen instead of a broken, partial one.
  Advisory and best-effort; a no-op for a non-pty exec.

  <a id="sail.exec_process.ExecProcess.close" />

  ### close

  Release the live stream without touching the remote command.

  ```python theme={null}
  def close() -> None
  ```

  <a id="sail.exec_process.ExecProcess.wait" />

  ### wait

  Wait for the exec to complete and return its buffered result.

  ```python theme={null}
  def wait(*, stop: Optional[threading.Event] = None) -> Optional[ExecResult]
  ```

  If the live stream delivers a clean exit it resolves immediately;
  otherwise it fetches the authoritative result from the server.
  `result.stdout`/`result.stderr` are always the full capped tail,
  independent of how much was consumed via live iteration.

  Ctrl-C sends SIGINT and resumes waiting (the guest's natural
  128+SIGINT=130 exit code flows back); a second Ctrl-C escalates to
  SIGKILL and re-raises so a wedged guest can't trap the caller.

  If `stop` is given and fires before the stream ends, `wait()` returns
  `None` and leaves the exec running, so a caller that no longer needs the
  result can return promptly. Without `stop` the result is non-None.

  If this exec opened a Voyages auto-span, `wait()` closes it so the span
  records this run's outcome.

  <a id="sail.exec_process.AsyncExecProcess" />

  ## AsyncExecProcess

  A command running in a Sailbox, with an async interface.

  Returned by `await Sailbox.exec.aio(...)`. `stdout` and `stderr` are
  async iterators over live output chunks (`async for chunk in
    proc.stdout`; chunks are arbitrary slices of the stream, not lines), and
  `await proc.wait()` returns the buffered result. Like the sync handle, the
  command runs detached inside the Sailbox, so dropping this handle leaves it
  running and its result retrievable through `wait()`.

  Cancelling the task awaiting `wait()` stops waiting but leaves the command
  running; call `await proc.cancel()` to signal the command itself.

  <a id="sail.exec_process.AsyncExecProcess.exec_request_id" />

  ### exec\_request\_id

  Stable server-assigned identifier of this exec.

  ```python theme={null}
  exec_request_id: str
  ```

  <a id="sail.exec_process.AsyncExecProcess.stdout" />

  ### stdout

  Live stdout async iterator yielding `str` chunks (incrementally
  decoded UTF-8; use `stdout_bytes` for raw bytes). Each access returns
  a fresh iterator that replays the retained output from the start, then
  follows live.

  ```python theme={null}
  stdout: AsyncIterator[str]
  ```

  <a id="sail.exec_process.AsyncExecProcess.stderr" />

  ### stderr

  Live stderr async iterator; same semantics as `stdout`. Empty for a
  pty exec, which merges stderr onto stdout.

  ```python theme={null}
  stderr: AsyncIterator[str]
  ```

  <a id="sail.exec_process.AsyncExecProcess.output" />

  ### output

  Live merged terminal output for a pty exec (alias of `stdout`).

  ```python theme={null}
  output: AsyncIterator[str]
  ```

  <a id="sail.exec_process.AsyncExecProcess.stdout_bytes" />

  ### stdout\_bytes

  Live stdout async iterator yielding raw `bytes` chunks, exactly as
  the command wrote them.

  ```python theme={null}
  stdout_bytes: AsyncIterator[bytes]
  ```

  <a id="sail.exec_process.AsyncExecProcess.stderr_bytes" />

  ### stderr\_bytes

  Raw `bytes` twin of `stderr`.

  ```python theme={null}
  stderr_bytes: AsyncIterator[bytes]
  ```

  <a id="sail.exec_process.AsyncExecProcess.output_bytes" />

  ### output\_bytes

  Raw `bytes` twin of `output` (alias of `stdout_bytes`).

  ```python theme={null}
  output_bytes: AsyncIterator[bytes]
  ```

  <a id="sail.exec_process.AsyncExecProcess.stdin" />

  ### stdin

  Stdin writer for an `open_stdin=True` exec; raises
  `sail.InvalidArgumentError` otherwise.

  ```python theme={null}
  stdin: AsyncStdinWriter
  ```

  <a id="sail.exec_process.AsyncExecProcess.exit_code" />

  ### exit\_code

  Exit code if the live stream delivered it, else None (see the sync
  handle's note).

  ```python theme={null}
  exit_code: Optional[int]
  ```

  <a id="sail.exec_process.AsyncExecProcess.poll" />

  ### poll

  Alias of `exit_code`; never blocks.

  ```python theme={null}
  def poll() -> Optional[int]
  ```

  <a id="sail.exec_process.AsyncExecProcess.cancel" />

  ### cancel

  Signal the guest command: SIGINT by default, SIGKILL if force=True.

  ```python theme={null}
  async def cancel(*, force: bool = False) -> None
  ```

  <a id="sail.exec_process.AsyncExecProcess.resize" />

  ### resize

  Set the pty window for a `pty=True` exec; a no-op otherwise.

  ```python theme={null}
  async def resize(cols: int, rows: int) -> None
  ```

  <a id="sail.exec_process.AsyncExecProcess.resync" />

  ### resync

  Ask a `pty=True` exec to repaint its current screen; a no-op
  otherwise. See the sync `ExecProcess.resync`.

  ```python theme={null}
  async def resync() -> None
  ```

  <a id="sail.exec_process.AsyncExecProcess.close" />

  ### close

  Release the live stream without touching the remote command.

  ```python theme={null}
  def close() -> None
  ```

  <a id="sail.exec_process.AsyncExecProcess.wait" />

  ### wait

  Wait for the exec to complete and return its buffered result.

  ```python theme={null}
  async def wait() -> ExecResult
  ```

  `result.stdout`/`result.stderr` are the full capped tail, independent
  of how much was consumed via live iteration. A repeat `wait()` returns
  the cached result. If this exec opened a Voyages auto-span, `wait()`
  closes it with the run's outcome.

  <a id="sail.exec_process.StdinWriter" />

  ## StdinWriter

  File-like write side of an exec's stdin, reached via `proc.stdin`.

  Writes block (with backoff) while the guest buffer is full, like a real pipe;
  a completed or stdin-closed exec surfaces as `BrokenPipeError`.

  <a id="sail.exec_process.StdinWriter.close" />

  ### close

  Send EOF; the guest closes the pipe once the backlog drains.

  ```python theme={null}
  def close() -> None
  ```

  <a id="sail.exec_process.AsyncStdinWriter" />

  ## AsyncStdinWriter

  Async write side of an exec's stdin, reached via `proc.stdin`.

  `await stdin.write(data)` applies backpressure (it resolves once the guest
  accepts the bytes); `await stdin.close()` sends EOF.

  <a id="sail.exec_process.AsyncStdinWriter.close" />

  ### close

  Send EOF; the guest closes the pipe once the backlog drains.

  ```python theme={null}
  async def close() -> None
  ```

  <a id="sail.function.function" />

  ## function

  Decorate a Python function so it can run through `Sailbox.exec`.

  ```python theme={null}
  def function(func: Optional[F] = None)
  ```

  <a id="sail.function.SailFunction" />

  ## SailFunction

  A Python function that can be executed inside a Sailbox.

  <a id="sail.sailbox.SailboxFs" />

  ## SailboxFs

  Filesystem operations on a Sailbox's guest, reached via `Sailbox.fs`.

  File I/O streams bytes to/from the guest; the directory helpers create,
  remove, and test paths. Paths are remote POSIX paths in the guest,
  accepted as `str` or `PurePosixPath`.

  <a id="sail.sailbox.SailboxFs.read" />

  ### read

  Read a regular file from the Sailbox as bytes.

  ```python theme={null}
  def read(path: GuestPath) -> bytes
  ```

  Loads the entire file into memory. For files larger than a few
  hundred MiB (model checkpoints, datasets) prefer
  `read_stream`, which yields chunks without buffering.

  <a id="sail.sailbox.SailboxFs.read_stream" />

  ### read\_stream

  Stream a regular file's contents from the Sailbox as chunks.

  ```python theme={null}
  def read_stream(path: GuestPath) -> FileStream
  ```

  The result is iterable both ways, so the same call serves sync and
  async code::

  for chunk in sb.fs.read\_stream(path): ...
  async for chunk in sb.fs.read\_stream(path): ...

  Chunks arrive at a fixed transfer size (currently 1 MiB). Iterate to
  completion so the underlying stream is released.

  <a id="sail.sailbox.SailboxFs.write_stream" />

  ### write\_stream

  Open a streaming write to a regular file in the Sailbox.

  ```python theme={null}
  def write_stream(
      path: GuestPath,
      *,
      create_parents: bool = True,
      mode: Optional[int] = None,
  ) -> FileWriter
  ```

  Returns a `FileWriter`: push chunks with `write` and
  confirm with `finish` (only `finish` commits the write). Best used
  as a context manager, which finishes on a clean exit and aborts on an
  exception::

  with sb.fs.write\_stream("/logs/run.log") as writer:
  for chunk in produce\_chunks():
  writer.write(chunk)

  The async form returns an `AsyncFileWriter` whose `write` /
  `finish` / `abort` are awaited::

  async with await sb.fs.write\_stream.aio("/logs/run.log") as writer:
  await writer.write(chunk)

  <a id="sail.sailbox.SailboxFs.write" />

  ### write

  Write data to a regular file in the Sailbox.

  ```python theme={null}
  def write(
      path: GuestPath,
      data: Union[str, bytes, bytearray, memoryview, IOBase],
      *,
      create_parents: bool = True,
      mode: Optional[int] = None,
  ) -> None
  ```

  Missing parent directories are created by default.

  <a id="sail.sailbox.SailboxFs.mkdir" />

  ### mkdir

  Create a directory and any missing parents (like `mkdir -p`); a
  no-op if it already exists.

  ```python theme={null}
  def mkdir(path: GuestPath) -> None
  ```

  <a id="sail.sailbox.SailboxFs.remove" />

  ### remove

  Remove a file or directory tree (like `rm -rf`); a no-op if it is
  already absent.

  ```python theme={null}
  def remove(path: GuestPath) -> None
  ```

  <a id="sail.sailbox.SailboxFs.exists" />

  ### exists

  Whether `path` exists in the guest. Follows symlinks (like
  `test -e`), so a dangling symlink reports `False` even though
  `ls` lists it.

  ```python theme={null}
  def exists(path: GuestPath) -> bool
  ```

  <a id="sail.sailbox.SailboxFs.ls" />

  ### ls

  List a directory's immediate entries as `DirEntry` records (no
  recursion). Runs GNU `find` in the guest, which the default Debian
  image ships. A missing path raises, as does a path that is not a
  directory and a listing too large for the exec output cap. An entry
  whose name is not valid UTF-8 fails the listing, since the path API
  cannot address it.

  ```python theme={null}
  def ls(path: GuestPath) -> List[DirEntry]
  ```

  <a id="sail.sailbox.FileWriter" />

  ## FileWriter

  A streaming write to a guest file.

  Push chunks with `write` and confirm the write with `finish`;
  only `finish` commits it. An unfinished writer aborts on `__exit__`
  (or explicit `abort`), so a stream that ends without `finish` is
  never committed as a completed write; the guest file state after an abort
  is unspecified. Usable as a context manager: a clean exit finishes, an
  exception aborts and propagates. `AsyncFileWriter`, returned by
  `write_stream.aio`, is the async form.

  <a id="sail.sailbox.FileWriter.write" />

  ### write

  Write bytes (or UTF-8 text); writes are chunked at the transport size.

  ```python theme={null}
  def write(data: Union[str, bytes, bytearray, memoryview]) -> None
  ```

  <a id="sail.sailbox.FileWriter.finish" />

  ### finish

  Confirm the write, creating an empty file when nothing was written.

  ```python theme={null}
  def finish() -> None
  ```

  <a id="sail.sailbox.FileWriter.abort" />

  ### abort

  Cancel the write so it is never committed. Idempotent; a no-op
  after `finish`.

  ```python theme={null}
  def abort() -> None
  ```

  <a id="sail.sailbox.AsyncFileWriter" />

  ## AsyncFileWriter

  A streaming write to a guest file, with an async interface.

  Returned by `write_stream.aio`; same commit semantics as
  `FileWriter` with `await`-able `write`, `finish`, and
  `abort`. Usable as an async context manager: a clean exit finishes,
  an exception aborts and propagates.

  <a id="sail.sailbox.AsyncFileWriter.write" />

  ### write

  Write bytes (or UTF-8 text); writes are chunked at the transport size.

  ```python theme={null}
  async def write(data: Union[str, bytes, bytearray, memoryview]) -> None
  ```

  <a id="sail.sailbox.AsyncFileWriter.finish" />

  ### finish

  Confirm the write, creating an empty file when nothing was written.

  ```python theme={null}
  async def finish() -> None
  ```

  <a id="sail.sailbox.AsyncFileWriter.abort" />

  ### abort

  Cancel the write so it is never committed. Idempotent; a no-op
  after `finish`.

  ```python theme={null}
  async def abort() -> None
  ```

  <a id="sail.sailbox.FileStream" />

  ## FileStream

  An iterable stream of file chunks that opens on first use.

  Opening resolves the box's live endpoint (waking it on demand). Deferring
  that to first iteration keeps `read_stream` cheap to call, and the async
  path runs it off the event loop so other tasks keep running. Iterate to
  completion, or call `close` (or use it as a context manager) to
  release the stream early.

  <a id="sail.sailbox.FileStream.close" />

  ### close

  Stop the stream and release its resources; safe to call twice.

  ```python theme={null}
  def close() -> None
  ```

  <a id="sail.sailbox.FileStream.aclose" />

  ### aclose

  Async twin of `close`, run off the event loop.

  ```python theme={null}
  async def aclose() -> None
  ```

  <a id="sail.volume.Volume" />

  ## Volume

  A managed NFS volume that can be mounted into one or more Sailboxes.

  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](https://join.slack.com/t/sailresearchcrew/shared_invite/zt-41pdcym9j-UU0Ey~A~r6n2H0DQVQsQHQ).

  **Attributes:**

  | Attribute    | Type                 | Description                                 |
  | ------------ | -------------------- | ------------------------------------------- |
  | `volume_id`  | `str`                | The volume id.                              |
  | `name`       | `str`                | The volume name.                            |
  | `backend`    | `str`                | Storage backend serving the volume.         |
  | `status`     | `str`                | Lifecycle status.                           |
  | `mount_path` | `Optional[Path]`     | Mount path inside the Sailbox, if reported. |
  | `created_at` | `Optional[datetime]` | Creation time, if reported.                 |
  | `updated_at` | `Optional[datetime]` | Last-update time, if reported.              |

  <a id="sail.volume.Volume.find" />

  ### find

  Get an org-scoped NFS volume by name, optionally creating it.

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

  <a id="sail.volume.Volume.list" />

  ### list

  List active NFS volumes for the current organization, newest first.

  ```python theme={null}
  @staticmethod
  def list(*, max_objects: Optional[int] = None) -> List["Volume"]
  ```

  <a id="sail.volume.Volume.delete" />

  ### delete

  Delete this NFS volume, returning the deleted handle.

  ```python theme={null}
  def delete(*, allow_missing: bool = False) -> Optional["Volume"]
  ```

  With `allow_missing=True` an already-deleted volume returns `None`
  instead of raising. To delete by name without a handle, use
  `delete_by_name`.

  <a id="sail.volume.Volume.delete_by_name" />

  ### delete\_by\_name

  Delete the NFS volume with the given name, returning the deleted
  handle.

  ```python theme={null}
  @staticmethod
  def delete_by_name(name: str, *, allow_missing: bool = False) -> Optional["Volume"]
  ```

  With `allow_missing=True` a name that does not resolve to a volume
  returns `None` instead of raising.

  <a id="sail.volume.Volume.from_mount" />

  ### from\_mount

  Load the volume handle for a path mounted into a Sailbox.

  ```python theme={null}
  @staticmethod
  def from_mount(path: str | os.PathLike[str]) -> Volume
  ```

  <a id="sail.sailbox.ingress_auth_headers" />

  ## ingress\_auth\_headers

  Headers that authenticate this Sailbox as an ingress allowlist source.

  ```python theme={null}
  def ingress_auth_headers() -> Dict[str, str]
  ```

  Use these when making HTTP requests from one Sailbox to another listener
  whose `allowlist` contains the caller's app name. The helper is only
  available inside a Sailbox.

  ## Voyages

  A Voyage records what an agent run did. The module-level calls below act on the Voyage attached to the current process, so most code never holds a `Voyage` object itself.

  <a id="sail.voyage.create" />

  ### create

  Create a new Voyage and make it the current Voyage.

  ```python theme={null}
  def create(
      name: str,
      *,
      version: Optional[int] = None,
      metadata: Optional[Dict[str, Any]] = None,
      sailbox_id: Optional[str] = None,
  ) -> Union[Voyage, NoopVoyage]
  ```

  The current Voyage is tracked per execution context with a process-wide
  fallback: concurrent tasks or threads that each create their own Voyage
  keep their own attribution, and a context that never created one (a raw
  worker thread, code after `asyncio.run` returns) resolves the process's
  most recently created Voyage.

  Always creates, even when `SAIL_VOYAGE_ID` is set in the environment;
  a child process joining its parent's Voyage uses `attach` instead.
  Without a Sail API key (`SAIL_API_KEY` or a `sail auth login`
  credential) this degrades to a `NoopVoyage` so
  instrumented code keeps running with telemetry disabled. Arguments are
  validated before that gate: a malformed call raises even when telemetry
  is off.

  <a id="sail.voyage.attach" />

  ### attach

  Attach to an existing Voyage and make it the current Voyage
  (context-scoped, like `create`).

  ```python theme={null}
  def attach(voyage_id: Optional[str] = None) -> Union[Voyage, NoopVoyage]
  ```

  `voyage_id` defaults to `SAIL_VOYAGE_ID`, the handoff a parent
  process sets so its children join the parent's Voyage. Raises
  `ValueError` when neither is provided and telemetry is enabled.
  Without a Sail API key (`SAIL_API_KEY` or a `sail auth login`
  credential) this degrades to a `NoopVoyage` so
  instrumented code keeps running with telemetry disabled. This includes a
  keyless child whose parent exported no handoff env. An explicitly malformed
  argument always raises; an absent ambient config in a telemetry-off
  environment is the no-op state.

  <a id="sail.voyage.run" />

  ### run

  Run one Voyage around a block. This is the recommended entry point.

  ```python theme={null}
  def run(
      name: str,
      *,
      version: Optional[int] = None,
      metadata: Optional[Dict[str, Any]] = None,
      sailbox_id: Optional[str] = None,
  ) -> _VoyageRunContext
  ```

  `with sail.voyage.run("code-review") as voyage:` creates the Voyage on
  enter (same arguments and semantics as `create`: always creates,
  never reads `SAIL_VOYAGE_ID`), emits `voyage.completed` on a clean exit,
  and on an exception emits `voyage.failed` with the exception's type and
  message, then re-raises. Use `async with sail.voyage.run(...)` for the
  async form, which drives the same lifecycle without blocking the event loop.
  Terminal delivery is a bounded best-effort flush; call `voyage.flush()`
  inside the block for strict delivery confirmation. Without a Sail API key
  the block runs with telemetry disabled, exactly like `create`.
  Controllers that create and complete the Voyage in different places keep
  using `create` / `attach` directly.

  <a id="sail.voyage.disable" />

  ### disable

  Disable Voyage telemetry (context-scoped, like `create`).

  ```python theme={null}
  def disable() -> NoopVoyage
  ```

  Like `create`, this also resets the process-wide fallback, so contexts
  that never started their own Voyage (a raw worker thread, code after
  `asyncio.run`) resolve the disabled state too.

  Installs and returns a `NoopVoyage` as the current Voyage. This is
  the public form of the telemetry-off state `create()`/`attach()` enter
  when no Sail API key is available. For controllers that catch a startup
  telemetry failure and choose to continue unobserved:
  `except VoyageError: voyage = sail.voyage.disable()`.

  <a id="sail.voyage.child_env" />

  ### child\_env

  Env vars a child process needs to `attach()` to the current Voyage.

  ```python theme={null}
  def child_env(*, agent: bool = True) -> Dict[str, str]
  ```

  Returns `{}` when there is no current Voyage or telemetry is disabled,
  so the handoff pattern is safe to use without an API key. See
  `Voyage.child_env`.

  <a id="sail.voyage.voyage_id" />

  ### voyage\_id

  The current Voyage's id, or `None` when no Voyage is current.

  ```python theme={null}
  def voyage_id() -> Optional[str]
  ```

  <a id="sail.voyage.headers" />

  ### headers

  Headers attributing a Sail API request to the current Voyage and to
  the span/agent context active at call time. Compute per request, never
  once at client construction.

  ```python theme={null}
  def headers(existing: Optional[Mapping[str, str]] = None) -> Dict[str, str]
  ```

  <a id="sail.voyage.wrap_openai" />

  ### wrap\_openai

  Attribute an OpenAI-style client's Sail calls to the live Voyage context.

  ```python theme={null}
  def wrap_openai(
      client: Any,
      *,
      voyage: Optional[Union[Voyage, NoopVoyage]] = None,
  ) -> Any
  ```

  Wraps the client's request methods in place (`responses.create`,
  `responses.retrieve`, and `chat.completions.create`, whichever
  exist) so every call computes the attribution headers AT CALL TIME
  (voyage id plus the span/agent active at that moment) and injects them
  via `extra_headers`. Snapshotting stale headers at client construction
  (`default_headers=sail.voyage.headers()`) becomes impossible: there is
  nothing to snapshot. Like the `sail.inference` wrappers, un-spanned
  `create` calls get a synthesized auto-span so the model call
  lands scoped; `retrieve` polls carry headers but never synthesize.

  `voyage=` pins attribution to one Voyage handle; the default follows
  the current Voyage per call. Async clients
  (`AsyncOpenAI`) are supported: coroutine-function methods get an async
  wrapper whose auto-span covers the awaited request, not just coroutine
  creation. Wrapping mutates the client in place (every holder of the
  object sees attribution), is idempotent, returns the client, and raises
  `TypeError` for an object exposing none of the known request methods.

  <a id="sail.voyage.event" />

  ### event

  Record one timestamped event on the current Voyage.

  ```python theme={null}
  def event(
      kind: str,
      level: str = "info",
      message: Optional[str] = None,
      payload: Optional[Dict[str, Any]] = None,
      *,
      span_id: Optional[str] = None,
      parent_span_id: Optional[str] = None,
      error_type: Optional[str] = None,
      occurred_at: Optional[str] = None,
      sequence_id: Optional[int] = None,
  ) -> None
  ```

  Everything after `payload` is keyword-only so a stale positional caller
  fails loudly rather than being silently reinterpreted.

  Agent attribution comes from the enclosing `agent()` context (or the
  `SAIL_AGENT_*` env defaults); there is no per-event override.

  <a id="sail.voyage.span" />

  ### span

  Open a named span on the current Voyage; context manager or decorator.

  ```python theme={null}
  def span(
      span_name: Optional[str] = None,
      *,
      message: Optional[str] = None,
      payload: Optional[Dict[str, Any]] = None,
      span_id: Optional[str] = None,
      parent_span_id: Optional[str] = None,
  ) -> _DeferredVoyageContext
  ```

  The current Voyage is resolved when the context is entered (or the
  decorated function is called), not when `span()` is evaluated. A
  module-level `@sail.span(...)` declared before `create()` attributes
  correctly. `span_name` may be omitted only in the decorator form,
  where it defaults to the function's `__qualname__`.

  <a id="sail.voyage.agent" />

  ### agent

  Declare the named agent on the current Voyage; context manager or
  decorator.

  ```python theme={null}
  def agent(
      name: str,
      *,
      role: Optional[str] = None,
      slug: Optional[str] = None,
  ) -> _DeferredVoyageContext
  ```

  The current Voyage is resolved at enter/call time, not at construction,
  so a module-level `@sail.agent(...)` declared before `create()`
  attributes correctly. Arguments are validated eagerly: a bad name or
  slug raises at the declaration site regardless of voyage state.

  <a id="sail.voyage.complete" />

  ### complete

  Mark the current Voyage completed. A no-op when no Voyage is active.

  ```python theme={null}
  def complete(
      message: Optional[str] = None,
      payload: Optional[Dict[str, Any]] = None,
  ) -> None
  ```

  <a id="sail.voyage.fail" />

  ### fail

  Mark the current Voyage failed. A no-op when no Voyage is active.

  ```python theme={null}
  def fail(
      error_type: str = "harness_error",
      message: Optional[str] = None,
      payload: Optional[Dict[str, Any]] = None,
  ) -> None
  ```

  <a id="sail.voyage.cancel" />

  ### cancel

  Mark the current Voyage canceled. A no-op when no Voyage is active.

  ```python theme={null}
  def cancel() -> None
  ```

  <a id="sail.voyage.flush" />

  ### flush

  Flush the current Voyage's buffered events. A no-op when none is active.

  ```python theme={null}
  def flush(timeout: Optional[float] = None) -> None
  ```

  <a id="sail.voyage.Voyage" />

  ### Voyage

  A Sail Voyage attached to the current process.

  <a id="sail.voyage.Voyage.headers" />

  #### headers

  Headers attributing a Sail API request to this Voyage and to the
  span/agent context active at call time.

  ```python theme={null}
  def headers(existing: Optional[Mapping[str, str]] = None) -> Dict[str, str]
  ```

  Compute per request, never once at client construction, so each
  call carries the span and agent actually active when it is made.
  Stale Voyage context headers in `existing` are replaced. Agent ids
  are slug-derived and therefore header-safe by construction; a
  non-header-safe caller-supplied span id is omitted rather than sent.

  <a id="sail.voyage.Voyage.child_env" />

  #### child\_env

  Env vars a child process needs to `attach()` to this Voyage.

  ```python theme={null}
  def child_env(*, agent: bool = True) -> Dict[str, str]
  ```

  Merge into the child's environment:
  `subprocess.run(cmd, env={**os.environ, **voyage.child_env()})`.
  With `agent=True` (default) the active `agent()` context rides
  along as the child's `SAIL_AGENT_*` defaults, so the child's
  events attribute to the same agent without re-declaring it.
  `NoopVoyage.child_env()` returns `{}`. The handoff is
  safe to call without an API key.

  <a id="sail.voyage.Voyage.event" />

  #### event

  Record one timestamped event.

  ```python theme={null}
  def event(
      kind: str,
      level: str = "info",
      message: Optional[str] = None,
      payload: Optional[Dict[str, Any]] = None,
      *,
      span_id: Optional[str] = None,
      parent_span_id: Optional[str] = None,
      error_type: Optional[str] = None,
      occurred_at: Optional[str] = None,
      sequence_id: Optional[int] = None,
  ) -> None
  ```

  Everything after `payload` is keyword-only so a stale positional
  caller fails loudly rather than having an argument silently
  reinterpreted as `span_id`.

  Agent attribution carries no per-event override: it comes
  from the enclosing `agent()` context, or from the `SAIL_AGENT_*`
  env defaults when no context is active. A one-shot attributed event
  is `with voyage.agent(...): voyage.event(...)`.

  <a id="sail.voyage.Voyage.span" />

  #### span

  Open a named span of work; nests under the active span automatically.

  ```python theme={null}
  def span(
      span_name: Optional[str] = None,
      *,
      message: Optional[str] = None,
      payload: Optional[Dict[str, Any]] = None,
      span_id: Optional[str] = None,
      parent_span_id: Optional[str] = None,
  ) -> _SpanContextManager
  ```

  Usable as a context manager or as a decorator. `span_name` may be
  omitted only in the decorator form (`@voyage.span()`), where it
  defaults to the decorated function's `__qualname__`; the `with`
  form requires a name.

  A span carries no agent identity of its own. Events emitted inside it
  (including the span's own lifecycle events) are attributed to the
  enclosing `agent()` context.

  <a id="sail.voyage.Voyage.agent" />

  #### agent

  Declare the named agent as the active participant.

  ```python theme={null}
  def agent(
      name: str,
      *,
      role: Optional[str] = None,
      slug: Optional[str] = None,
  ) -> _AgentContextManager
  ```

  `name` is the display identity shown in the dashboard; the stable
  attribution key (`agent_id`) is derived from it: lowercased,
  ASCII-folded, hyphenated. Pass `slug=` to pin the attribution key
  across display renames or multi-process attach; `role=` is an
  optional freeform taxonomy used for dashboard filtering.

  <a id="sail.voyage.Voyage.cancel" />

  #### cancel

  Mark this Voyage cancelled without emitting a customer event.

  ```python theme={null}
  def cancel() -> None
  ```

  Cancel stops recording for the Voyage; it does not terminate external
  agent code. `NoopVoyage.cancel()` and unattached Voyage instances are
  no-ops when no API key is configured.

  Unlike `complete()`/`fail()` (which never raise: there is a
  buffered terminal flush behind them), `cancel()` is a single
  synchronous control-plane request and **raises** `VoyageHTTPError` on
  a failed response. Wrap it if you call it from a `finally`/cleanup
  path where an exception would mask the original error.

  <a id="sail.voyage.NoopVoyage" />

  ### NoopVoyage

  No-op Voyage used when no Sail API key is available.

  Attribute-compatible with `Voyage` so fail-open code that reads voyage
  fields does not crash when telemetry is disabled.

  <a id="sail.tinker.SailTokenCompleter" />

  ## SailTokenCompleter

  Tinker TokenCompleter backed by Sail's raw-token Responses path.

  Extends `TokenCompleter`.

  <a id="sail._config.Config" />

  ## Config

  SDK configuration resolved from the environment and `~/.sail`.

  Sail resolves the API key and service endpoints, with environment
  variables taking precedence over the stored `~/.sail` credentials. Set
  `SAIL_API_KEY` (or run `sail auth login`) to authenticate.
  `SAIL_API_URL`, `SAILBOX_API_URL`, `SAIL_IMAGEBUILDER_URL`, and
  `SAILBOX_INGRESS_URL` override individual endpoints, for custom or
  self-hosted stacks. Configuration is resolved once per process; in a
  long-lived process, call `sail.reset_transports()` after changing these
  variables. `ingress_base` and `ingress_scheme` describe how a
  listener's public URL is built from the Sailbox id and port when the
  server does not return one: `"path"` addresses
  `<base>/_sailbox/{id}/{port}`, `"subdomain"` addresses
  `<sailbox>-<port>.<base host>`.

  <a id="sail._config.Config.from_env" />

  ### from\_env

  Load SDK config, raising `ValueError` when no API key is configured.

  ```python theme={null}
  @classmethod
  def from_env() -> Config
  ```

  <a id="sail._config.Config.from_env_optional_api_key" />

  ### from\_env\_optional\_api\_key

  Load SDK config without requiring an API key.

  ```python theme={null}
  @classmethod
  def from_env_optional_api_key() -> Config
  ```

  Like `from_env()` but does not raise when no key is configured, so
  endpoints still resolve for paths that do not need to authenticate (such
  as building a listener's public URL).

  <a id="sail._retry.RetryPolicy" />

  ## RetryPolicy

  Knobs governing the auto-retry path.

  `max_attempts` counts the initial request, so `max_attempts=3` means at
  most two retries. `base_delay` and `max_delay` are seconds; the schedule
  is exponential with full jitter, capped at `max_delay`, and a server-sent
  `Retry-After` wins (also capped at `max_delay`). Retried mutations
  carry an `Idempotency-Key` header, so a retry cannot double-apply.

  **Attributes:**

  | Attribute      | Type    | Description                                                         |
  | -------------- | ------- | ------------------------------------------------------------------- |
  | `max_attempts` | `int`   | Attempts including the initial request.                             |
  | `base_delay`   | `float` | First backoff delay, in seconds.                                    |
  | `max_delay`    | `float` | Backoff cap, in seconds; a server `Retry-After` is capped here too. |
  | `multiplier`   | `float` | Backoff growth factor between attempts.                             |

  ## Types

  Plain data types accepted by and returned from the calls above.

  <a id="sail.sailbox.IngressPort" />

  ### IngressPort

  A guest port to expose for ingress.

  `protocol` selects how the port is published:

  * `"http"` (the default) exposes the port as an HTTP service with a stable
    HTTPS URL.
  * `"tcp"` exposes the port as a byte-transparent raw-TCP service reachable
    at a stable host and port by any TCP client (for example a database client
    such as `psql -h <host> -p <port>`).

  `Sailbox.create(ingress_ports=...)` also accepts a bare `int` as
  shorthand for `IngressPort(port)` (i.e. an HTTP port).

  `allowlist` restricts which sources may connect to *this* port. Entries
  that parse as CIDR prefixes (e.g. `["203.0.113.0/24"]`) match source IPs;
  other entries are treated as Sail app names whose Sailboxes may connect.
  App-name entries are supported on `"http"` listeners only. Raw-TCP
  connections carry no source app identity, so `"tcp"` allowlists must
  be CIDR prefixes. Each port carries its own allowlist. An empty/omitted
  list means any source may connect.

  Exposing a well-known unauthenticated service port (e.g. a database) as raw
  TCP without an explicit `allowlist` is rejected. Use source restrictions,
  or pass `["0.0.0.0/0", "::/0"]` to explicitly allow every source.

  **Attributes:**

  | Attribute    | Type                  | Description                                                   |
  | ------------ | --------------------- | ------------------------------------------------------------- |
  | `guest_port` | `int`                 | The in-guest port to expose (1-65535).                        |
  | `protocol`   | `IngressProtocol`     | `"http"` (the default) or `"tcp"`.                            |
  | `allowlist`  | `Optional[List[str]]` | Source addresses allowed to reach the port; empty allows all. |

  <a id="sail.sailbox.HttpEndpoint" />

  ### HttpEndpoint

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

  **Attributes:**

  | Attribute | Type  | Description             |
  | --------- | ----- | ----------------------- |
  | `url`     | `str` | The routable HTTPS URL. |

  <a id="sail.sailbox.TcpEndpoint" />

  ### TcpEndpoint

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

  **Attributes:**

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

  <a id="sail.sailbox.Listener" />

  ### Listener

  An exposed guest port.

  `guest_port` is the guest port you exposed. `endpoint` is how you reach it: an
  `HttpEndpoint` for `"http"` listeners or a `TcpEndpoint` for `"tcp"`
  listeners. It is `None` until the listener is routable.

  **Attributes:**

  | Attribute      | Type                                         | Description                                             |
  | -------------- | -------------------------------------------- | ------------------------------------------------------- |
  | `guest_port`   | `int`                                        | The in-guest port traffic is forwarded to.              |
  | `protocol`     | `str`                                        | Wire protocol exposed (`"http"` or `"tcp"`).            |
  | `route_status` | `str`                                        | Status of the listener's ingress route.                 |
  | `endpoint`     | `Optional[Union[HttpEndpoint, TcpEndpoint]]` | How to reach the listener; `None` until it is routable. |

  <a id="sail.sailbox.SailboxPage" />

  ### SailboxPage

  One page of `Sailbox.list_page` results plus the server's pagination envelope.

  **Attributes:**

  | Attribute  | Type            | Description                                |
  | ---------- | --------------- | ------------------------------------------ |
  | `items`    | `List[Sailbox]` | The Sailboxes on this page.                |
  | `limit`    | `int`           | The page size that was applied.            |
  | `offset`   | `int`           | The offset that was applied.               |
  | `total`    | `int`           | Total matching Sailboxes across all pages. |
  | `has_more` | `bool`          | Whether more results exist past this page. |

  <a id="sail.sailbox.SailboxCheckpoint" />

  ### SailboxCheckpoint

  A durable checkpoint handle that can be used to start new Sailboxes.

  **Attributes:**

  | Attribute               | Type                 | Description                                                                                                                                                                                                |
  | ----------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `checkpoint_id`         | `str`                | The checkpoint id.                                                                                                                                                                                         |
  | `sailbox_id`            | `str`                | The Sailbox the checkpoint was taken from.                                                                                                                                                                 |
  | `checkpoint_generation` | `int`                | Checkpoint generation captured by this checkpoint.                                                                                                                                                         |
  | `expires_at`            | `Optional[datetime]` | RFC3339 UTC time at which the handle expires and is eligible for garbage collection, or `None` for a handle to an existing checkpoint that carries no fresh retention bound (a paused or sleeping source). |
  | `status`                | `str`                | The source Sailbox's status after checkpointing.                                                                                                                                                           |

  <a id="sail.sailbox.UpgradeResult" />

  ### UpgradeResult

  The outcome of a Sailbox runtime upgrade.

  **Attributes:**

  | Attribute | Type   | Description                                                                        |
  | --------- | ------ | ---------------------------------------------------------------------------------- |
  | `applied` | `bool` | True when applied immediately (running box); False when deferred to the next wake. |
  | `status`  | `str`  | Lifecycle status of the Sailbox after the upgrade call.                            |

  <a id="sail.sailbox.SailboxDeprecation" />

  ### SailboxDeprecation

  Actionable notice that a Sailbox's runtime should be upgraded.

  **Attributes:**

  | Attribute  | Type  | Description                                                                                                                           |
  | ---------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------- |
  | `deadline` | `str` | Date after which the Sailbox may be upgraded automatically on its next wake, stopping running processes and clearing in-memory state. |
  | `message`  | `str` | Upgrade guidance from the server.                                                                                                     |

  <a id="sail.sailbox.DirEntry" />

  ### DirEntry

  One entry in a directory listing from `SailboxFs.ls`.

  **Attributes:**

  | Attribute       | Type                                               | Description                                                                     |
  | --------------- | -------------------------------------------------- | ------------------------------------------------------------------------------- |
  | `name`          | `str`                                              | The entry's base name, with no directory prefix.                                |
  | `type`          | `Literal["file", "directory", "symlink", "other"]` | The entry's own kind. A symlink is `"symlink"` regardless of what it points at. |
  | `size`          | `int`                                              | Size in bytes as reported by the guest.                                         |
  | `modified_time` | `float`                                            | Last-modified time as a Unix timestamp in seconds (with a fractional part).     |
  | `mode`          | `int`                                              | Unix permission bits, e.g. `0o644`. The file-type bits are not included.        |

  <a id="sail.exec_process.ExecResult" />

  ### ExecResult

  The completed result of a Sailbox command: its buffered output, exit
  code, whether it was killed on timeout, and flags describing how complete
  the buffered output is.

  **Attributes:**

  | Attribute          | Type   | Description                                                                                                                                                                                |
  | ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
  | `stdout`           | `str`  | Buffered stdout: a capped, drop-oldest tail of the output.                                                                                                                                 |
  | `stderr`           | `str`  | Buffered stderr: a capped, drop-oldest tail of the output.                                                                                                                                 |
  | `exit_code`        | `int`  | The command's exit code.                                                                                                                                                                   |
  | `timed_out`        | `bool` | Whether the command was killed for exceeding its timeout.                                                                                                                                  |
  | `stdout_truncated` | `bool` | The server output ring overflowed and dropped the oldest bytes of the buffered copy returned here.                                                                                         |
  | `stderr_truncated` | `bool` | Like `stdout_truncated`, for stderr.                                                                                                                                                       |
  | `stdout_complete`  | `bool` | The live stream delivered the output through to the command's exit, so a consumer that streamed it already holds the complete output even when the buffered copy here is a truncated tail. |
  | `stderr_complete`  | `bool` | Like `stdout_complete`, for stderr.                                                                                                                                                        |

  <a id="sail.sailbox.GuestPath" />

  ### GuestPath

  A path inside the guest: a str or a `PurePosixPath`. Guest paths are
  remote POSIX paths, independent of the local platform.

  <a id="sail.sailbox.DEFAULT_LIST_LIMIT" />

  ### DEFAULT\_LIST\_LIMIT

  Default page size for `Sailbox.list` and `Sailbox.list_page`, sourced
  from the shared core.

  ```python theme={null}
  DEFAULT_LIST_LIMIT: int
  ```

  ## Errors

  Exceptions raised by this SDK surface. Every one of them extends `SailError`, so `except sail.SailError` catches them all. `SailDeprecationWarning` is the one entry below that is not an error: it is a warning the SDK emits through Python's `warnings` module.

  <a id="sail.errors.SailError" />

  ### SailError

  Base class for Sail SDK errors.

  Every operation failure the SDK raises derives from this class, and the
  classes that match a Python builtin also inherit it (for example
  `NotFoundError` is a `LookupError`), so `except sail.SailError` and
  builtin-based handlers both work. A few argument-type mistakes raise the
  plain builtin `TypeError`.

  **Attributes:**

  | Attribute     | Type            | Description                                                                                                                                                                  |
  | ------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `retryable`   | `bool`          | Whether retrying the same call may succeed. Advisory and conservative: `True` for transport failures and transient API statuses, `False` where the failure is deterministic. |
  | `status_code` | `Optional[int]` | HTTP status code, on API and creation failures; `None` elsewhere, so a catch-all handler can inspect it without narrowing first.                                             |
  | `rpc_status`  | `str`           | RPC status, on exec failures; empty elsewhere.                                                                                                                               |
  | `body`        | `Optional[Any]` | Parsed response body, on API and creation failures; `None` elsewhere.                                                                                                        |

  <a id="sail.errors.NotFoundError" />

  ### NotFoundError

  Raised when a resource (Sailbox, app, volume, or checkpoint) is not found.

  Extends `SailError`, `LookupError`.

  <a id="sail.errors.PermissionDeniedError" />

  ### PermissionDeniedError

  Raised for a missing or invalid API key, or insufficient scope.

  Extends `SailError`, `PermissionError`.

  <a id="sail.errors.InvalidArgumentError" />

  ### InvalidArgumentError

  Raised when an argument or the SDK configuration is rejected as invalid.

  Extends `SailError`, `ValueError`.

  <a id="sail.errors.InternalError" />

  ### InternalError

  Raised for an unexpected internal SDK failure.

  Extends `SailError`, `RuntimeError`.

  <a id="sail.errors.FileNotFoundError" />

  ### FileNotFoundError

  Raised when a guest file operation references a path that does not exist.

  Extends `SailError`, `builtins.FileNotFoundError`.

  <a id="sail.errors.BrokenPipeError" />

  ### BrokenPipeError

  Raised when a stdin write hits a command that already finished.

  Extends `SailError`, `builtins.BrokenPipeError`.

  <a id="sail.errors.TimeoutError" />

  ### TimeoutError

  Raised when a transport attempt exceeds its deadline.

  Extends `SailError`, `builtins.TimeoutError`.

  <a id="sail.errors.TransportError" />

  ### TransportError

  Raised when the transport cannot establish or maintain a connection.

  Extends `SailError`, `ConnectionError`.

  <a id="sail.errors.ApiError" />

  ### ApiError

  Raised for any other non-2xx API response.

  Extends `SailError`, `RuntimeError`.

  `RuntimeError` inheritance keeps generic retry-on-RuntimeError loops
  working; prefer branching on `retryable`.

  <a id="sail.errors.SailDeprecationWarning" />

  ### SailDeprecationWarning

  Warning that a Sail client or Sailbox runtime should be upgraded.

  Extends `UserWarning`.

  <a id="sail.errors.SailboxError" />

  ### SailboxError

  Base class for Sailbox-specific SDK errors.

  Extends `SailError`.

  <a id="sail.errors.SailboxCreationError" />

  ### SailboxCreationError

  Raised when Sailbox creation fails.

  Extends `SailboxError`.

  <a id="sail.errors.ImageBuildError" />

  ### ImageBuildError

  Raised when a custom image build fails.

  Extends `SailboxError`.

  <a id="sail.errors.SailboxExecutionError" />

  ### SailboxExecutionError

  Base class for Sailbox exec-related SDK errors.

  Extends `SailboxError`.

  <a id="sail.errors.SailboxTerminatedError" />

  ### SailboxTerminatedError

  Raised when the Sailbox no longer exists.

  Extends `SailboxExecutionError`.

  <a id="sail.errors.SailboxExecRequestNotFoundError" />

  ### SailboxExecRequestNotFoundError

  Raised when a wait references an unknown exec request.

  Extends `SailboxExecutionError`.

  <a id="sail.errors.SailboxHostLostError" />

  ### SailboxHostLostError

  Raised when the machine hosting your Sailbox failed before the command finished.

  Extends `SailboxExecutionError`.

  The command may have run only partially, and its output is gone. The run
  cannot be resumed. Calling `Sailbox.exec` again starts it over from the
  beginning, so any side effects the partial run applied will happen again.
  The Sailbox itself recovers automatically, so you do not need to resume it.

  <a id="sail.errors.SailboxFunctionError" />

  ### SailboxFunctionError

  Raised when a Python function fails while running in a Sailbox.

  Extends `SailboxExecutionError`.

  <a id="sail.errors.SailboxFunctionSerializationError" />

  ### SailboxFunctionSerializationError

  Raised when a Python function payload or result cannot be serialized.

  Extends `SailboxExecutionError`.

  <a id="sail.errors.CommandFailedError" />

  ### CommandFailedError

  Raised by `run(check=True)` when the command exits nonzero or times out.

  Extends `SailboxExecutionError`.

  Carries the completed `sail.ExecResult` as `result`.

  <a id="sail.errors.VoyageError" />

  ### VoyageError

  Base class for Voyage SDK errors.

  Extends `SailError`.

  <a id="sail.errors.VoyageHTTPError" />

  ### VoyageHTTPError

  Raised when the Voyage API returns an HTTP error.

  Extends `VoyageError`.

  <a id="sail.errors.VoyageNotFoundError" />

  ### VoyageNotFoundError

  Raised when a Voyage cannot be found for the current API key.

  Extends `VoyageHTTPError`.

  <a id="sail.errors.InferenceError" />

  ### InferenceError

  Base class for Sail inference wrapper errors.

  Extends `SailError`.

  <a id="sail.errors.InferenceHTTPError" />

  ### InferenceHTTPError

  Raised when a Sail inference endpoint returns an HTTP error.

  Extends `InferenceError`.
</div>
