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

> API reference for the Sailbox surface of the Sail Python SDK.

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

#### DEFAULT\_LIST\_LIMIT

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

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

## IngressPort

```python theme={null}
@dataclass(frozen=True)
class IngressPort
```

A guest port to expose for ingress.

`protocol` selects how the port is published:

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

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

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

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

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

#### guest\_port

The in-guest port to expose (1-65535).

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

#### protocol

`"http"` (the default) or `"tcp"`.

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

#### allowlist

Source addresses allowed to reach the port; empty allows all.

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

## ingress\_auth\_headers

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

Headers that authenticate this sailbox as an ingress allowlist source.

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.

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

## HttpEndpoint

```python theme={null}
@dataclass(frozen=True)
class HttpEndpoint
```

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

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

#### url

The routable HTTPS URL.

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

## TcpEndpoint

```python theme={null}
@dataclass(frozen=True)
class TcpEndpoint
```

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

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

#### host

Hostname to dial.

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

#### port

Port to dial.

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

## Listener

```python theme={null}
@dataclass(frozen=True)
class 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.

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

#### guest\_port

The in-guest port traffic is forwarded to.

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

#### protocol

Wire protocol exposed (`"http"` or `"tcp"`).

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

#### route\_status

Status of the listener's ingress route.

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

#### endpoint

How to reach the listener; `None` until it is routable.

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

## SailboxPage

```python theme={null}
@dataclass(frozen=True)
class SailboxPage
```

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

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

#### items

The sailboxes on this page.

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

#### limit

The page size that was applied.

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

#### offset

The offset that was applied.

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

#### total

Total matching sailboxes across all pages.

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

#### has\_more

Whether more results exist past this page.

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

## UpgradeResult

```python theme={null}
@dataclass(frozen=True)
class UpgradeResult
```

The outcome of an in-guest agent upgrade.

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

#### applied

True when applied immediately (running box); False when deferred to the
next wake.

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

#### status

Lifecycle status of the sailbox after the upgrade call.

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

## SailboxCheckpoint

```python theme={null}
@dataclass(frozen=True)
class SailboxCheckpoint
```

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

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

#### checkpoint\_id

The checkpoint id.

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

#### sailbox\_id

The sailbox the checkpoint was taken from.

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

#### checkpoint\_generation

Checkpoint generation captured by this checkpoint.

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

#### expires\_at

RFC3339 UTC time at which the handle expires and is eligible for garbage
collection, or `None` for a handle to an existing checkpoint that
carries no fresh retention bound (a paused or sleeping source).

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

#### status

The source sailbox's status after checkpointing.

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

## Sailbox

```python theme={null}
@dataclass(frozen=True)
class 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.
The remaining fields are the read-only snapshot as of `get`/`list`
(`status`, resource usage, image, timestamps); a handle born from
`create` carries only what the create response returns. Fetch a fresh
snapshot with `Sailbox.get(sailbox_id)`. Every operation resolves the
live endpoint on demand, waking a sleeping box only when the operation
needs it.

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

#### sailbox\_id

The sailbox id: the stable, durable external handle.

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

#### name

The sailbox name.

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

#### status

Lifecycle status (for example `"running"`).

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

#### app\_id

Identifier of the owning app.

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

#### app\_name

Name of the owning app.

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

#### image\_id

Identifier of the image the sailbox was created from.

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

#### memory\_mib

Configured memory, in MiB.

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

#### vcpu\_count

Configured number of vCPUs.

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

#### state\_disk\_size\_gib

Configured state-disk size, in GiB.

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

#### cpu\_requested\_vcpu

Requested CPU, in vCPUs.

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

#### cpu\_used\_vcpu

Current CPU usage, in vCPUs.

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

#### memory\_requested\_bytes

Requested memory, in bytes.

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

#### memory\_used\_bytes

Current memory usage, in bytes.

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

#### disk\_requested\_bytes

Requested disk, in bytes.

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

#### disk\_used\_bytes

Current disk usage, in bytes.

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

#### architecture

CPU architecture (for example `"arm64"`).

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

#### guest\_schema\_version

In-guest agent version the sailbox last booted (or was created) with.
`None` for handles born from `create` or for boxes created before
agent versions were recorded.

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

#### error\_message

Human-readable error detail when the box is in an error state.

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

#### checkpoint\_generation

Monotonic checkpoint generation counter.

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

#### started\_at

When the box last started, if it ever has.

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

#### last\_checkpointed\_at

When the most recent checkpoint was taken, if any.

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

#### created\_at

When the sailbox was created.

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

#### updated\_at

When the sailbox was last updated.

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

#### create

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

Create a new sailbox.

Custom image definitions are built first. Then this sends a synchronous
POST /v1/sailboxes that returns once the VM is running or creation has
failed.

