> ## 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+. Sail also provides
[TypeScript](/reference/typescript-sdk) and [Rust](/reference/rust-sdk) SDKs.

## 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 addresses the
  Sailbox by this id. Sail wakes a sleeping Sailbox only when an 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.                                                                                                                                                                                                                                           |
  | `volume_mounts`          | `Optional[Tuple[SailboxVolumeMount, ...]]` | Volumes attached to this Sailbox and the paths they are mounted at. `None` for handles that carry no snapshot of them (for example ones born from `create`); fetch a fresh snapshot with `get`.                                                                               |
  | `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 Sailbox is in an error state.                                                                                                                                                                                                            |
  | `checkpoint_generation`  | `Optional[int]`                            | Monotonic checkpoint generation counter.                                                                                                                                                                                                                                      |
  | `started_at`             | `Optional[datetime]`                       | When the Sailbox first started running, if it ever has. A resume does not rewrite it.                                                                                                                                                                                         |
  | `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 Sailbox (for a 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.                                                                                                                                                                          |
  | `auto_sleep`             | `Optional[AutoSleep]`                      | When Sail may sleep this Sailbox on its own: from `get` or `list`, or your own last `set_auto_sleep` through this object; `None` otherwise. A Sailbox created by `from_checkpoint` inherits its source's preference, so read it back with `get` to learn the inherited value. |
  | `network_policy`         | `Optional[NetworkPolicyInfo]`              | The Sailbox's network policy, frozen at creation: from `get` or `list`. `None` means public (unrestricted outbound access). A Sailbox created by `from_checkpoint` keeps the policy of the one it came from, so read it back to verify the enforced value.                    |

  <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_limit_gib: Optional[int] = None,
      disk_limit_gib: Optional[int] = None,
      ingress_ports: Optional[Sequence[Union[int, IngressPort]]] = (),
      volumes: Optional[Mapping[str, Any]] = None,
      visibility: Literal["org", "private"] = "org",
      auto_sleep: Optional[AutoSleep] = None,
      network_policy: Union[
              NetworkPolicy, NetworkAllowlist, Literal["public", "no_network"]
          ] = NetworkPolicy.PUBLIC,
  ) -> Sailbox
  ```

  Custom image definitions are built first; the call then returns once
  the new Sailbox is running or creation has failed.

  `timeout` (seconds) bounds each attempt of the call, since creating
  a Sailbox can block for many minutes while it queues for capacity and
  boots the VM. A call that times out raises, and the Sailbox may still
  come up in the background; it then shows up in `list`. Pass
  `0` to wait without a bound.

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

  SSH is enabled after create with `enable_ssh`, which trusts
  your org's SSH certificate authority, starts `sshd`, and exposes
  guest port 22 as `tcp`.

  `visibility` chooses who may operate the Sailbox, fixed for its
  life. `"org"` (the default) lets any credential in your org exec,
  copy files, SSH, or run lifecycle operations on it. `"private"`
  restricts all of that to you. An org admin can override that with a
  recorded reason for exec, files, setting a wake time, and the pause,
  sleep, resume, terminate, and upgrade operations. SSH, exposing or
  removing listeners, checkpoint, and restore stay creator-only.
  `"private"` 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
  and resumes. Its lower ceilings also cap what a runaway workload
  can consume, so you don't accidentally use more than you need.
  `memory_limit_gib` and `disk_limit_gib` tune that size's default memory and
  disk ceilings in whole GiB, within its range. Omit them to keep the
  size's own ceilings.

  `image` is the image to boot; omit it for the prebuilt Debian base
  (no image 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 Sailbox; it wakes transparently on traffic
  or the next operation. `auto_sleep` turns that off or replaces the
  default idle window; see `AutoSleep`.

  `network_policy` sets how the Sailbox may reach the network, chosen
  here and fixed for its life: a `NetworkPolicy` (`public` or
  `no_network`), or a `NetworkAllowlist` to permit only a list
  of destinations. The default leaves network access open. A no-network
  Sailbox cannot serve inbound connections, so
  `NetworkPolicy.NO_NETWORK` cannot be combined with `ingress_ports`;
  an allowlist limits only connections the Sailbox opens, so ingress
  ports and SSH still work. A Sailbox created by `from_checkpoint`
  inherits its source's policy, so read it back with `get` to
  verify the inherited value.

  `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 Sailbox 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: str,
      timeout: Optional[int] = None,
  ) -> Sailbox
  ```

  The new Sailbox uses the checkpoint's writable disk and cleaned memory
  state, so background processes continue and the new Sailbox runs
  independently of the source. Commands started with `exec` stop,
  though their writes up to the checkpoint remain. Host-specific identity
  and network routes are removed before the checkpoint handle becomes
  ready. A Sailbox with volume mounts cannot create a reusable checkpoint.
  If Sail cannot resume the saved memory, it starts the child cold with
  its writable disk intact and without the saved processes. The new
  Sailbox keeps the original's network policy; read it back with
  `get`.

  `name` names the new Sailbox. `timeout` (seconds)
  bounds the call, since a restore can block for many minutes while the
  new Sailbox queues for capacity. A call that times out raises, and
  the restore may still finish in the background; the new Sailbox then
  shows up in `list`. `timeout` must be positive when given;
  omit it to wait without a bound.

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

  ### terminate

  Permanently terminate this Sailbox.

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

  Idempotent: terminating a Sailbox 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.set_auto_sleep" />

  ### set\_auto\_sleep

  Replace when Sail may sleep this Sailbox on its own.

  ```python theme={null}
  def set_auto_sleep(auto_sleep: AutoSleep) -> None
  ```

  Each call replaces the whole setting: switching to `AutoSleep.never`
  clears any minimum wait set earlier, and switching back does not
  restore it.

  <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
  reuse their existing checkpoint. The call returns after Sail has
  prepared the clean start state that new Sailboxes use. Sailboxes with
  volume mounts are not supported. Upgrade a Sailbox that uses an older
  guest payload before you create a checkpoint handle.

  `name` sets a display name for the handle. `ttl_seconds`, 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 the checkpoint expires; starting a Sailbox from it after that
  fails.

  <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 Sailbox. A running Sailbox reboots
  in place on its current disk: all filesystem state is preserved, but
  processes restart as they would after a machine reboot (data not yet
  written to disk is lost). Upgrading a paused or sleeping Sailbox does
  not wake it: the upgrade is recorded and applies automatically at the
  next wake.

  Returns an `UpgradeResult`: `applied` is `True` when nothing
  is left to apply, either because the Sailbox took the upgrade just now
  or because it was already current, and `False` when the upgrade is
  recorded for the next wake.

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

  ### resume

  Resume this paused or sleeping Sailbox, returning it to running.

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

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

  ### listener

  Fetch one listener by guest port without waking the Sailbox.

  ```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 sources may connect: an entry that reads
  as an address or a range (e.g. `["203.0.113.0/24"]`) matches source
  IPs, and every other entry is a Sail app name (app names on `"http"`
  listeners only; `"tcp"` allowlists must be addresses or ranges). An
  address must not carry an IPv6 zone, such as `fe80::1%eth0`, which
  names an interface on one machine rather than a source.
  Re-exposing a port under the same protocol sets its `allowlist` to
  what you pass, so pass the whole list every time; passing none clears
  the restriction and reopens the port. 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 Sailbox, so wait only once the Sailbox 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.set_http_policy" />

  ### set\_http\_policy

  Attach an HTTP policy to this Sailbox.

  ```python theme={null}
  def set_http_policy(policy: Union[HttpPolicy, HttpPolicySummary, str]) -> None
  ```

  This replaces any policy already attached to this Sailbox. Accepts a
  `sail.HttpPolicy`, a listing summary, or a policy id string.

  The policy applies to HTTPS connections this Sailbox opens after the
  call; connections already open keep the previous policy until they
  close.

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

  ### http\_policy

  The HTTP policy attached to this Sailbox, or `None` when no
  policy is attached.

  ```python theme={null}
  def http_policy() -> Optional[HttpPolicy]
  ```

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

  ### clear\_http\_policy

  Clear this Sailbox's attached HTTP policy.

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

  The change applies to HTTPS connections this Sailbox opens after the
  call; connections already open keep the previous policy until they
  close. The call also succeeds when no policy is attached.

  <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 and is the only way to enable
  SSH: create the Sailbox, then call this. Safe to re-run: the Sailbox'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 addresses or ranges may connect
  to port 22, replacing any existing restriction. Left empty, a first
  enable opens the port to any source, and a re-enable 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 Sailbox 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 Sailbox. 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,
      user: Optional[Union[str, int]] = None,
      check: bool = False,
      idempotency_key: Optional[str] = None,
      output_buffer_bytes: int = DEFAULT_OUTPUT_BUFFER_BYTES,
  ) -> 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`); `user` picks the
  guest user the command runs as (see `exec`). The result's stdout
  and stderr hold only the most recent `output_buffer_bytes` of each
  stream (1 MiB by default, up to 64 MiB), with `stdout_truncated` and
  `stderr_truncated` set when older output was dropped; the command
  never pauses for unread output. To get every byte, use `exec`
  and read the stream
  (see `ExecProcess`).

  `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. While the first call is still running, a
  second call with the same key takes over its output stream, and the
  earlier call's result may come back truncated. The UTF-8 value can be
  up to 256 KiB.

  <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: Union[bool, PtyConfig] = False,
      env: Optional[Mapping[str, str]] = None,
      user: Optional[Union[str, int]] = None,
      output_mode: Union[OutputMode, Literal["auto", "pipe", "tail"]] = "auto",
      output_buffer_bytes: int = DEFAULT_OUTPUT_BUFFER_BYTES,
      kwargs: Optional[Mapping[str, Any]] = None,
  ) -> Union[ExecProcess, Any]
  ```

  For shell commands, returns a `ExecProcess` immediately
  after Sail accepts the command. By default a stream you are reading
  pauses the command when you fall behind, so nothing is lost until a
  cancel or the exec `timeout` ends the pauses, and a
  stream you are not reading keeps only its most recent 1 MiB.
  Accessing `proc.stdout` or `proc.stderr` is what starts reading,
  so access it right after this call returns when you need every byte
  (see `ExecProcess`). `output_mode` changes that: `"pipe"` holds
  both streams until you read them, so a late reader still gets every
  byte, and `"tail"` never pauses the command for you (see
  `OutputMode`). `output_buffer_bytes` sets each stream's
  buffer size, from 64 KiB to 64 MiB; it is what `wait()` returns per
  stream and how far a reader can fall behind before the command pauses.
  `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 `IS_SANDBOX=1` sandbox
  marker) and the image environment. A few
  reserved variables that identify the Sailbox (such as `SAILBOX_ID`)
  cannot be overridden. For pty execs the local terminal environment
  (`COLORTERM`, `LANG`, `LC_*`, `TERM_PROGRAM`) is forwarded
  automatically for keys not set here.

  `cwd` runs the command in the directory you name (string commands
  only). Without it, commands start in the image's working directory, or
  `/` when the image does not set one.

  `user` runs the command as that guest user: a user name, a numeric
  uid, or either with a group appended after a colon (`"alice"`,
  `1000`, `"alice:staff"`, `"1000:100"`, the Docker `USER`
  syntax). A named user must exist in the Sailbox's `/etc/passwd`; a
  numeric uid need not. `HOME` (and `USER`/`LOGNAME` when a name
  resolves) default to the resolved account, with `env` entries still
  winning. When `user` is not given, commands run as the image's
  `USER` if the image sets one, root otherwise; pass `user="0:0"`
  to force root (`"root"` is a user name like any other, resolved
  through the Sailbox's `/etc/passwd`). Sailboxes created before user
  support shipped must call `upgrade` once first; until then such
  execs fail rather than run as root. The exact spelling `user="0:0"`
  needs no upgrade.

  `idempotency_key` deduplicates the launch, so a retry with the
  same key attaches to the same command instead of starting a new one.
  An exec has one live handle at a time: a second handle started with
  the same key takes over the stream, and the first stops receiving
  live output and resolves from a bounded recorded result. A first
  handle reconnecting after a dropped connection can race a handle
  that attached meanwhile, and either handle's result may come back
  incomplete; avoid overlapping same-key handles. The UTF-8 value can be
  up to 256 KiB.

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

  <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,
      user: Optional[Union[str, int]] = None,
      timeout: Optional[int] = None,
      env: Optional[Mapping[str, str]] = None,
      no_forward: 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 Sailbox, without a separate
  SSH server. `shell` overrides the login shell (default `$SHELL` or
  `/bin/bash`); it is ignored when `command` is given.

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

  While attached, several local conveniences are forwarded: the Sailbox's
  browser opens and localhost servers reach your machine, files dragged
  onto the terminal upload into the Sailbox 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 Sailbox's clipboard, and text
  copied inside the Sailbox comes back to yours. Other images upload a pasted
  image as a file and paste its path instead. Pass `no_forward=True` to
  turn all of it off, for example for an untrusted or automated session.

  <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 the built definition.

  ```python theme={null}
  def build(*, timeout: int = 1800, force_build: bool = False) -> 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. Any automatic retries are included in that
  timeout.

  Creating Sailboxes from the returned definition needs no further
  build. For an image imported with `Image.from_registry` through
  a tag, it is also pinned to the exact version the build resolved
  the tag to, even if the tag later moves upstream.

  By default, Sail may reuse an existing ready build for this
  definition. Pass `force_build=True` to build it again: new
  Sailboxes use the fresh image once it is ready, Sailboxes that
  already exist keep the filesystem they were created with, and a
  forced build that fails changes nothing. For an image imported
  through a registry tag, a forced build also asks the registry what
  the tag points at now and builds that version. The tag then means
  that version for your whole organization, while definitions built
  earlier keep their pinned version. A forced build of an image
  built with `Image.from_dockerfile` looks up the tags its
  `FROM` and `COPY --from` instructions name and moves those
  pins for your whole organization, while definitions built
  earlier keep the versions their build used. If
  forced builds overlap, the last-requested one
  that succeeds decides which image new Sailboxes use and, for a
  tag, what the tag means.

  <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.debian" />

  ### debian

  Debian base for the given CPU architecture (default amd64).

  ```python theme={null}
  def debian(
      architecture: Literal["amd64", "arm64"] = "amd64",
      *,
      install_python: bool = True,
  ) -> ImageDefinition
  ```

  By default the image gets a `python3` matching your local Python
  version, which is what lets `@sail.function` run local Python
  functions inside a Sailbox. Pass `install_python=False` to keep
  the base's stock `python3` instead; a base with no Python
  install and no other build steps is prebuilt, so creating a Sailbox
  from it needs no build.

  `Image.debian_amd64` and `Image.debian_arm64` are shorthand for
  `debian("amd64")` and `debian("arm64")`.

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

  ### debian\_amd64

  Debian base for x86-64; shorthand for `debian("amd64")`.

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

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

  ### debian\_arm64

  Debian base for arm64; shorthand for `debian("arm64")`.

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

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

  ### devbox

  Devbox base for the given CPU architecture (default amd64): Debian
  plus a baked development toolchain.

  ```python theme={null}
  def devbox(architecture: Literal["amd64", "arm64"] = "amd64") -> ImageDefinition
  ```

  Docker is included, and its daemon starts automatically when the
  Sailbox boots and keeps running across sleeps. The daemon can take
  a few seconds to accept commands right after boot. If it stops, it
  is not restarted automatically.

  The devbox base is prebuilt only: build steps and `env` are not
  supported on it, so start from `debian` to customize.
  `Image.devbox_amd64` and `Image.devbox_arm64` are shorthand for
  `devbox("amd64")` and `devbox("arm64")`.

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

  ### devbox\_amd64

  Devbox base for x86-64; shorthand for `devbox("amd64")`.

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

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

  ### devbox\_arm64

  Devbox base for arm64; shorthand for `devbox("arm64")`.

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

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

  ### from\_registry

  Your own image as the Sailbox root filesystem.

  ```python theme={null}
  def from_registry(
      ref: str,
      *,
      architecture: Optional[Literal["amd64", "arm64"]] = None,
  ) -> ImageDefinition
  ```

  Sail pulls the image and layers everything a Sailbox needs on top, so
  the result behaves like any other image: build steps, env, and
  `Sailbox.create` all work the same. The image keeps its own
  `python3`, which `pip_install` and `@sail.function` use. Unlike
  `debian`, an imported image never gets a Python matching your local
  interpreter: installing one would shadow the Python the image was
  built around. An image without Python still gets one from the
  packages Sail installs. If you use `@sail.function`, an optional
  feature of the Python SDK that runs local Python functions inside a
  Sailbox, the image's Python must match your local Python's
  major.minor version.

  Reference an image on a supported public registry (`docker.io`,
  `ghcr.io`, `public.ecr.aws`, or `quay.io`), written as you
  would for `docker pull`: `python:3.13` means
  `docker.io/library/python:3.13` and `acme/tool` means
  `docker.io/acme/tool`; name the registry for the others, as in
  `ghcr.io/acme/tool`. You can pass a tag, a `@sha256:...` digest,
  or just the name, which means the `latest` tag. The image must be
  Debian- or Ubuntu-based.

  Your Sailbox runs on the CPU architecture the image was built for. An
  image published for both amd64 and arm64 runs on amd64. Pass
  `architecture` to require one instead, and building fails if the
  image was not built for it.

  A tag is pinned for your organization once an image has been built
  from it: later builds keep using that image even after the tag
  moves upstream. Call `build(force_build=True)` to look the tag up
  again and build the version it points at now for your whole
  organization; see `build` for how the switch propagates. A digest
  names exactly one image, so it never moves.

  The image's environment variables, working directory, and `USER`
  become the defaults for commands you run with `Sailbox.exec` or
  `Sailbox.run`; per-call `env`, `cwd`, and `user` override them
  (pass `user="0:0"` to run as root on an image that sets `USER`).
  The image's `ENTRYPOINT` and `CMD` are not run: a Sailbox manages
  its own processes, and your commands say what to execute. Build steps
  you chain onto the image (such as `apt_install`) and SSH sessions
  still run as root.

  ```python theme={null}
  image = sail.Image.from_registry("python:3.13").apt_install("git")
  ```

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

  ### from\_dockerfile

  Build a Dockerfile into a Sailbox image.

  ```python theme={null}
  def from_dockerfile(
      dockerfile: Union[str, Path, None] = None,
      *,
      contents: Optional[str] = None,
      context_dir: Optional[Union[str, Path]] = None,
      build_args: Optional[Dict[str, str]] = None,
      ignore: Optional[Sequence[str]] = None,
      architecture: Optional[Literal["amd64", "arm64"]] = None,
  ) -> ImageDefinition
  ```

  Pass the path to a Dockerfile as the positional argument, or its
  literal text with `contents=`.

  `context_dir` is the build context that `COPY` and `ADD` read
  from. A `.dockerignore` file in the context is honored, and
  `ignore` patterns are applied on top of it. A file named after
  your Dockerfile, like `Dockerfile.dockerignore`, sitting next to
  it is used instead of the context's `.dockerignore`, as it is
  with Docker. The files the ignore rules keep are hashed and
  uploaded when `from_dockerfile` is called, and their modes,
  empty directories, and symbolic links are carried into the build.
  Edits made after the call do not reach the build; call
  `from_dockerfile` again to pick them up. Without
  `context_dir` the build runs with an empty context.

  Every image a `FROM` or `COPY --from` instruction names must
  live on a supported public registry (`docker.io`, `ghcr.io`,
  `public.ecr.aws`, or `quay.io`). A short name like
  `python:3.12` means `docker.io/library/python:3.12`. The
  Dockerfile must produce a Debian- or Ubuntu-based filesystem.
  Sail layers everything a Sailbox needs on top, so build steps,
  `env`, and `Sailbox.create` all work the same as any other
  image.

  A `# syntax=` line can declare `docker/dockerfile:1` or a
  release from 1.4 through 1.22.0. A file that declares anything
  else is rejected. The declared release does not change how the
  file is built.

  The image builds for amd64 unless `architecture` says otherwise.
  `build_args` provides values for the Dockerfile's `ARG`
  instructions. Names may not start with the reserved `BUILDKIT_`
  prefix, and Docker's proxy names (`HTTP_PROXY`, `HTTPS_PROXY`,
  `FTP_PROXY`, `NO_PROXY`, `ALL_PROXY`, in any letter case) are
  rejected; a step that needs a proxy can set one inside its `RUN`
  command.

  Multi-stage Dockerfiles work. A `RUN --mount` of type `cache`,
  `secret`, or `ssh` is rejected; `tmpfs` mounts work, and
  `bind` mounts work when they read from the build context or
  another build stage. Mount options must be literal text, and
  `ONBUILD` is not supported, in the Dockerfile or in an image a
  `FROM` names.

  Each image a `FROM` or `COPY --from` names is pinned to the
  version its tag pointed at the first time your organization used
  it, so rebuilding the same Dockerfile keeps using those versions
  even after a tag moves. To look every tag up again and build with
  the versions they point at now, pass `force_build=True` to
  `build`.

  The image's environment variables, working directory, and `USER`
  become the defaults for commands you run with `Sailbox.exec` or
  `Sailbox.run`; per-call `env`, `cwd`, and `user` override
  them (pass `user="0:0"` to run as root on an image that sets
  `USER`), and values set with `.env()` win over the image's. The
  image's `ENTRYPOINT` and `CMD` are not run: a Sailbox manages
  its own processes, and your commands say what to execute. Build
  steps you chain onto the image (such as `apt_install`) and SSH
  sessions still run as root. The image keeps its own `python3`; if
  you use `@sail.function`, that Python must match your local
  Python's major.minor version.

  ```python theme={null}
  from pathlib import Path

  env_dir = Path("./envs/task1")
  image = sail.Image.from_dockerfile(env_dir / "Dockerfile", context_dir=env_dir)
  ```

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

  ## ExecProcess

  A command running in a Sailbox.

  Returned by `Sailbox.exec`. Each stream has a buffer, 1 MiB by default
  (`output_buffer_bytes`), and `output_mode` says what happens when
  it fills. With the default `"auto"`: if you are not reading a stream,
  the command never pauses and the stream keeps only its most recent bytes;
  if you are reading a stream and fall behind, the command pauses when the
  buffer fills and resumes as you read, like a pipe. Reading a stream is
  how you get every byte, and it slows the command when you cannot keep
  up. `"pipe"` holds both streams from the start, so a reader that starts
  late still gets every byte; `"tail"` never pauses the command for you.
  See `sail.OutputMode`.

  With `"auto"`, start reading right after `exec()` returns to get
  every byte. You can read stdout without holding stderr, or the reverse;
  the stream you are not holding keeps its most recent bytes and never
  pauses the command when it fills. If you hold both, read them at the
  same time, each from its own thread. The exit code is available from
  `exit_code` once the streams end and from `wait()`.

  Accessing `proc.stdout` or `proc.stderr` (`stdout_bytes` /
  `stderr_bytes` for raw bytes) claims the stream and returns a
  generator. The stream is released when the generator ends, when you call
  `close()` on it, or when nothing references it any more (a `for`
  loop drops it when the loop ends, including by `break`); to stop early
  on purpose, keep the generator in a variable and call `close()`. Each
  stream can be claimed once; a second access raises
  `sail.InvalidArgumentError`.

  `wait()` returns each stream's buffer, its most recent output, with
  `stdout_truncated` / `stderr_truncated` set when older output was
  dropped. `close()`, or your process exiting, releases both streams; the
  command keeps running, and `wait()` raises
  `sail.InvalidArgumentError` after `close()` unless it already
  resolved a result. Sail may reattach
  after an interruption, but reattachment does not guarantee exact output
  replay. A pty command never pauses; `resync()` requests a fresh screen.

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

  ### exec\_request\_id

  The durable identifier of this exec: the launch's idempotency
  key as Sail recorded it (yours, or the one Sail generated when you
  did not supply one; read it here to learn the generated value).
  Reading it marks a generated identity as shareable: a second handle
  started with it takes over the stream, so from then on this handle
  no longer reclaims the stream after any interruption, a clean end or
  a dropped connection alike, and resolves from the recorded result
  instead. Reading back a key you supplied changes nothing.

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

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

  ### stdout

  Live stdout as a generator of `str` chunks (incrementally decoded
  UTF-8). Accessing this property claims the stream, so access it right
  after `exec()` returns when you need every byte; the generator
  releases the stream when it ends, when you call `close()` on it, or
  when nothing references it any more (see `ExecProcess`). Use
  `stdout_bytes` instead for raw bytes; a second access of either
  raises `sail.InvalidArgumentError`.

  ```python theme={null}
  stdout: Generator[str, None, None]
  ```

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

  ### stderr

  Live stderr generator; same as `stdout`, including that accessing
  it claims the stream. Empty for a pty exec, which merges stderr onto
  stdout.

  ```python theme={null}
  stderr: Generator[str, None, None]
  ```

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

  ### output

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

  ```python theme={null}
  output: Generator[str, None, None]
  ```

  `output` and `stdout` are the same one-consumer stream.

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

  ### stdout\_bytes

  Live stdout as a generator of raw `bytes` chunks, exactly as the
  command wrote them (escape sequences and binary payloads included). The
  same one-consumer stream as `stdout` and `output`: accessing it
  claims the stream the same way, and `close()` on the generator
  releases the stream early.

  ```python theme={null}
  stdout_bytes: Generator[bytes, None, None]
  ```

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

  ### stderr\_bytes

  Raw `bytes` twin of `stderr`. Choose either accessor; this stream
  can be claimed once, and `close()` on the generator releases it.

  ```python theme={null}
  stderr_bytes: Generator[bytes, None, None]
  ```

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

  ### output\_bytes

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

  ```python theme={null}
  output_bytes: Generator[bytes, None, None]
  ```

  <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 once the streams end, else None.

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

  Never blocks and never drops output. If the connection was lost
  for good mid-command, the streams end early with the outcome still
  unknown: this stays None and `wait()` fetches the result Sail
  recorded. A host-lost exec (the machine running the Sailbox was
  lost mid-command) has no real exit code: `wait()` always raises
  `SailboxHostLostError` for one, and this raises it when that
  loss is what ended the streams.

  <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

  Abandon the handle without killing the command.

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

  It releases both streams. The command keeps running and never pauses,
  and Sail keeps only the most recent output of each stream. Call
  `cancel()` instead if the command should stop. `wait()` raises
  `sail.InvalidArgumentError` after `close()` unless it
  already resolved a result.

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

  ### wait

  Wait for the exec to complete and return its result.

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

  `result.stdout` and `result.stderr` hold each stream's buffer, its
  most recent output (1 MiB by default), with `stdout_truncated` and
  `stderr_truncated` set when older output was dropped; to get every
  byte, read the stream (see `ExecProcess`). `wait()` itself
  never pauses the command and may run while a generator is still open.
  With `output_mode="pipe"`, a stream nobody reads pauses the command when
  its buffer fills, and `wait()` then waits for as long as the command
  stays paused. After `close()` it raises
  `sail.InvalidArgumentError` unless a result was already
  resolved; a repeat `wait()` returns the cached result.

  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(...)`. A chunk can be a partial
  line. Each stream has a buffer, 1 MiB by default
  (`output_buffer_bytes`), and `output_mode` says what happens when
  it fills. With the default `"auto"`: if you are not reading a stream,
  the command never pauses and the stream keeps only its most recent bytes;
  if you are reading a stream and fall behind, the command pauses when the
  buffer fills and resumes as you read, like a pipe. Reading a stream is
  how you get every byte, and it slows the command when you cannot keep
  up. `"pipe"` holds both streams from the start, so a reader that starts
  late still gets every byte; `"tail"` never pauses the command for you.
  See `sail.OutputMode`.

  With `"auto"`, start reading right after `exec()` returns, before
  awaiting anything else, to get every byte. You can read stdout without
  holding stderr, or the reverse; the stream you are not holding keeps its
  most recent bytes and never pauses the command when it fills. If you hold
  both, read them at the same time, each from its own task
  (`asyncio.gather`). The exit code is available from `exit_code` once
  the streams end and from `wait()`.

  Accessing `proc.stdout` or `proc.stderr` (`stdout_bytes` /
  `stderr_bytes` for raw bytes) claims the stream and returns an async
  generator. The stream is released when the generator ends, when you
  `await` its `aclose()`, or when nothing references it any more (an
  `async for` loop drops it when the loop ends, including by `break`);
  to stop early on purpose, keep the generator in a variable and `await`
  its `aclose()`. Each stream can be claimed once; a second access raises
  `sail.InvalidArgumentError`.

  `wait()` returns each stream's buffer, its most recent output, with
  `stdout_truncated` / `stderr_truncated` set when older output was
  dropped. `close()`, or your process exiting, releases both streams; the
  command keeps running, and `wait()` raises
  `sail.InvalidArgumentError` after `close()` unless it already
  resolved a result. Sail may reattach
  after an interruption, but reattachment does not guarantee exact output
  replay. A pty command never pauses; `resync()` requests a fresh screen.

  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

  The durable identifier of this exec: the launch's idempotency
  key as Sail recorded it (yours, or the one Sail generated when you
  did not supply one; read it here to learn the generated value).
  Reading it marks a generated identity as shareable: a second handle
  started with it takes over the stream, so from then on this handle
  no longer reclaims the stream after any interruption, a clean end or
  a dropped connection alike, and resolves from the recorded result
  instead. Reading back a key you supplied changes nothing.

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

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

  ### stdout

  Live stdout as an async generator of `str` chunks (incrementally
  decoded UTF-8). Accessing this property claims the stream, so access
  it right after `exec()` returns when you need every byte; the
  generator releases the stream when it ends, when you `await` its
  `aclose()`, or when nothing references it any more (see
  `AsyncExecProcess`). Use `stdout_bytes` instead for raw
  bytes; a second access of either raises
  `sail.InvalidArgumentError`.

  ```python theme={null}
  stdout: AsyncGenerator[str, None]
  ```

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

  ### stderr

  Live stderr async generator; same as `stdout`, including that
  accessing it claims the stream. Empty for a pty exec, which merges
  stderr onto stdout.

  ```python theme={null}
  stderr: AsyncGenerator[str, None]
  ```

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

  ### output

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

  ```python theme={null}
  output: AsyncGenerator[str, None]
  ```

  `output` and `stdout` are the same one-consumer stream.

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

  ### stdout\_bytes

  Live stdout as an async generator of raw `bytes` chunks, exactly
  as the command wrote them. The same one-consumer stream as `stdout`
  and `output`: accessing it claims the stream the same way; `await`
  the generator's `aclose()` to release the stream early.

  ```python theme={null}
  stdout_bytes: AsyncGenerator[bytes, None]
  ```

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

  ### stderr\_bytes

  Raw `bytes` twin of `stderr`. Choose either accessor; this stream
  can be claimed once, and `aclose()` on the generator releases it.

  ```python theme={null}
  stderr_bytes: AsyncGenerator[bytes, None]
  ```

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

  ### output\_bytes

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

  ```python theme={null}
  output_bytes: AsyncGenerator[bytes, None]
  ```

  <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 once the streams end, 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

  Abandon the handle without killing the command.

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

  It releases both streams. The command keeps running and never pauses,
  and Sail keeps only the most recent output of each stream. Call
  `cancel()` instead if the command should stop. `wait()` raises
  `sail.InvalidArgumentError` after `close()` unless it
  already resolved a result.

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

  ### wait

  Wait for the exec to complete and return its result.

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

  `result.stdout` and `result.stderr` hold each stream's buffer, its
  most recent output (1 MiB by default), with `stdout_truncated` and
  `stderr_truncated` set when older output was dropped; to get every
  byte, read the stream (see `AsyncExecProcess`). `wait()`
  itself never pauses the command and may run while an async generator
  is still open. With `output_mode="pipe"`, a stream nobody reads pauses the
  command when its buffer fills, and `wait()` then waits for as long as
  the command stays paused. After `close()` it raises
  `sail.InvalidArgumentError` unless a result was already
  resolved; 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, test, and transfer paths. Paths are remote POSIX paths in the
  guest, accepted as `str` or `PurePosixPath`.

  Writes give what they create to the image's `USER` by default (root
  when the image sets none), the same identity commands run as, so an
  uploaded file is usable by the code in the Sailbox. Reads and the
  directory helpers act as root by default, so they work on any path.

  Every operation except the reads and the directory download takes an
  optional `user` in Docker's `USER` syntax (`name`, `uid`,
  `name:group`, or `uid:gid`; `"0:0"` is always root). The
  directory helpers other than the transfers run their command as that
  user, with its permissions enforced. Writes and the directory upload
  keep running as root but give that user what they create, like
  `COPY --chown`. Reads and the download take no `user`: a `user`
  only decides which paths an operation may touch and who owns what it
  creates. A read creates nothing in the Sailbox, and a download reads
  any path as root, the way the reads do. A `user` other than
  `"0:0"` requires a Sailbox whose guest honors requested users; on
  older Sailboxes these calls fail until `Sailbox.upgrade` is
  called.

  <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:

  ```python theme={null}
  for chunk in sb.fs.read_stream(path): ...
  async for chunk in sb.fs.read_stream(path): ...
  ```

  Chunk sizes depend on network delivery and are bounded by the transfer
  path. 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: int = 0o644,
      user: Optional[Union[str, 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:

  ```python theme={null}
  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:

  ```python theme={null}
  async with await sb.fs.write_stream.aio("/logs/run.log") as writer:
      await writer.write(chunk)
  ```

  The file gets mode `0o644` unless `mode` says otherwise. A
  `user` names the owner for the written file and any parent
  directories the write creates, defaulting to the image's `USER`,
  else root; the write itself always runs as root.

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

  ### write

  Write data to a regular file in the Sailbox.

  ```python theme={null}
  def write(
      path: GuestPath,
      data: FileContents,
      *,
      create_parents: bool = True,
      mode: int = 0o644,
      user: Optional[Union[str, int]] = None,
  ) -> None
  ```

  Missing parent directories are created by default, and the file
  gets mode `0o644` unless `mode` says otherwise. A `user` names
  the owner for the written file and any parent directories the write
  creates, defaulting to the image's `USER`, else root; the write
  itself always runs as root. A file-like `data` is streamed from
  the source, so it can be larger than memory. See
  `write_files` to write several files in one call.

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

  ### write\_files

  Write several complete files in one call.

  ```python theme={null}
  def write_files(
      files: Mapping[GuestPathT, FileContents],
      *,
      create_parents: bool = True,
      mode: int = 0o644,
      user: Optional[Union[str, int]] = None,
  ) -> None
  ```

  `files` maps each absolute guest path to its contents: a string
  (UTF-8), bytes, or a file-like object that is read into memory first.
  Each file is its own request, up to eight at a time, and every file
  gets the same `create_parents`, `mode`, and `user` as
  `write`. A batch is not atomic across paths: the first failure
  stops the batch, files that already completed stay written, writes
  already in flight finish, and the error names the file that failed.
  A path may appear only once. Use `write_stream` to stream a
  large source.

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

  ### mkdir

  Create a directory and any missing parents (like `mkdir -p`); a
  no-op if it already exists. A `user` runs the mkdir as that user, so
  created directories are owned by it.

  ```python theme={null}
  def mkdir(path: GuestPath, *, user: Optional[Union[str, int]] = None) -> 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. A `user` runs the removal as that user, limiting it
  to what that user may delete.

  ```python theme={null}
  def remove(path: GuestPath, *, user: Optional[Union[str, int]] = None) -> 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. A `user` reports existence as observable by
  that user: a path the user lacks permission to reach also reports
  `False`.

  ```python theme={null}
  def exists(path: GuestPath, *, user: Optional[Union[str, int]] = None) -> 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. A `user` runs the listing as that user, so a
  directory it may not read raises a permission error.

  ```python theme={null}
  def ls(path: GuestPath, *, user: Optional[Union[str, int]] = None) -> List[DirEntry]
  ```

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

  ### upload\_dir

  Upload a local directory's contents into a directory on the Sailbox.

  ```python theme={null}
  def upload_dir(
      local_dir: Union[str, "os.PathLike[str]"],
      guest_dir: GuestPath,
      *,
      user: Optional[Union[str, int]] = None,
  ) -> None
  ```

  `local_dir`'s entries land inside `guest_dir`, which is created
  if needed. Entries the upload does not name are left in place; a
  same-named file is replaced. Uploaded files belong to the image's
  `USER`, the same identity commands run as, so the code in the
  Sailbox can use them. When the image sets no `USER`, or that user
  cannot be resolved in the Sailbox, they belong to root.
  `guest_dir` and any missing parents the upload creates get the
  same owner. Files keep their permission bits, except that the
  setuid, setgid, and sticky bits are cleared. A `user` (the same
  syntax the other operations take) gives the entries to that user
  instead, like `COPY --chown`; it must exist in the Sailbox, and
  like the other operations' `user` it requires a Sailbox whose
  guest honors requested users. The Sailbox's image must provide
  `tar` and `gzip`, which the transfer uses to ship the directory
  as one compressed archive; the default images do.

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

  ### download\_dir

  Download a directory's contents from the Sailbox into a local
  directory.

  ```python theme={null}
  def download_dir(
      guest_dir: GuestPath,
      local_dir: Union[str, "os.PathLike[str]"],
  ) -> None
  ```

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

  <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 the transfer can wake the Sailbox. 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
  ```

  ## HTTP policies

  See [Credential injection](/sailboxes-credentials) for setup, examples, and cleanup. The entries below list the available Python calls.

  <a id="sail.secret.Secret" />

  ### Secret

  A value an HTTP policy can insert into matching HTTPS requests.

  Secrets belong to your organization. Sail never returns a stored value.
  Get and list calls return only the secret's name and timestamps.

  **Attributes:**

  | Attribute    | Type       | Description                                         |
  | ------------ | ---------- | --------------------------------------------------- |
  | `name`       | `str`      | The secret's name, unique within your organization. |
  | `created_at` | `datetime` | When the secret was first set.                      |
  | `updated_at` | `datetime` | When the secret's value last changed.               |

  <a id="sail.secret.Secret.set" />

  #### set

  Set (create or update) the named secret's value. An HTTP policy
  inserts it with `${secrets.NAME}`.

  ```python theme={null}
  @staticmethod
  def set(name: str, value: str) -> Secret
  ```

  After this call succeeds, the next matching request from any Sailbox
  whose attached HTTP policy uses this secret gets the new value.

  Names start with a letter or number and use letters, numbers,
  underscores, and dashes (up to 128 characters). Values cannot be
  empty. They can be up to 64 KiB and cannot contain ASCII control
  characters such as tabs or line breaks.

  <a id="sail.secret.Secret.get" />

  #### get

  Fetch one secret's name and timestamps. The value is never returned.

  ```python theme={null}
  @staticmethod
  def get(name: str) -> Secret
  ```

  Raises `sail.NotFoundError` when no secret has that name.

  <a id="sail.secret.Secret.list" />

  #### list

  List your organization's secret names and timestamps, sorted by name.

  ```python theme={null}
  @staticmethod
  def list() -> List["Secret"]
  ```

  <a id="sail.secret.Secret.delete" />

  #### delete

  Delete this secret.

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

  A secret cannot be deleted while an HTTP policy refers to it; the
  call raises `sail.SecretInUseError` until every referencing
  policy is deleted. Policy summaries from `sail.HttpPolicy.list`
  include the secret names they use.

  To delete by name without a handle, use `delete_by_name`.

  <a id="sail.secret.Secret.delete_by_name" />

  #### delete\_by\_name

  Delete the named secret. Same contract as `delete`.

  ```python theme={null}
  @staticmethod
  def delete_by_name(name: str) -> None
  ```

  <a id="sail.http_policy.HttpPolicy" />

  ### HttpPolicy

  Rules that shape the HTTPS requests your Sailboxes send.

  A policy is a named document owned by your organization. Obtain one from
  `create` or `get`; do not construct it directly. The document
  cannot change after creation, but `rename` can change its name.

  **Attributes:**

  | Attribute    | Type                | Description                                                                          |
  | ------------ | ------------------- | ------------------------------------------------------------------------------------ |
  | `id`         | `str`               | The policy's stable identifier.                                                      |
  | `name`       | `str`               | The policy's name (the only mutable field).                                          |
  | `document`   | `Mapping[str, Any]` | The saved policy document: Sail's normalized form of the document given at creation. |
  | `created_at` | `datetime`          | When the policy was created.                                                         |
  | `updated_at` | `datetime`          | When the policy's name last changed.                                                 |

  <a id="sail.http_policy.HttpPolicy.create" />

  #### create

  Create a policy from `document`.

  ```python theme={null}
  @staticmethod
  def create(name: str, document: Mapping[str, Any]) -> HttpPolicy
  ```

  Every `${secrets.NAME}` in the document must name a secret that
  already exists. An invalid document raises
  `sail.InvalidArgumentError` identifying the field to fix. Sail
  saves a normalized form of the document (for example, host names are
  lowercased and defaults are filled in), so reading the policy back can
  return a different shape with the same behavior.

  Policy names must contain visible text, use at most 128 characters,
  and cannot contain tabs, line breaks, or other control characters.

  Sail does not retry this call. If the connection ends before the
  result arrives, list policies before trying again; a second call can
  create a second policy.

  <a id="sail.http_policy.HttpPolicy.get" />

  #### get

  Fetch one policy by id, document included.

  ```python theme={null}
  @staticmethod
  def get(policy_id: str) -> HttpPolicy
  ```

  Raises `sail.NotFoundError` when no policy has that id.

  <a id="sail.http_policy.HttpPolicy.list" />

  #### list

  List your organization's policies as summaries, without documents.

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

  `search` filters by id or name, case-insensitively, and `limit`
  caps the number returned. Fetch a policy's document with `get`.

  <a id="sail.http_policy.HttpPolicy.rename" />

  #### rename

  Rename the policy and return the updated policy object.

  ```python theme={null}
  def rename(name: str) -> HttpPolicy
  ```

  Names follow the same rules as `create`.
  The document cannot change; create a new policy to change behavior.

  <a id="sail.http_policy.HttpPolicy.delete" />

  #### delete

  Delete the policy.

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

  A policy still attached to a Sailbox cannot be deleted; the call
  raises `sail.HttpPolicyInUseError` until every Sailbox clears
  or replaces it.

  <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 request to Sail 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.tinker.TinkerSandbox" />

  ## TinkerSandbox

  Run a tinker-cookbook sandbox on a Sailbox.

  Implements the cookbook's sandbox interface, so recipes that take a
  sandbox (or a sandbox factory, via `tinker_sandbox_factory`) can
  execute their rollout commands in an isolated Sailbox instead of on the
  training machine. Each instance owns one Sailbox for its whole life,
  and `cleanup` terminates it; give each sandbox its own Sailbox, since
  two sandboxes sharing one would share its filesystem and processes and
  the first cleanup would terminate it for both. The factory creates a
  fresh Sailbox per sandbox; constructing directly is for supplying your
  own, such as one restored from a warmed checkpoint.

  `timeout_seconds` is the sandbox's lifetime budget: once it has
  elapsed, the next operation terminates the Sailbox and raises the
  cookbook's `SandboxTerminatedError`. `None` means no budget.

  <a id="sail.tinker.TinkerSandbox.sandbox_id" />

  ### sandbox\_id

  The backing Sailbox's id.

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

  <a id="sail.tinker.TinkerSandbox.run_command" />

  ### run\_command

  Run a shell command in the Sailbox and return its `SandboxResult`.

  ```python theme={null}
  async def run_command(
      command: str,
      workdir: Optional[str] = None,
      timeout: Optional[float] = 60,
      max_output_bytes: Optional[int] = None,
  ) -> Any
  ```

  `max_output_bytes` keeps only the first bytes of each output
  stream; without it the full output is returned, up to a large
  safety ceiling that keeps a runaway stream from exhausting the
  training process's memory. A
  command that outlives `timeout`
  (seconds) is killed and reported with `metrics["timed_out"]` set.
  Errors from the Sailbox surface as a result with exit code `-1`,
  except a terminated or lost Sailbox, which raises the cookbook's
  `SandboxTerminatedError`.

  <a id="sail.tinker.TinkerSandbox.read_file" />

  ### read\_file

  Read a file from the Sailbox into a `SandboxResult`'s stdout.

  ```python theme={null}
  async def read_file(
      path: str,
      max_bytes: Optional[int] = None,
      timeout: float = 60,
  ) -> Any
  ```

  `max_bytes` keeps the file's first bytes (without it the whole
  file, up to a large safety ceiling), and `timeout` (seconds)
  bounds the whole read. A missing or unreadable file is reported as a
  result with exit code `1` rather than raised, matching the
  cookbook's contract, and so is a read that runs out of time.

  <a id="sail.tinker.TinkerSandbox.write_file" />

  ### write\_file

  Write a file into the Sailbox, marked executable when asked.

  ```python theme={null}
  async def write_file(
      path: str,
      content: Union[str, bytes],
      executable: bool = False,
      timeout: float = 60,
  ) -> Any
  ```

  `timeout` (seconds) bounds the write; one that runs out of time is
  reported as a result with exit code `1`.

  <a id="sail.tinker.TinkerSandbox.send_heartbeat" />

  ### send\_heartbeat

  Check the sandbox's lifetime budget.

  ```python theme={null}
  async def send_heartbeat(timeout: float = 30) -> None
  ```

  A Sailbox stays alive without keep-alives, so the heartbeat sends
  nothing; it only enforces `timeout_seconds`, terminating the
  Sailbox and raising the cookbook's `SandboxTerminatedError` once
  the budget has elapsed. `timeout` is part of the cookbook's
  heartbeat signature and is unused here, since there is no request
  for it to bound.

  <a id="sail.tinker.TinkerSandbox.cleanup" />

  ### cleanup

  Terminate the backing Sailbox. Safe to call more than once
  (termination is idempotent for an already-gone Sailbox); a
  cancellation arriving mid-cleanup still lets the termination finish,
  within a bounded grace, before propagating, so the Sailbox does not
  stay running and billable.

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

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

  ## tinker\_sandbox\_factory

  Create a `TinkerSandbox` for a tinker-cookbook environment.

  ```python theme={null}
  async def tinker_sandbox_factory(
      env_dir: Union[str, Path],
      timeout_seconds: Optional[float] = None,
      *,
      app: Optional[str] = None,
      size: SailboxSize = "s",
      image_ref: Optional[str] = None,
      name_prefix: str = "tinker",
  ) -> TinkerSandbox
  ```

  Pass this function (or a `functools.partial` of it, to preset the
  keyword arguments) wherever the cookbook accepts a sandbox factory; being
  a module-level function, it pickles by reference, so it survives the
  cookbook's process boundaries.

  `image_ref` takes precedence when given. Otherwise, the image comes
  from `[environment].docker_image` in the `task.toml` next to
  `env_dir`, or from `env_dir / "Dockerfile"` with `env_dir` as its
  build context. Docker-style short references are accepted
  (`python:3.11`). The registry image or Dockerfile must produce a
  Debian- or Ubuntu-based filesystem. The Sailbox is created in the
  `app` app (default `$SAIL_APP` or `"tinker"`, created on first
  use) and `timeout_seconds` becomes the sandbox's lifetime budget.

  <a id="sail.harbor.SailboxEnvironment" />

  ## SailboxEnvironment

  A Harbor environment whose commands run in a Sailbox.

  Extends `ComposeServiceOpsMixin`, `BaseEnvironment`.

  `start` creates the Sailbox from the task's image: a declared
  `docker_image` is pulled from its registry (Docker-style short
  references are accepted), and a task that ships an
  `environment/Dockerfile` instead has it built into a Sailbox image.
  Task and persistent environment variables apply to every command, and
  a prebuilt-image task's `environment/` directory is uploaded into its
  working directory, the same way Harbor's other cloud providers do.

  A task that ships an `environment/docker-compose.yaml` runs as a
  Docker Compose project inside the Sailbox: the services' images are
  pulled or built there, commands run in the task's `main` service, and
  per-service operations (exec, download, stop) reach the other services.

  `stop` puts the Sailbox to sleep, so a kept environment stops billing
  and resumes with its filesystem intact on the next `start`; deleting
  the environment terminates the Sailbox. In Compose mode the whole
  project sleeps, wakes, and terminates with the Sailbox.

  GPUs, TPUs, Windows, IPv6 allowlist entries, and a no-network or
  allowlist policy for a Compose task (whose bring-up must pull images
  over the network) are declared unsupported, so Harbor rejects a task
  that needs them up front rather than running it degraded. A plain
  task's no-network or allowlist policy is honored, and a task with
  default public networking is unaffected. Host mount specs are accepted
  and unused outside Compose mode, as Harbor permits for cloud providers
  that do not bind-mount; in Compose mode they are bound into the `main`
  service from the Sailbox's own filesystem.

  <a id="sail.harbor.SailboxEnvironment.start" />

  ### start

  Create the environment's Sailbox, building its image if needed.

  ```python theme={null}
  async def start(force_build: bool) -> None
  ```

  Starting an environment that was stopped without deletion resumes
  its sleeping Sailbox, filesystem intact, instead of creating a new
  one.

  <a id="sail.harbor.SailboxEnvironment.exec" />

  ### exec

  Run a shell command in the environment and return its result.

  ```python theme={null}
  async def exec(
      command: str,
      cwd: Optional[str] = None,
      env: Optional[Mapping[str, str]] = None,
      timeout_sec: Optional[float] = None,
      user: Optional[Union[str, int]] = None,
  ) -> Any
  ```

  In Compose mode the command runs inside the `main` service;
  otherwise it runs in the Sailbox itself.

  <a id="sail.harbor.SailboxEnvironment.upload_file" />

  ### upload\_file

  Copy a local file in, keeping its permission bits.

  ```python theme={null}
  async def upload_file(source_path: Union[str, Path], target_path: str) -> None
  ```

  <a id="sail.harbor.SailboxEnvironment.download_file" />

  ### download\_file

  Copy a file out to the local filesystem.

  ```python theme={null}
  async def download_file(source_path: str, target_path: Union[str, Path]) -> None
  ```

  <a id="sail.harbor.SailboxEnvironment.upload_dir" />

  ### upload\_dir

  Copy a local directory's contents in.

  ```python theme={null}
  async def upload_dir(source_dir: Union[str, Path], target_dir: str) -> None
  ```

  <a id="sail.harbor.SailboxEnvironment.download_dir" />

  ### download\_dir

  Copy a directory's contents out to a local directory.

  ```python theme={null}
  async def download_dir(source_dir: str, target_dir: Union[str, Path]) -> None
  ```

  <a id="sail.harbor.SailboxEnvironment.stop" />

  ### stop

  Stop the environment: put its Sailbox to sleep, or terminate it.

  ```python theme={null}
  async def stop(delete: bool = False) -> None
  ```

  A sleeping Sailbox stops billing and keeps its state, so starting
  the same environment again resumes it; deletion is permanent. In
  Compose mode the project's containers sleep and wake with the
  Sailbox, and termination takes the whole project with it.

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

  ## 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. An entry
  that reads as an address or a range (e.g. `["203.0.113.0/24"]`) matches
  source IPs; every other entry is a Sail app name whose Sailboxes may
  connect. An app name cannot read as an address or a range, and cannot
  contain a `/`. An address must not carry an IPv6 zone, such as
  `fe80::1%eth0`, which names an interface on one machine rather than a
  source.
  App-name entries are supported on `"http"` listeners only. Raw-TCP
  connections carry no source app identity, so `"tcp"` allowlists must
  be addresses or ranges. 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]]` | Sources allowed to reach the port: an address or a range, or a Sail app name on an `"http"` listener. 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]` | When the checkpoint expires: seven days out unless `ttl_seconds` asked for a different window. Starting a Sailbox from it after that fails. |
  | `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 no upgrade is left to apply, either because the Sailbox took one just now or because it was already current. False when the upgrade is recorded and takes effect the next time the Sailbox wakes. |
  | `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.SailboxVolumeMount" />

  ### SailboxVolumeMount

  One volume attached to a Sailbox and where it is mounted.

  **Attributes:**

  | Attribute    | Type  | Description                                                   |
  | ------------ | ----- | ------------------------------------------------------------- |
  | `volume_id`  | `str` | Identifier of the mounted volume.                             |
  | `mount_path` | `str` | Absolute path inside the Sailbox where the volume is mounted. |

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

  ### AutoSleep

  When Sail may put a Sailbox to sleep on its own.

  Sail sleeps Sailboxes that are doing nothing, freeing their memory and
  waking them the moment anything needs them again. Waking takes a
  couple of seconds: free for a batch job, unwelcome if someone is waiting
  at a terminal.

  Build one with `default`, `not_before`, or `never`.
  An explicit idle window replaces Sail's default and can make automatic
  sleep happen sooner or later. The window only controls when Sail may
  consider sleeping the Sailbox. Sail still sleeps it only when it sits fully
  idle: no busy process, no imminent timer, nothing a sleep would interrupt.

  Calling `Sailbox.sleep` yourself is unaffected, and so are
  `Sailbox.pause`, `Sailbox.resume`, and scheduled wakes.

  **Attributes:**

  | Attribute                  | Type            | Description                                                                                                                                        |
  | -------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `automatic`                | `bool`          | False stops Sail sleeping the Sailbox on its own.                                                                                                  |
  | `min_seconds_before_sleep` | `Optional[int]` | Use this idle window instead of Sail's default. Once it passes, Sail may sleep the Sailbox only when it is fully idle. `None` uses Sail's default. |

  <a id="sail.sailbox.AutoSleep.default" />

  #### default

  Let Sail decide, on its own timing. The default.

  ```python theme={null}
  @classmethod
  def default() -> AutoSleep
  ```

  <a id="sail.sailbox.AutoSleep.not_before" />

  #### not\_before

  Let Sail decide, but not before this much idle time.

  ```python theme={null}
  @classmethod
  def not_before(seconds: int) -> AutoSleep
  ```

  This idle window replaces Sail's default. Whole-second values from 1
  through 3600 are accepted. A value of 0 is the same as
  `default`, and is stored and read back that way. Other numeric
  values are rejected; use `never` instead.

  <a id="sail.sailbox.AutoSleep.never" />

  #### never

  Stop Sail sleeping a Sailbox on its own.

  ```python theme={null}
  @classmethod
  def never() -> AutoSleep
  ```

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

  ### NetworkPolicy

  How a Sailbox may reach the network, chosen at creation and fixed for
  its life.

  Extends `str`, `Enum`.

  `PUBLIC` leaves network access open and is the default. `NO_NETWORK`
  cuts the Sailbox off from other hosts and the internet: it cannot make
  outbound connections or expose inbound services, and name resolution
  does not work. Running commands is unaffected (`exec` and the shell
  reach the Sailbox over a Sail-internal path, not its network), and
  mounted volumes and other platform features it was created with keep
  working. To allow only some destinations, pass a
  `NetworkAllowlist` as the policy instead.

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

  ### NetworkAllowlist

  Restrict the destinations a Sailbox can reach, chosen when it is created
  and fixed for its whole life.

  Each entry is a hostname, a `*.` wildcard hostname (one extra name part),
  an IPv4 address, or an IPv4 range in CIDR form such as `203.0.113.0/24`.
  Give at least one entry and at most 128; a list that breaks the entry rules
  is rejected before the Sailbox is created. Only connections the Sailbox
  opens are limited, so `ingress_ports` and SSH still work. The
  [network policy guide](https://docs.sailresearch.com/sailboxes-network-policy)
  has the entry rules and what each entry allows.

  Pass the hosts as a list:

  ```python theme={null}
  sail.NetworkAllowlist(["api.example.com", "*.internal.example.com", "203.0.113.0/24"])
  ```

  **Attributes:**

  | Attribute       | Type            | Description                                           |
  | --------------- | --------------- | ----------------------------------------------------- |
  | `allowed_hosts` | `Sequence[str]` | The destinations the Sailbox may reach; at least one. |

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

  ### NetworkPolicyInfo

  A Sailbox's network policy as reported by `Sailbox.get` and
  `Sailbox.list`.

  `mode` is the policy mode as a string (`"no_network"` or
  `"allowlist"`) rather than a `NetworkPolicy`, so a mode this
  version of the SDK does not know is still reported instead of reading
  back as public. Because `NetworkPolicy` is a string enum,
  `mode == NetworkPolicy.NO_NETWORK` still holds for the modes this
  version knows. `allowed_hosts` carries the destinations in allowlist
  mode.

  **Attributes:**

  | Attribute       | Type              | Description                                                      |
  | --------------- | ----------------- | ---------------------------------------------------------------- |
  | `mode`          | `str`             | The policy mode, for example `"no_network"`.                     |
  | `allowed_hosts` | `Tuple[str, ...]` | Allowlist destinations when the mode uses them; empty otherwise. |

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

  ### OutputMode

  What happens when a stream's output buffer fills. Each stream has its
  own buffer, 1 MiB by default (`output_buffer_bytes` on
  `Sailbox.exec`). Accepted as the enum or its string value.

  Extends `str`, `Enum`.

  Sending `cancel()`, and the exec `timeout`, end every pause: from then
  on each stream keeps only its most recent bytes, so a reader more than a
  buffer behind skips. A command that ignores the cancel signal keeps
  running that way; `cancel(force=True)` stops it. The command keeps its
  original timeout. If this handle attaches to a command launched earlier
  under the same `idempotency_key`, this handle's pause deadline starts
  when the attachment succeeds, so it can release the pauses one full
  timeout after that; `cancel()` and `close()` release them at once.

  **Attributes:**

  | Attribute | Type | Description                                                                                                                                                                                                                                                                                                                      |
  | --------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `AUTO`    |      | A stream you are reading pauses the command when its buffer fills and resumes as you read, like a pipe. A stream you are not reading never pauses the command and keeps only its most recent bytes. The default.                                                                                                                 |
  | `PIPE`    |      | Both streams pause the command when their buffer fills, until you read them, so nothing is lost while you are late to start reading. Read both streams, or the command stays paused on the one you ignore. Once a reader is released, its stream goes back to keeping only its most recent bytes. Not available with `pty=True`. |
  | `TAIL`    |      | The command never pauses for you. Each stream keeps only its most recent bytes, even while you are reading it, so a reader that falls behind skips output without notice; `stdout_truncated` and `stderr_truncated` say only that the result holds less than the command wrote. A pty command always behaves this way.           |

  <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. It holds the most recent
  output of each stream (up to the exec's `output_buffer_bytes`, 1 MiB by
  default), the exit code, timeout status, and truncation flags.

  **Attributes:**

  | Attribute          | Type   | Description                                                                                                                                                    |
  | ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `stdout`           | `str`  | The most recent standard output, up to the exec's buffer size (1 MiB by default). Older bytes are dropped when the command wrote more; see `stdout_truncated`. |
  | `stderr`           | `str`  | The most recent standard error, up to the exec's buffer size (1 MiB by default; see `stderr_truncated`).                                                       |
  | `exit_code`        | `int`  | The command's exit code.                                                                                                                                       |
  | `timed_out`        | `bool` | Whether the command was killed for exceeding its timeout.                                                                                                      |
  | `stdout_truncated` | `bool` | The command wrote more stdout than `stdout` holds, so older bytes are missing.                                                                                 |
  | `stderr_truncated` | `bool` | Like `stdout_truncated`, for stderr.                                                                                                                           |

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

  ### PtyConfig

  The pseudo-terminal a `pty` exec runs under. Every field has a
  default, so `PtyConfig()` (or `pty=True`) is a usable terminal.

  **Attributes:**

  | Attribute | Type            | Description                                                      |
  | --------- | --------------- | ---------------------------------------------------------------- |
  | `term`    | `Optional[str]` | `$TERM` for the pty; `None` takes the default, `xterm-256color`. |
  | `cols`    | `int`           | Initial width in columns; `0` takes the default, 80.             |
  | `rows`    | `int`           | Initial height in rows; `0` takes the default, 24.               |

  <a id="sail.http_policy.HttpPolicySummary" />

  ### HttpPolicySummary

  A policy as returned by `HttpPolicy.list`, with usage counts but
  without the document. Fetch the full policy with `HttpPolicy.get`.

  **Attributes:**

  | Attribute                 | Type              | Description                                             |
  | ------------------------- | ----------------- | ------------------------------------------------------- |
  | `id`                      | `str`             | The policy's stable identifier.                         |
  | `name`                    | `str`             | The policy's name.                                      |
  | `host_count`              | `int`             | How many hosts the document covers.                     |
  | `rule_count`              | `int`             | How many rules the document carries across every host.  |
  | `referenced_secret_names` | `Tuple[str, ...]` | The secret names the document refers to.                |
  | `attachment_count`        | `int`             | How many Sailboxes the policy is currently attached to. |
  | `created_at`              | `datetime`        | When the policy was created.                            |
  | `updated_at`              | `datetime`        | When the policy's name last changed.                    |

  <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.FileContents" />

  ### FileContents

  The contents of one file passed to `write` or `write_files`: a str
  (written as UTF-8), a bytes-like object, or a readable file-like object
  that is read to its end first.

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

  ### DEFAULT\_LIST\_LIMIT

  Default page size for `Sailbox.list` and `Sailbox.list_page`.

  ```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`           | Transport status on exec failures; empty elsewhere. The attribute name stays `rpc_status` for compatibility.                                                                 |
  | `body`        | `Optional[Any]` | Parsed response body, on API and creation failures; `None` elsewhere.                                                                                                        |

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

  ### NotFoundError

  Raised when a requested Sailbox, app, volume, checkpoint, secret, or
  HTTP policy 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.SecretInUseError" />

  ### SecretInUseError

  Raised when deleting a secret that HTTP policies still refer to.

  Extends `ApiError`.

  Delete those policies first, then delete the secret.

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

  ### HttpPolicyInUseError

  Raised when deleting an HTTP policy that is still attached to a Sailbox.

  Extends `ApiError`.

  Clear or replace the policy on every Sailbox first, then delete it.

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