Metadata-Version: 2.4
Name: sail-sdk
Version: 0.1.8
Summary: Python SDK for the Sail sandbox platform
Project-URL: Homepage, https://app.sailresearch.com
Project-URL: Documentation, https://github.com/sailresearch/sail/tree/main/sdk/python#readme
Project-URL: Repository, https://github.com/sailresearch/sail
Project-URL: Issues, https://github.com/sailresearch/sail/issues
Author: Sail
License-Expression: Apache-2.0
Keywords: grpc,sail,sailbox,sandbox,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: grpcio>=1.80.0
Requires-Dist: protobuf>=6.31.1
Provides-Extra: dev
Requires-Dist: grpcio-tools>=1.80.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# sail-sdk

Python SDK for Sail sailboxes.

Sailboxes are persistent Linux sandboxes managed by Sail. Use the `sail-sdk`
Python package to create a VM, run shell commands, expose ports, make tracked
network requests, checkpoint state, pause, resume, and terminate it.

Sailboxes are currently in beta. APIs and operational behavior may change as we
stabilize the product.

## Install

```bash
pip install sail-sdk
```

```bash
uv add sail-sdk
```

Set your API key before using the SDK:

```bash
export SAIL_API_KEY=sk_...
```

By default, the SDK connects to production. For internal dev or staging
environments, set `SAIL_MODE=dev` or `SAIL_MODE=staging`. For an isolated
local tunnel created by `just redeploy-sailbox-dev`, set `SAIL_MODE=local`.

## Create a sailbox

```python
import sail

app = sail.App.find(name="example-app", mint_if_missing=True)

sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64,
    name="sandbox-1",
    cpu=1,
    memory_mib=1024,
    disk_gib=8,
)

print(sb.sailbox_id)
print(sb.status)
```

`Sailbox.create()` returns after the VM is running. The main create arguments
are:

| Argument | Default | Description |
| --- | --- | --- |
| `app` | Required | A `sail.App`, usually from `sail.App.find()` |
| `image` | Required | A `sail.Image` value or a custom image returned by `build()` |
| `name` | Required | Human-readable sailbox name |
| `cpu` | `1` | vCPU count; must be greater than `0` |
| `memory_mib` | `1024` | Memory in MiB; must be between `1024` and `65536` |
| `disk_gib` | `8` | Writable state disk in GiB; must be between `1` and `64` |
| `ingress_ports` | `None` | Guest ports to expose publicly |

Sailboxes should use `sail.Image.debian_arm64` or `sail.Image.debian_arm`. We
have plans to support AMD64 images soon - please contact us if you would like
us to prioritise this.

## Run commands

`exec()` starts a shell command and returns a `SailboxExecRequest`. Call `wait()`
to retrieve stdout, stderr, and the return code.

```python
result = sb.exec("echo hi", timeout=5).wait()

print(result.stdout)
print(result.stderr)
print(result.returncode)
```

`timeout` is the command runtime budget in seconds. Omit it to run without an
SDK-provided runtime limit. Only one exec request can be active on a sailbox at
a time; if another command is running, the SDK raises
`sail.SailboxExecAlreadyRunningError`.

With `background=True`, Sail launches the process through a detached shell and
`wait()` only waits for that launcher to succeed.

## Expose ports

Pass `ingress_ports` when creating the sailbox, then start a service inside the
VM and fetch its listener URL.

```python
sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64,
    name="web-demo",
    ingress_ports=[3000],
)

sb.exec("python3 -m http.server 3000", background=True).wait()

listener = sb.listener(3000)
listener.wait(timeout=60)

print(listener.url)
print(listener.route_status)
```

Use `listeners()` to list every exposed port:

```python
for listener in sb.listeners():
    print(listener.port, listener.url, listener.route_status)
```

Ports must be unique and between `1` and `65535`. Ports `22` and `10000` are
reserved by the platform.

## Tracked network requests

Use `request()` to ask the worker proxy to make an HTTP request and track its
result. `idempotency_key` is highly recommended so that retries do not send
duplicate outbound requests.

```python
req = sb.request(
    "POST",
    "https://example.com/api",
    headers={"X-Source": "sailbox"},
    json={"hello": "world"},
    idempotency_key="example-request-1",
)

completed = req.wait(timeout=30)
print(completed.status)

if completed.response:
    print(completed.response.status_code)
    print(completed.response.headers)
    print(completed.response.text)
    print(completed.response.json())
```