`timeout` (seconds) bounds each create attempt, since creating a
sailbox can block for many minutes while it queues for
capacity and boots the VM. The create is retried (reattaching to the same
box), so it returns as soon as the box is ready and gives up after roughly
three attempts. If the budget is exhausted it raises rather than hanging;
the box may still come up server-side, and because the request was aborted
before returning an id you find or terminate it by `name`. Pass `0` to
leave each attempt unbounded.

`size` picks the vCPU count and the default memory/disk ceilings;
`memory_gib` and `disk_gib` tune those ceilings in whole GiB
within the size's range.

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

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

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

`size` selects the resource size: `"s"` or `"m"` (the default).
Each size sets the vCPU count plus default memory and disk.
Billing is based on observed usage, so a bigger size costs no more on
its own. Choose `"s"` when you want the fastest cold starts, forks,
and resumes. Its lower ceilings also cap what a runaway workload can
consume, so you don't accidentally use more than you need.
`autosleep_timeout_minutes` optionally lets Sail sleep the box after that
many minutes of continuous idleness. When set, Sail will sleep the box
after this many minutes with no active execs, no recent inbound network
ingress, and CPU usage less than `0.05`. Leave it as `None` to
disable autosleep. The value must be between 1 and 1440 minutes.

`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

```python theme={null}
def list(cls,
         *,
         app_id: Optional[str] = None,
         status: Optional[SailboxStatus] = None,
         search: Optional[str] = None,
         max_guest_schema_version: Optional[int] = None,
         limit: int = DEFAULT_LIST_LIMIT,
         offset: int = 0) -> List[Sailbox]
```

List sailboxes for the current org via public HTTP.

Filters are server-side. `app_id` filters by the owning app id (resolve
an app name through `App.find` first if needed).
`max_guest_schema_version` keeps only sailboxes whose in-guest agent
version is at or below the given version (sailboxes created before
agent versions were recorded always match), useful for finding
sailboxes that need `upgrade()`.

Returns just the sailboxes. Use `list_page` for the same call with the
pagination envelope (`limit`/`offset`/`total`/`has_more`).

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

#### list\_page

```python theme={null}
def list_page(cls,
              *,
              app_id: Optional[str] = None,
              status: Optional[SailboxStatus] = None,
              search: Optional[str] = None,
              max_guest_schema_version: Optional[int] = None,
              limit: int = DEFAULT_LIST_LIMIT,
              offset: int = 0) -> SailboxPage
```

List sailboxes and return them alongside the pagination envelope.

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

#### get

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

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

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

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

#### from\_checkpoint

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

Create a new running sailbox from a durable checkpoint handle.

This is a new box branched from the checkpoint's state, not a resumption
of the original. In-flight `Sailbox.exec()` sessions are reaped in the
child: a command still running when the checkpoint was taken does not
resume here. Its on-disk effects up to the checkpoint are preserved, but
the command itself is not continued. Start fresh execs on the new box.

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

#### terminate

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

Permanently terminate this sailbox.

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

#### pause

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

Checkpoint and pause this sailbox until it is explicitly resumed.

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

#### sleep

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

Checkpoint and sleep this sailbox until traffic or explicit resume wakes it.

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

#### checkpoint

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

Create a durable checkpoint handle for this sailbox.

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

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

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

#### upgrade

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

Upgrade this sailbox's in-guest agent to the latest version.

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

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

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

#### resume

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

Resume this sailbox through the public API.

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

#### listener

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

Fetch one listener by guest port without waking the box.

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

#### listeners

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

List this sailbox's listeners without waking it.

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

#### wait\_for\_listener

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

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

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

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

Expose an additional ingress port on this sailbox at runtime.

`protocol` is `"http"` (a routable URL, the default) or `"tcp"`
(a public `host`/`port` for raw TCP: ssh, Postgres, etc.).
`allowlist` restricts which source IP CIDRs (e.g. `["203.0.113.0/24"]`)
or Sail app names may connect (app names on `"http"` listeners only;
`"tcp"` allowlists must be CIDR prefixes). Re-exposing a port under the same protocol updates its
`allowlist` to the value you pass (otherwise keeping the same address:
a raw-TCP port reclaims its reserved one). 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 (its address stays reserved).
Returns the `Listener` (its `route_status` is
`"unspecified"`: the expose response does not report reachability).

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

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

#### unexpose

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

Stop serving an exposed ingress port on this sailbox.

A `"tcp"` port stops counting against your org's raw-TCP quota once
removed, but its public `host`/`port` stays reserved to this sailbox.
Re-`expose`-ing the same guest port reclaims the exact address, and no
other sailbox ever takes it (a sailbox that ran on an address may have an
ssh host key pinned to it, so the address is never reused). An `"http"`
port carries no such reservation and is removed outright. Removing a port
that is not exposed raises a `LookupError`.

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

#### ingress\_auth\_headers

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

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

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

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

Make this sailbox reachable over SSH, returning its endpoint.

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

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

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

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

#### read

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

Read a regular file from the sailbox as 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.Sailbox.read_stream" />

#### read\_stream

```python theme={null}
def read_stream(path: str) -> Any
```

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

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

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

Chunks are sized by the guest file service (currently 1 MiB per
response). Iterate to completion so the underlying stream is released.

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

#### write\_stream

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

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

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

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

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

#### write

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

Write data to a regular file in the sailbox.

Missing parent directories are created by default.

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

#### exec

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

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

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

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

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

`retry_timeout` budgets the transient-RPC retries while the backend
admits the command, covering the delay of waking or migrating a
sailbox.

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

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

#### shell

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

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

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

This is the streaming-exec equivalent of `ssh`-ing into the box,
without a separate SSH server: it rides the same channel as
`exec(pty=True)`. `shell` overrides the login shell (default
`$SHELL` or `/bin/bash`); it is ignored when `command` is given.

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

## FileWriter

```python theme={null}
class 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`), canceling the RPC so the server does not
treat the stream end as a completed write; the guest file state after an
abort is unspecified. Usable as a context manager (sync and async): a
clean exit finishes, an exception aborts and propagates.

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

#### write

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

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

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

#### write\_aio

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

Async variant of `write`.

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

#### finish

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

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

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

#### finish\_aio

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

Async variant of `finish`.

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

#### abort

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

Cancel the write so the server does not commit it. Idempotent; a
no-op after `finish`.

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

## FileStream

```python theme={null}
class FileStream
```

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

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

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

#### close

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

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

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

## App

```python theme={null}
@dataclass(frozen=True)
class App
```

A Sail application.

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

#### id

Stable server-assigned app identifier.

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

#### name

Human-readable app name unique within the owning org.

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

#### created\_at

App creation time.

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

#### find

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

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

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

#### list

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

Return every app the current org owns, newest first.

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

```python theme={null}
@dataclass(frozen=True)
class ImageDefinition
```

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

#### apt\_install

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

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

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

#### pip\_install

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

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

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

#### run\_commands

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

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

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

#### add\_local\_file

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

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

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

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

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

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

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

Bake environment variables into the image.

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

#### build

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

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

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.

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

## ImageNamespace

```python theme={null}
class ImageNamespace
```

Base images a sailbox can build on.

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

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

#### builtin\_debian

```python theme={null}
@property
def builtin_debian() -> ImageDefinition
```

Prebuilt Debian base, matching the Sailbox host architecture.

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

#### debian\_amd64

```python theme={null}
@property
def debian_amd64() -> ImageDefinition
```

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

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

#### debian\_amd

```python theme={null}
@property
def debian_amd() -> ImageDefinition
```

Alias for `debian_amd64`.

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

#### builtin\_debian\_amd64

```python theme={null}
@property
def builtin_debian_amd64() -> ImageDefinition
```

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

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

#### debian\_arm64

```python theme={null}
@property
def debian_arm64() -> ImageDefinition
```

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

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

#### debian\_arm

```python theme={null}
@property
def debian_arm() -> ImageDefinition
```

Alias for `debian_arm64`.

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

#### builtin\_debian\_arm64

```python theme={null}
@property
def builtin_debian_arm64() -> ImageDefinition
```

Debian base for arm64, without Python-version pinning.

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

#### devbox\_amd64

```python theme={null}
@property
def devbox_amd64() -> ImageDefinition
```

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

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

#### devbox\_arm64

```python theme={null}
@property
def devbox_arm64() -> ImageDefinition
```

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

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

#### builtin\_devbox\_amd64

```python theme={null}
@property
def builtin_devbox_amd64() -> ImageDefinition
```

Alias for `devbox_amd64`.

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

#### builtin\_devbox\_arm64

```python theme={null}
@property
def builtin_devbox_arm64() -> ImageDefinition
```

Alias for `devbox_arm64`.

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

## Volume

```python theme={null}
@dataclass(frozen=True)
class Volume
```

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

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

#### volume\_id

The volume id.

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

#### name

The volume name.

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

#### backend

Storage backend serving the volume.

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

#### status

Lifecycle status.

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

#### mount\_path

Mount path inside the sailbox, if reported.

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

#### created\_at

Creation time, if reported.

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

#### updated\_at

Last-update time, if reported.

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

#### find

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

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

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

#### list

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

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

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

#### delete

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

Delete this NFS volume.

This method also supports `sail.Volume.delete("name")` as a
convenience for deleting a named volume without first keeping the
returned handle.

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

#### from\_mount

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

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

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

## ExecResult

```python theme={null}
@dataclass(frozen=True)
class ExecResult
```

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

#### stdout

Buffered stdout: a capped, drop-oldest tail of the output.

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

#### stderr

Buffered stderr: a capped, drop-oldest tail of the output.

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

#### exit\_code

The command's exit code.

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

#### timed\_out

Whether the command was killed for exceeding its timeout.

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

#### stdout\_truncated

The server output ring overflowed and dropped the oldest bytes of the
buffered copy returned here.

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

#### stderr\_truncated

Like `stdout_truncated`, for stderr.

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

#### stdout\_complete

The live stream delivered the output through to the command's exit, so
a consumer that streamed it already holds the complete output even when
the buffered copy here is a truncated tail.

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

#### stderr\_complete

Like `stdout_complete`, for stderr.

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

## ExecProcess

```python theme={null}
class ExecProcess
```

A command running in a sailbox.

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

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

#### exec\_request\_id

```python theme={null}
@property
def exec_request_id() -> str
```

Stable server-assigned identifier of this exec.

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

#### stdout

```python theme={null}
@property
def stdout() -> Iterator[str]
```

Live stdout iterator. Ends when the stream ends; a reader that falls
more than the buffer cap behind skips the dropped head. Each access
returns a fresh iterator that replays the retained output from the
beginning before following live.

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

#### stderr

```python theme={null}
@property
def stderr() -> Iterator[str]
```

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

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

#### output

```python theme={null}
@property
def output() -> Iterator[str]
```

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

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

#### stdin

```python theme={null}
@property
def stdin() -> _StdinWriter
```

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

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

#### exit\_code

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

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

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

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

#### poll

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

Alias of `exit_code`; never blocks.

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

#### cancel

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

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

Idempotent on the server. By default does not retry transient errors;
`retry_timeout > 0` keeps retrying within that budget, covering the
brief window right after the command starts when the guest cannot
accept signals for it yet.

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

#### resize

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

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

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

#### close

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

Release the live stream without touching the remote command.

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

#### wait

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

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

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

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

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

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

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

## AsyncExecProcess

```python theme={null}
class AsyncExecProcess
```

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

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

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

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

#### exec\_request\_id

```python theme={null}
@property
def exec_request_id() -> str
```

Stable server-assigned identifier of this exec.

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

#### stdout

```python theme={null}
@property
def stdout() -> Any
```

Live stdout async iterator. Each access returns a fresh iterator that
replays the retained output from the start, then follows live.

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

#### stderr

```python theme={null}
@property
def stderr() -> Any
```

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

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

#### output

```python theme={null}
@property
def output() -> Any
```

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

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

#### stdin

```python theme={null}
@property
def stdin() -> _AsyncStdinWriter
```

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

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

#### exit\_code

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

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

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

#### poll

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

Alias of `exit_code`; never blocks.

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

#### cancel

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

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

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

#### resize

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

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

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

#### close

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

Release the live stream without touching the remote command.

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

#### wait

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

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

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

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

## SailFunction

```python theme={null}
@dataclass
class SailFunction
```

A Python function that can be executed inside a Sailbox.

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

## function

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

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

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

## SailError

```python theme={null}
class SailError(Exception)
```

Base class for Sail SDK errors.

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

## SailboxError

```python theme={null}
class SailboxError(SailError)
```

Base class for sailbox-specific SDK errors.

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

## SailboxCreationError

```python theme={null}
class SailboxCreationError(SailboxError)
```

Raised when sailbox creation fails.

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

## ImageBuildError

```python theme={null}
class ImageBuildError(SailboxError)
```

Raised when a custom image build fails.

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

## SailboxExecutionError

```python theme={null}
class SailboxExecutionError(SailboxError)
```

Base class for sailbox exec-related SDK errors.

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

## SailboxTerminatedError

```python theme={null}
class SailboxTerminatedError(SailboxExecutionError)
```

Raised when the sailbox no longer exists.

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

## SailboxExecRequestNotFoundError

```python theme={null}
class SailboxExecRequestNotFoundError(SailboxExecutionError)
```

Raised when a wait references an unknown exec request.

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

## SailboxWorkerLostError

```python theme={null}
class SailboxWorkerLostError(SailboxExecutionError)
```

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

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

```python theme={null}
class SailboxFunctionError(SailboxExecutionError)
```

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

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

## SailboxFunctionSerializationError

```python theme={null}
class SailboxFunctionSerializationError(SailboxExecutionError)
```

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

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

## Config

```python theme={null}
class Config
```

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

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

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

#### from\_env

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

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

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

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

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

Load SDK config without requiring an API key.

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

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

## RetryPolicy

```python theme={null}
@dataclass(frozen=True)
class RetryPolicy
```

Knobs governing the auto-retry path.

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

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

#### max\_attempts

Attempts including the initial request.

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

#### base\_delay

First backoff delay, in seconds.

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

#### max\_delay

Backoff cap, in seconds; a server `Retry-After` is capped here too.

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

#### multiplier

Backoff growth factor between attempts.