`params`, `headers`, `data`, `json`, `timeout`, and `durability` are supported.
`data` and `json` are mutually exclusive. A `SailboxRequest` can be refreshed,
waited on, or cancelled:

```python
req.refresh()
req.wait(timeout=30)
req.cancel()
```

`refresh()` re-fetches the current status of the tracked request from the worker
proxy and updates the same `SailboxRequest` object in place. It does not wait
for completion, does not retry the outbound HTTP request, and does not send a
new request to the destination URL. Use it when you want to poll manually or
inspect whether a request has completed before deciding what to do next. Use
`wait()` when you want to block until the request reaches a terminal state.

## Daemon sailboxes

Use `Sailbox.create_daemon()` for a sailbox whose primary workload is one
managed long-running process. Daemon sailboxes support lifecycle operations,
listeners, and tracked network requests, but public `exec()` is disabled after
the daemon is registered.

```python
daemon = sail.Sailbox.create_daemon(
    app=app,
    image=sail.Image.debian_arm64,
    name="daemon-1",
    command="python3 -m http.server 3000",
    cwd="/",
    checkpoint_poll_frequency_s=5,
    min_checkpoint_cooldown_s=30,
    ingress_ports=[3000],
)

print(daemon.sailbox_id)
print(daemon.daemon_pgid)
```

## Lifecycle

```python
sb.checkpoint()  # Snapshot while keeping the sailbox running
sb.pause()       # Checkpoint and pause until explicit resume
sb.sleep()       # Checkpoint and sleep until network ingress, exec, or resume
sb.resume()      # Resume a paused or sleeping sailbox
sb.terminate()   # Permanently destroy the sailbox
```

After `pause()`, `exec()` raises `sail.SailboxExecutionError` until `resume()`
succeeds. Network traffic does not wake a paused sailbox; only `resume()` does.
After `sleep()`, network ingress, `exec()`, or `resume()` wakes the sailbox from
its latest checkpoint.
`terminate()` is permanent.

We highly recommend calling `sb.checkpoint()` after any non-deterministic
command. Upon a node failure, Sailboxes will replay commands from the latest
checkpoint onwards. Checkpointing immediately after a non-deterministic command
allows you to avoid diverging behaviour on replays.

## Custom images

Start from the arm64 Debian base image, add build steps, and call `build()`:

```python
image = (
    sail.Image.debian_arm64
    .apt_install("git", "curl")
    .pip_install("requests")
    .run_commands("python3 -m pip show requests >/tmp/requests.txt")
    .env({"APP_ENV": "demo"})
)

built_image = image.build(timeout=1800)

sb = sail.Sailbox.create(
    app=app,
    image=built_image,
    name="custom-image-demo",
)
```

Image definitions are immutable; each helper returns a new definition. Supported
helpers are `apt_install(*packages)`, `pip_install(*packages)`,
`run_commands(*commands)`, `env(dict[str, str])`, and `build(timeout=1800)`.

## API surface

Common exported SDK types:

| Type | Description |
| --- | --- |
| `sail.App` | Sail application namespace; use `App.find(name=..., mint_if_missing=True)` |
| `sail.Image` | Image namespace with `debian_arm64`, `debian_arm`, `debian_amd64`, and `debian_amd` |
| `sail.Sailbox` | Standard sailbox handle |
| `sail.DaemonSailbox` | Sailbox handle for daemon workloads |
| `sail.SailboxExecRequest` | Durable exec request returned by `exec()` |
| `sail.SailboxExecResult` | `stdout`, `stderr`, and `returncode` from `wait()` |
| `sail.SailboxListener` | Public listener metadata for an exposed guest port |
| `sail.SailboxRequest` | Tracked outbound HTTP request |
| `sail.SailboxResponse` | HTTP response with `status_code`, `headers`, `content`, `text`, and `json()` |

Common exceptions:

| Exception | Raised when |
| --- | --- |
| `sail.SailError` | Base SDK error |
| `sail.SailboxError` | Base sailbox-specific error |
| `sail.SailboxCreationError` | Creation, checkpoint, stop, start, or scheduler lifecycle operation fails |
| `sail.SailboxExecutionError` | Exec, listener, or network request operation fails |
| `sail.SailboxExecAlreadyRunningError` | Another exec request is already active |
| `sail.SailboxExecRequestNotFoundError` | A durable exec request cannot be found |
| `sail.SailboxTerminatedError` | The sailbox no longer exists on the worker |
| `sail.SailboxWorkerLostError` | The assigned worker was lost during exec wait |
| `sail.ImageBuildError` | Custom image build fails |
