# Building a tool-calling agent Source: https://docs.sailresearch.com/agents Multi-turn tool-use conversations with the Sail API Sail supports tool calling through the Responses API, letting you build agents that call external tools and reason over the results across multiple turns. ## How it works 1. Send a user message along with **tool definitions** to `/v1/responses`. 2. The model may return one or more `function_call` items instead of (or alongside) text. 3. Execute the tools locally, then send the results back as `function_call_output` items in a new request, together with the full conversation history. 4. Repeat until the model responds with text only. Each request includes the entire conversation so far. Append `response.output` items directly to your conversation list (they are valid input items with no conversion needed), then append the `function_call_output` results. ## Full example: multi-turn weather agent This example uses `zai-org/GLM-5.3` to build a two-turn conversation where the model calls a weather tool and then answers a follow-up question using context from the first turn. ```python theme={null} import json import time from openai import OpenAI client = OpenAI( base_url="https://api.sailresearch.com/v1", api_key="YOUR_SAIL_API_KEY", ) MODEL = "zai-org/GLM-5.3" TOOLS = [ { "type": "function", "name": "get_weather", "description": "Get the current weather for a location.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City and state, e.g. San Francisco, CA", } }, "required": ["location"], "additionalProperties": False, }, "strict": True, }, ] TOOL_DISPATCH = {"get_weather": lambda location: '{"temperature": "62°F", "condition": "Foggy"}'} def poll(response, timeout=300): start = time.time() terminal_statuses = ("completed", "incomplete", "failed", "cancelled") while response.status not in terminal_statuses: if time.time() - start > timeout: raise TimeoutError(f"{response.id} did not complete within {timeout}s") time.sleep(2) response = client.responses.retrieve(response.id) if response.status in ("failed", "cancelled"): raise RuntimeError(f"{response.id} status: {response.status}") return response def agent_turn(conversation, user_message): """Send a user message and loop until the model stops calling tools.""" conversation.append({"role": "user", "content": user_message}) while True: response = client.responses.create( model=MODEL, input=conversation, tools=TOOLS, max_output_tokens=4096, background=True, ) response = poll(response) # Incomplete is terminal, but content-filtered output is not a turn result. if response.status == "incomplete": reason = response.incomplete_details.reason if reason == "content_filter": raise RuntimeError("Response stopped by a content filter") if reason != "max_output_tokens": raise RuntimeError(f"Response incomplete: {reason}") # Token-limited responses preserve partial output for the caller. return response tool_calls = [ item for item in (response.output or []) if getattr(item, "type", None) == "function_call" ] conversation.extend(response.output) if not tool_calls: return response for call in tool_calls: args = json.loads(call.arguments) output = TOOL_DISPATCH[call.name](**args) conversation.append( {"type": "function_call_output", "call_id": call.call_id, "output": output} ) conversation = [] # Turn 1: triggers a get_weather tool call, then the model summarizes the result response = agent_turn(conversation, "What's the weather in San Francisco?") print("Turn 1:", response.output_text) # Turn 2: follow-up reuses conversation context response = agent_turn(conversation, "How about New York, warmer or colder?") print("Turn 2:", response.output_text) ``` ### What happens under the hood 1. **Turn 1**: the model receives the user question plus the tool definition. It calls `get_weather` for San Francisco. After we send the tool result back, a second request is made and the model produces a text summary. 2. **Turn 2**: the full conversation (including Turn 1's tool call and result) is sent again. The model calls `get_weather` for New York, gets the result, and compares it with the San Francisco data it already has in context. ## Tips * **Optionally choose a completion window for your agent loop.** `balanced` buys more tokens per dollar for autonomous work and `flex` offers the lowest prices for time-insensitive workloads. See [Completion windows](/completion-windows) for the cost and latency tradeoff. * **`background=True`** is recommended for long-running agents and required for `flex`. Background mode avoids HTTP timeouts and lets you poll for completion. * **Treat `incomplete` as terminal.** It is not an error and can contain partial output and usage. Inspect `incomplete_details.reason` before deciding whether to continue with the partial result or submit a new request. For a normalized `max_output_tokens` reason, consider both the request's output limit and its total input plus output context usage. Depending on which limit stopped the response, reduce the input or adjust the output budget. * **Do not assume every terminal response is `completed`.** Stop polling on `completed`, `incomplete`, `failed`, or `cancelled`. Only `completed` and `incomplete` can contain output to consume. * **Send the full conversation** in each request. Include all prior messages, `response.output` items, and tool results. Output items from previous responses can be appended directly. No serialization or conversion is needed. * **`strict: true`** on tool parameters enables structured output guarantees: the model's `arguments` JSON will always conform to your schema. * **Parallel tool calls** are supported by default. The model may return multiple `function_call` items in a single response. * **Add a per-request `Idempotency-Key` header** so retries will use the stored response instead of re-running inference and double charging. See [Idempotent Requests](/idempotency). # AI Quickstart Source: https://docs.sailresearch.com/ai-quickstart Set up your coding agent with Sail's docs MCP and workflow skills for migration and agent building. Sail works with the coding agent you already use. Claude Code and local Codex sessions can install the full Sail plugin with its workflow skills. Other tools can still use the documentation MCP server. ## Overview Start using Sail by providing your agent with Sail's docs and the patterns to migrate existing apps or build observable agents on Sail. Connects your agent to Sail's documentation: the API, models, pricing, and SDK. Served live from these docs, so it stays current. Gives your agent the patterns to migrate existing apps and to build, instrument, and debug observable agents on Sail. ## Step 1: Give your agent Sail's docs Connect your tool to the Sail docs MCP server at `https://docs.sailresearch.com/mcp`. It gives your agent a live connection to these docs, so answers stay current: * **Claude Code:** `claude mcp add --transport http sail-docs https://docs.sailresearch.com/mcp` * **Codex:** `codex mcp add sail-docs --url https://docs.sailresearch.com/mcp` * **Claude Desktop:** Settings → Connectors → Add custom connector, then paste the URL. * **ChatGPT:** Where your workspace allows Developer mode and custom connectors, go to Settings → Apps & Connectors → Developer mode → Add new connector, then paste the URL. * **Cursor:** Settings → MCP, then add the URL. See the [MCP server guide](/mcp-server) for more clients and example questions. **Prefer not to install anything?** Every docs page has a copy button at the top if you want to paste content directly into your agent interface. Or point your tool at [llms.txt](https://docs.sailresearch.com/llms.txt) or [llms-full.txt](https://docs.sailresearch.com/llms-full.txt). ## Step 2: Give your agent Sail's workflow skills The canonical package is [sailresearchco/sail-skills](https://github.com/sailresearchco/sail-skills). It packages skills for migrating existing apps to Sail and building observable agents. An agent run is recorded as a Voyage: a trace of named agents, spans, and events, with every model call and Sailbox command attributed to the right step. | Skill | Use it when | | ---------------------------- | -------------------------------------------------------------------------------------------------- | | `sail-migrate` | Migrate an existing app, agent, or workflow to Sail while preserving behavior. | | `sail-voyage` | Build or instrument a Voyage with agents, spans, events, Sailbox commands, and terminal lifecycle. | | `sail-inference-with-voyage` | Attribute Sail inference model calls to the active agent and span. | | `sail-voyage-debugging` | Diagnose a Voyage that ran but appears incomplete or incorrect in the dashboard. | | `sail-gpu-marketplace` | Allocate and use preemptible GPU compute or recover checkpointed work after an interruption. | Install for your agent: ```text Claude Code theme={null} /plugin marketplace add sailresearchco/sail-skills /plugin install sail@sail ``` ```bash Codex theme={null} codex plugin marketplace add sailresearchco/sail-skills codex plugin add sail@sail ``` Before you run generated SDK code, install `sail`. The Sail SDK can use a credential stored by `sail auth login` on the same machine. For CI, deployments, Sailbox guests, or third-party inference SDKs configured for Sail, provide `SAIL_API_KEY` in that runtime environment. ## Next steps **Already have an agent or workflow?** Install the skills above, then ask your coding agent to "Migrate this app to Sail". The `sail-migrate` skill guides the whole migration, including moving sandboxed execution to Sail. Prefer not to install anything? Copy the migration prompt from [Migrate to the Sail API](/migrate#copy-migration-prompt) instead. **Starting fresh?** Use the `sail-voyage` skill to build your own observable AI workflow. Ask your coding agent: ```text theme={null} Build a small background research agent on Sail and make the whole run show up as a trace I can open in the dashboard, each step, the model calls it makes, and the sandbox commands it runs, attributed to the right part of the workflow. ``` The `sail-voyage` skill takes it from there. **Or** make a plain inference call first. See the [inference Quickstart](/quickstart). # Find or create an app Source: https://docs.sailresearch.com/api-reference/apps/find-or-create-an-app /sailbox-openapi.json post /apps/find Looks up an app by name and returns its id, which every Sailbox create needs. An app groups Sailboxes that belong to the same workload, and a listener allowlist that names an app lets every Sailbox in it through. Set `mint_if_missing` to create the app when it does not exist yet. That makes this safe to call on every start. Apps are managed on `https://api.sailresearch.com/v1`, not on the Sailbox base URL. The same API key works on both. # List apps Source: https://docs.sailresearch.com/api-reference/apps/list-apps /sailbox-openapi.json get /apps Returns every app in your organization, including apps with no Sailboxes yet. Apps are managed on `https://api.sailresearch.com/v1`, not on the Sailbox base URL. The same API key works on both. # Create a batch Source: https://docs.sailresearch.com/api-reference/batches-api/create-a-batch /openapi.json post /batches Submit a batch of requests for asynchronous processing. # Get batch request result Source: https://docs.sailresearch.com/api-reference/batches-api/get-batch-request-result /openapi.json get /batches/{batch_id}/{custom_id} Retrieve the result of a specific request within a batch by its custom_id. # Get batch status Source: https://docs.sailresearch.com/api-reference/batches-api/get-batch-status /openapi.json get /batches/{batch_id} Retrieve the current status of a batch. # List batches Source: https://docs.sailresearch.com/api-reference/batches-api/list-batches /openapi.json get /batches List batches with optional pagination. # Create a chat completion Source: https://docs.sailresearch.com/api-reference/chat-completions-api/create-a-chat-completion /openapi.json post /chat/completions OpenAI-compatible Chat Completions endpoint. Supports streaming via stream: true, which returns a Server-Sent Events stream of chat.completion.chunk objects. # Checkpoint a Sailbox Source: https://docs.sailresearch.com/api-reference/checkpoints/checkpoint-a-sailbox /sailbox-openapi.json post /sailboxes/{sailbox_id}/checkpoint Saves the Sailbox's filesystem and memory as a checkpoint you can start new Sailboxes from. The Sailbox is left as it was, so a running Sailbox keeps running and a sleeping or paused one stays down. # Start a Sailbox from a checkpoint Source: https://docs.sailresearch.com/api-reference/checkpoints/start-a-sailbox-from-a-checkpoint /sailbox-openapi.json post /sailboxes/from_checkpoint Creates a new Sailbox from a checkpoint you took earlier. It comes up with the memory and filesystem saved in the checkpoint, so processes the original was running carry on there. Commands run through an SDK or the CLI stop there. Everything they had written by then is on the filesystem, and one started with their background option keeps running. Start whatever else you need running again. A Sailbox that mounts a volume always comes up cold, with the disk intact and nothing running, and any Sailbox can come up cold, so write code that expects it. Volumes mounted on the original Sailbox are mounted on the new one at the same paths. They are the same volumes, not copies, so both Sailboxes read and write the same files. The new Sailbox keeps the original's network policy. # Cancel an exec Source: https://docs.sailresearch.com/api-reference/exec/cancel-an-exec /sailbox-openapi.json post /sailboxes/{sailbox_id}/exec/{exec_id}/cancel Sends SIGINT by default. Set `force` to send SIGKILL. Repeated requests are safe. # Resize an exec PTY Source: https://docs.sailresearch.com/api-reference/exec/resize-an-exec-pty /sailbox-openapi.json post /sailboxes/{sailbox_id}/exec/{exec_id}/resize Sets the latest terminal dimensions. An unknown or non-PTY exec is a no-op. # Resync an exec PTY Source: https://docs.sailresearch.com/api-reference/exec/resync-an-exec-pty /sailbox-openapi.json post /sailboxes/{sailbox_id}/exec/{exec_id}/resync Requests a terminal repaint on the live exec stream. An unknown or non-PTY exec is a no-op. # Run a command Source: https://docs.sailresearch.com/api-reference/exec/run-a-command /sailbox-openapi.json post /sailboxes/{sailbox_id}/exec Starts a command or reconnects to one with the same idempotency key. The response is newline-delimited JSON. Its first event is `started`. Output `data` and terminal repaint `data` are base64. The highest stdout and stderr `seq` values are reconnect cursors. `exit` is terminal. A `heartbeat` can appear after 30 seconds without another event. # Wait for an exec Source: https://docs.sailresearch.com/api-reference/exec/wait-for-an-exec /sailbox-openapi.json post /sailboxes/{sailbox_id}/exec/{exec_id}/wait Waits for the guest's authoritative result. It can wake a sleeping Sailbox. # Write exec stdin Source: https://docs.sailresearch.com/api-reference/exec/write-exec-stdin /sailbox-openapi.json put /sailboxes/{sailbox_id}/exec/{exec_id}/stdin Writes raw bytes at an absolute offset. Retries can overlap bytes already accepted. # Read a file Source: https://docs.sailresearch.com/api-reference/files/read-a-file /sailbox-openapi.json get /sailboxes/{sailbox_id}/files Streams one regular file without buffering it in the service. # Write a file Source: https://docs.sailresearch.com/api-reference/files/write-a-file /sailbox-openapi.json put /sailboxes/{sailbox_id}/files Streams raw bytes into one regular file. A complete retry safely replaces the target. # Attach an HTTP policy Source: https://docs.sailresearch.com/api-reference/http-policies/attach-an-http-policy /sailbox-openapi.json put /sailboxes/{sailbox_id}/http-policy Attaches a policy to a Sailbox, replacing any policy already attached. New HTTPS connections use the new policy. A connection that is already open keeps its previous policy until it closes. # Clear a Sailbox's HTTP policy Source: https://docs.sailresearch.com/api-reference/http-policies/clear-a-sailboxs-http-policy /sailbox-openapi.json delete /sailboxes/{sailbox_id}/http-policy Removes the attached policy. This succeeds when no policy is attached. New HTTPS connections have no policy. A connection that is already open keeps its previous policy until it closes. # Create an HTTP policy Source: https://docs.sailresearch.com/api-reference/http-policies/create-an-http-policy /sailbox-openapi.json post /http-policies Creates a named policy for your organization. Every secret named in the document must already exist. A policy document cannot change after creation, but its name can. 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. # Delete an HTTP policy Source: https://docs.sailresearch.com/api-reference/http-policies/delete-an-http-policy /sailbox-openapi.json delete /http-policies/{policy_id} Deletes a policy. Clear it from every Sailbox first. # Get a Sailbox's HTTP policy Source: https://docs.sailresearch.com/api-reference/http-policies/get-a-sailboxs-http-policy /sailbox-openapi.json get /sailboxes/{sailbox_id}/http-policy Returns the HTTP policy currently attached to a Sailbox. # Get an HTTP policy Source: https://docs.sailresearch.com/api-reference/http-policies/get-an-http-policy /sailbox-openapi.json get /http-policies/{policy_id} Returns one policy, including its document. # List HTTP policies Source: https://docs.sailresearch.com/api-reference/http-policies/list-http-policies /sailbox-openapi.json get /http-policies Returns a page of your organization's HTTP policies. Each item includes usage counts and secret names, but not the policy document. # Rename an HTTP policy Source: https://docs.sailresearch.com/api-reference/http-policies/rename-an-http-policy /sailbox-openapi.json patch /http-policies/{policy_id} Changes the policy's display name. Its document stays the same. # whoami Source: https://docs.sailresearch.com/api-reference/identity/whoami /sailbox-openapi.json get /whoami Returns the organization behind the API key, and the user when the key is user-scoped. Use it to tell your own Sailboxes apart from a teammate's by comparing `user_id` against `created_by_user_id`. # Create a Sailbox Source: https://docs.sailresearch.com/api-reference/lifecycle/create-a-sailbox /sailbox-openapi.json post /sailboxes Creates a Sailbox and waits for startup to finish, which can take a few minutes. `image` has to describe an image that is ready to boot: a base image on its own, or one already built from that same spec through an SDK. Send an `Idempotency-Key` so a network retry replays this answer instead of starting a second Sailbox. A server error can still arrive once the Sailbox exists, so read [Retrying safely](/sailboxes-http-api#retrying-safely) before you retry one. # List Sailboxes Source: https://docs.sailresearch.com/api-reference/lifecycle/list-sailboxes /sailbox-openapi.json get /sailboxes Returns Sailboxes in your organization, most recently active first. Terminated Sailboxes stay in the list, so filter by `status` if you only want live ones. # Pause a Sailbox Source: https://docs.sailresearch.com/api-reference/lifecycle/pause-a-sailbox /sailbox-openapi.json post /sailboxes/{sailbox_id}/pause Saves the Sailbox's memory and filesystem and stops charging for compute. Published listeners stop answering. Call resume to bring it back with its processes intact. A Sailbox that mounts a volume, or that has an upgrade waiting, comes back cold instead, with the disk intact and nothing running. Any Sailbox can come back cold, so write code that expects it. # Resume a Sailbox Source: https://docs.sailresearch.com/api-reference/lifecycle/resume-a-sailbox /sailbox-openapi.json post /sailboxes/{sailbox_id}/resume Brings a paused or sleeping Sailbox back with its memory and running processes intact. A Sailbox that mounts a volume, or that has an upgrade waiting, comes back cold instead, with the disk intact and nothing running. Any Sailbox can come back cold, so write code that expects it. The call returns 200 whether or not the Sailbox came back, so read `resume_state` before you use it. `running` and `already_running` both mean it is ready. `terminal_unavailable` means it can never resume, and `error_message` says why. # Retrieve a Sailbox Source: https://docs.sailresearch.com/api-reference/lifecycle/retrieve-a-sailbox /sailbox-openapi.json get /sailboxes/{sailbox_id} Returns one Sailbox, including its latest observed resource usage. # Schedule a wake Source: https://docs.sailresearch.com/api-reference/lifecycle/schedule-a-wake /sailbox-openapi.json post /sailboxes/{sailbox_id}/wake_at Sets the time a sleeping Sailbox comes back on its own. Give a time in the future. Sending this to a running Sailbox records the wake without putting it to sleep. Sleep it separately and it comes back at the scheduled time. A sooner wake replaces one already scheduled. A later one leaves the existing wake in place, so read `wake_at` in the response to see which one is in effect. A Sailbox that is paused, terminated, or in a failed state takes no scheduled wake. # Set the automatic-sleep preference Source: https://docs.sailresearch.com/api-reference/lifecycle/set-the-automatic-sleep-preference /sailbox-openapi.json post /sailboxes/{sailbox_id}/auto_sleep Replaces the Sailbox's automatic-sleep preference: whether Sail may sleep it automatically, and how long Sail waits first. The whole preference is replaced in one call, so send the complete shape you want. 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; it still sleeps only when fully idle. Your own `sleep`, `pause`, `resume`, and scheduled wakes work the same whatever it says. # Sleep a Sailbox Source: https://docs.sailresearch.com/api-reference/lifecycle/sleep-a-sailbox /sailbox-openapi.json post /sailboxes/{sailbox_id}/sleep Saves the Sailbox's memory and filesystem, stops charging for compute, and lets the Sailbox wake by itself when traffic arrives on a published listener. That first connection waits while the Sailbox comes back. It comes back with its processes intact. A Sailbox that mounts a volume, or that has an upgrade waiting, comes back cold instead, with the disk intact and nothing running. Any Sailbox can come back cold, so write code that expects it. # Terminate a Sailbox Source: https://docs.sailresearch.com/api-reference/lifecycle/terminate-a-sailbox /sailbox-openapi.json post /sailboxes/{sailbox_id}/terminate Shuts the Sailbox down for good and releases its resources. Billing stops. The Sailbox cannot be restarted, and anything not written to a volume or a checkpoint is gone. Terminating an already terminated Sailbox succeeds. # Upgrade a Sailbox Source: https://docs.sailresearch.com/api-reference/lifecycle/upgrade-a-sailbox /sailbox-openapi.json post /sailboxes/{sailbox_id}/upgrade Moves the Sailbox onto the current Sail runtime. A running Sailbox restarts to pick it up, which stops running processes and clears memory, so pick the moment yourself. A paused or sleeping Sailbox records the upgrade and applies it on its next wake, and the response has `applied: false`. That wake is a cold start too, so its memory and running processes do not survive it. A Sailbox already on the current runtime answers `applied: true` and is left as it is, running or not. A Sailbox left past its upgrade deadline can be restarted for you. # Count tokens for an Anthropic message Source: https://docs.sailresearch.com/api-reference/messages-api/count-tokens-for-an-anthropic-message /openapi.json post /messages/count_tokens Counts the input tokens a create-message request would consume, without running the model. # Create an Anthropic message Source: https://docs.sailresearch.com/api-reference/messages-api/create-an-anthropic-message /openapi.json post /messages Anthropic-compatible Messages endpoint supporting system prompts, tool calling, and Anthropic SSE framing after generation completes. # Retrieve a message Source: https://docs.sailresearch.com/api-reference/messages-api/retrieve-a-message /openapi.json get /messages/{messageID} Returns the current status of a queued message. Completed responses include the Anthropic message fields. Use this operation with the X-Sail-Message-Id returned after a non-streaming timeout. # List supported models Source: https://docs.sailresearch.com/api-reference/models-api/list-supported-models /openapi.json get /models # Add a custom domain Source: https://docs.sailresearch.com/api-reference/networking/add-a-custom-domain /sailbox-openapi.json post /sailboxes/{sailbox_id}/domains Serves a published `http` port under a hostname you own. Point the hostname at the target first, with a CNAME record. A name at the root of a domain cannot hold a CNAME, so route it with your provider's ALIAS or ANAME record and add a TXT record at `_sail-domains.` plus the hostname carrying the same target. The TXT record proves the hostname is yours; it does not route on its own. Read the target from `GET /custom-domains`. Sail checks DNS while handling this request, so create the record and give it time to propagate before calling. Until it resolves the request answers 400, which is worth sending again once the record is live. Turn off any proxying or acceleration your DNS provider applies to the record: that resolves the hostname to the provider rather than to the target, and no amount of waiting clears it. The port has to be published as an `http` listener already. The TLS certificate is obtained on the first HTTPS request to the hostname, so that one request can take up to a minute; the rest are served straight away, and renewal is automatic. Registering a hostname that already serves the same port returns what is there, so the call is safe to repeat. Registering one of your own hostnames against a different Sailbox moves it, with no need to remove it first. This works on a running, paused, or sleeping Sailbox. A request to the hostname wakes a sleeping one. # Get custom-domain DNS targets Source: https://docs.sailresearch.com/api-reference/networking/get-custom-domain-dns-targets /sailbox-openapi.json get /custom-domains Returns the two hostnames to use in custom-domain DNS. Point an attached hostname or wildcard CNAME at `cname_target`. Point the `_acme-challenge` CNAME at `acme_challenge_target` to use one wildcard certificate for direct subdomains. Both targets are the same for every domain in your organization. Registering a hostname needs its DNS to resolve to `cname_target`, so this is the first call to make. # Get headers that identify a Sailbox Source: https://docs.sailresearch.com/api-reference/networking/get-headers-that-identify-a-sailbox /sailbox-openapi.json get /sailboxes/{sailbox_id}/ingress-auth Returns headers you attach to a request so it is recognized as coming from this Sailbox. Use them to reach a listener whose allowlist names an app when your code runs outside a Sailbox. The headers are a credential, so they are only issued while the Sailbox can still run. One that has stopped for good, whether you terminated it or it failed, no longer has an identity to hand out. Paused and sleeping Sailboxes still have one. # List custom domains Source: https://docs.sailresearch.com/api-reference/networking/list-custom-domains /sailbox-openapi.json get /sailboxes/{sailbox_id}/domains Returns every hostname of your own that serves this Sailbox, and the target to point DNS at. The target is the same across your organization, so a caller registering its first hostname can read it here first. # List published ports Source: https://docs.sailresearch.com/api-reference/networking/list-published-ports /sailbox-openapi.json get /sailboxes/{sailbox_id}/listeners Returns every port the Sailbox publishes, with the address to reach each one. This works on a paused or sleeping Sailbox and does not wake it. # Publish a port Source: https://docs.sailresearch.com/api-reference/networking/publish-a-port /sailbox-openapi.json post /sailboxes/{sailbox_id}/listeners Publishes a port from inside the Sailbox so it can be reached from outside. This works on a running Sailbox without restarting it, and on a paused or sleeping one. An `http` listener gets an HTTPS URL. A `tcp` listener gets a host and port to dial, which is what SSH and database clients need. Without an `allowlist` the port is reachable by anyone who has the address, so set one unless you mean to publish to the internet. Publishing a port that is already published under the same protocol sets its `allowlist` to what you send, so send the whole list every time. Sending none clears the restriction and reopens the port. # Remove a custom domain Source: https://docs.sailresearch.com/api-reference/networking/remove-a-custom-domain /sailbox-openapi.json delete /sailboxes/{sailbox_id}/domains/{domain} Stops serving the hostname and releases its certificate. The published port keeps serving its own address, and the body echoes the registration that was removed. Delete the DNS record afterwards. One left pointing at the target resolves to nothing Sail serves. # Retrieve a published port Source: https://docs.sailresearch.com/api-reference/networking/retrieve-a-published-port /sailbox-openapi.json get /sailboxes/{sailbox_id}/listeners/{guest_port} Returns one published port and the address to reach it. # Unpublish a port Source: https://docs.sailresearch.com/api-reference/networking/unpublish-a-port /sailbox-openapi.json delete /sailboxes/{sailbox_id}/listeners/{guest_port} Stops serving traffic on a published port. This works whether the Sailbox is running, paused, or sleeping, and does not wake it. Any custom domain serving an `http` port goes with it, so re-publishing the port leaves the hostnames to register again. Sail keeps an unpublished `tcp` address inside your organization and can reuse it for another of your Sailboxes. Publish the port again and read the new address out of the response. # Create a response Source: https://docs.sailresearch.com/api-reference/responses-api/create-a-response /openapi.json post /responses Creates an OpenAI Responses API task. Returns 202 when background=true, otherwise returns 200 after completion. Foreground stream=true requests return OpenAI Responses Server-Sent Events. # Retrieve a response Source: https://docs.sailresearch.com/api-reference/responses-api/retrieve-a-response /openapi.json get /responses/{response_id} # Delete a secret Source: https://docs.sailresearch.com/api-reference/secrets/delete-a-secret /sailbox-openapi.json delete /secrets/{name} Deletes a secret from your organization. Delete every HTTP policy that uses it first. # Get a secret Source: https://docs.sailresearch.com/api-reference/secrets/get-a-secret /sailbox-openapi.json get /secrets/{name} Returns a secret's name and timestamps, but never its value. # List policies using a secret Source: https://docs.sailresearch.com/api-reference/secrets/list-policies-using-a-secret /sailbox-openapi.json get /secrets/{name}/references Returns the HTTP policies that refer to this secret and must be deleted before the secret can be deleted. # List secrets Source: https://docs.sailresearch.com/api-reference/secrets/list-secrets /sailbox-openapi.json get /secrets Returns the name and timestamps for every secret in your organization. Sail never returns the stored values. # Set a secret Source: https://docs.sailresearch.com/api-reference/secrets/set-a-secret /sailbox-openapi.json put /secrets/{name} Creates a secret for your organization, or replaces its value when the name already exists. Sail never returns the stored value. After this call succeeds, the next matching request from any Sailbox using this organization secret gets the new value. # Get your SSH certificate authority Source: https://docs.sailresearch.com/api-reference/ssh-access/get-your-ssh-certificate-authority /sailbox-openapi.json get /ssh/ca Returns the public key your organization's Sailboxes trust for SSH. It is created the first time you ask for it. A Sailbox starts trusting it when you turn SSH on in that Sailbox, which needs an SDK or the CLI. # Issue an SSH certificate Source: https://docs.sailresearch.com/api-reference/ssh-access/issue-an-ssh-certificate /sailbox-openapi.json post /ssh/certificate Signs your SSH public key so you can connect to a Sailbox that has SSH turned on. Save the returned certificate next to your private key as `-cert.pub` and `ssh` presents it automatically. Certificates are short-lived, so ask for a fresh one rather than storing it long term. Turning SSH on inside a Sailbox needs an SDK or the CLI, and only has to happen once per Sailbox. After that, publish port 22 as a `tcp` listener and dial the host and port it returns. See [Networking](/sailboxes-networking) for the full walkthrough. # Get resource usage over time Source: https://docs.sailresearch.com/api-reference/usage/get-resource-usage-over-time /sailbox-openapi.json get /sailboxes/{sailbox_id}/metrics Returns CPU, memory, and disk usage for one Sailbox as a time series. # Get Sailbox spend Source: https://docs.sailresearch.com/api-reference/usage/get-sailbox-spend /sailbox-openapi.json get /sailboxes/spend Returns Sailbox usage and estimated cost for your organization over a time window, with a per-Sailbox breakdown. Defaults to the current UTC calendar month up to now. Costs are reported in billionths of a US dollar, and the active portion is an estimate that settles when the Sailbox stops. # Create a volume Source: https://docs.sailresearch.com/api-reference/volumes/create-a-volume /sailbox-openapi.json post /sailbox-volumes Creates a volume you can mount into Sailboxes. Volume names are unique within an organization, so creating a name that already exists returns the existing volume rather than failing. That makes this safe to call on every start. # Delete a volume Source: https://docs.sailresearch.com/api-reference/volumes/delete-a-volume /sailbox-openapi.json delete /sailbox-volumes/{volume_id} Deletes a volume. Nothing can mount it again and its contents are permanently unreachable. Terminate every Sailbox that still mounts it first, because a paused or sleeping Sailbox expects the volume when it wakes. A Sailbox that is already shutting down, or that failed and cannot be restored, no longer holds it. # List volumes Source: https://docs.sailresearch.com/api-reference/volumes/list-volumes /sailbox-openapi.json get /sailbox-volumes Returns the volumes in your organization, newest first. Pass `name` to look one up without creating it. The page holds that volume if it exists and is empty if it does not. # Completion windows Source: https://docs.sailresearch.com/completion-windows Trade off latency for lower token prices Completion windows let you express how long your requests can wait, giving Sail room to increase cost efficiency. Sail serves low-latency inference by default for core models, at lower cost than traditional inference providers. You can opt in to the `balanced` or `flex` completion windows to cut token costs drastically, for work that is latency tolerant. Completion windows can be specified on a per-request basis. Long-horizon agents can make use of all three depending on the task at hand. ## Completion windows at a glance | Window | Scheduling | Typical use case | Price vs. traditional inference providers | | ---------- | ----------------------- | ---------------------------------------------------------- | ----------------------------------------- | | `asap` | Low-latency serving | Interactive UIs, human-in-the-loop | \~5-35% less | | `balanced` | Wider scheduling window | Background agents, subagents, and pipelines; parallel work | \~45-65% less | | `flex` | Best-effort scheduling | Batch processing, evals, offline | \~60-80% less | Not every core model supports every completion window yet. Check [Pricing](/pricing) for current per-model availability. ## Completion window details ### `asap` `asap` is the default, low-latency path for Sail's core models. Use it when a person is waiting or when one agent turn blocks the next.
For GLM-5.3 FP8 requests using `asap`, Sail targets: * Average time to first token (TTFT) under 10 seconds * P90 TTFT under `10 + (input tokens / 1,000)` seconds * Average generation speed of about 20 to 30 tokens per second (TPS) These operating targets apply only to GLM-5.3 FP8 on `asap` and are not an SLA.
### `balanced` `balanced` gives Sail more time to place work on efficient capacity, greatly lowering token costs for long-horizon background agents and pipelines. This tier enables you to run many background agents, taking on very large tasks, at reasonable cost. ### `flex` `flex` gives Sail the widest scheduling window and offers the lowest available token prices. It has no TTFT or TPS target. Common use cases include batch jobs, evals, and offline processing. For individual requests, `flex` requires the [Responses API](/support#responses-api) with `background=True`. [Batch API](/support#batch-api) requests can also use `flex`.
flex with Chat Completions and Messages}> Chat Completions and Messages technically accept `flex` as a completion window, but both wait synchronously and may time out before the request finishes. Therefore, we recommend using the Responses API or Batch API instead. `flex` requests can spend longer queued than other completion windows. Once Sail accepts a request, Sail keeps processing it unless it reaches a terminal status. A client-side polling timeout does not cancel its background work. Keep polling the response instead of creating a duplicate request. If submission fails ambiguously, retry with the same `Idempotency-Key` so Sail returns the existing work instead of creating a duplicate. If the response reaches `failed`, inspect its error before deciding whether to submit a new request.
## How to set completion windows Set `metadata.completion_window` to `asap`, `balanced`, or `flex`: ```bash Responses theme={null} curl https://api.sailresearch.com/v1/responses \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "Explain the key ideas behind transformers.", "metadata": { "completion_window": "balanced" } }' ``` ```bash Chat theme={null} curl https://api.sailresearch.com/v1/chat/completions \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "messages": [ {"role": "user", "content": "Explain the key ideas behind transformers."} ], "metadata": { "completion_window": "balanced" } }' ``` ```bash Messages theme={null} curl https://api.sailresearch.com/v1/messages \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Explain the key ideas behind transformers."} ], "metadata": { "completion_window": "balanced" } }' ``` ```bash Batch theme={null} curl https://api.sailresearch.com/v1/batches \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "/v1/responses", "label": "transformers", "requests": [ { "custom_id": "transformers-1", "params": { "model": "zai-org/GLM-5.3", "input": "Explain the key ideas behind transformers.", "metadata": { "completion_window": "balanced" } } } ] }' ``` For `flex`, set `background=true` when using the [Responses API](/support#responses-api). For reliable `flex` requests, use Responses API background mode or Batch instead of waiting synchronously through Chat Completions or Messages. ```bash Responses highlight={7} theme={null} curl https://api.sailresearch.com/v1/responses \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "Explain the key ideas behind transformers.", "background": true, "metadata": { "completion_window": "flex" } }' ``` ```bash Batch theme={null} curl https://api.sailresearch.com/v1/batches \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "/v1/responses", "label": "transformers", "requests": [ { "custom_id": "transformers-1", "params": { "model": "zai-org/GLM-5.3", "input": "Explain the key ideas behind transformers.", "metadata": { "completion_window": "flex" } } } ] }' ``` ### Default behavior For inference requests for [core models](/models), the default behavior (when no `completion_window` is specified) is low-latency inference (`asap`). The exceptions to this default behavior are: * Batch inference requests, which default to `balanced` for core models. * LoRA requests, which default to `balanced` for core models. For flex-only models, the default (and only) behavior is `flex` for all inference requests, including Batch and LoRA. Flex-only models require background requests (see examples above). # Data Processing Agreement Source: https://docs.sailresearch.com/dpa How Sail securely handles customer data for **inference** This Data Processing Agreement (“**DPA**”) forms part of the Self-Service Terms of Service or other written agreement between Sail and Customer for the provision of the Services (“**Agreement**”). Unless otherwise defined in this DPA, capitalized terms used in this DPA will have the meaning given to them in the Agreement. For more about how Sail secures customer data, see Sail’s [Trust Center](https://trust.sailresearch.com). *** ## 1. Introduction 1. **Roles of Parties.** For the purposes of the Agreement, the Parties agree that (a) Customer is the “controller” and “business” (as such terms are defined under applicable Data Protection Law) and (b) Sail is the “processor” and “service provider” (as such terms are defined under applicable Data Protection Law) with respect to the “Processing” (as such term is defined under applicable Data Protection Law) of Customer Data that constitutes “personal data,” “personal information,” “personally identifiable information,” or any analogous term under applicable Data Protection Law (“**Customer Personal Data**”). 2. **Order of Precedence.** If there is any conflict or inconsistency between the terms of the Agreement or this Data Processing Addendum (“**DPA**”), the terms of this DPA shall control to the extent of such conflict or inconsistency. ## 2. Customer Personal Data 1. **Scope of Processing.** The subject matter, nature and purpose of Sail’s Processing of Customer Personal Data, the types of Customer Personal Data Processed by Sail, and categories of applicable data subjects are set out in Annex I. 2. **Customer Personal Data Processing.** Sail will Process Customer Personal Data to provide the Services and in accordance with Customer’s documented instructions as set forth in this DPA, the Agreement, or otherwise communicated in writing by Customer to Sail provided that such instructions are consistent with this DPA and the Agreement (“**Documented Instructions**”). Unless prohibited by applicable Law, Sail will inform Customer if Sail is subject to a legal obligation that requires Sail to Process Customer Personal Data in contravention of Customer’s Documented Instructions. 3. **Documented Instructions.** Customer will ensure that its Documented Instructions comply with applicable privacy, data protection, and cybersecurity law (“**Data Protection Law**”) and is responsible for determining whether the Services are appropriate for the Processing of Customer Personal Data. 4. **Zero Training Commitment.** Sail will not use Customer Data to train, fine-tune, or improve any artificial intelligence or machine learning models, including any models operated by Sail or accessible through the Services without prior written consent of Customer (“**Zero Training Commitment**”). Sail contractually extends this prohibition to its model provider Subprocessors for Customer Data processed through their APIs. The Zero Training Commitment constitutes a material contractual obligation of Sail under this DPA. 5. **CCPA.** Sail will not (a) “sell” or “share” (as such terms are defined in the California Consumer Privacy Act) Customer Personal Data, (b) retain, use, or disclose Customer Personal Data for any purpose other than in accordance with the Documented Instructions, (c) retain, use, or disclose Customer Personal Data outside of the direct business relationship between Customer and Sail, nor (d) except as otherwise permitted under applicable Data Protection Law, combine Customer Personal Data with personal data that Sail receives from or on behalf of any third party. ## 3. Personnel 1. **Personnel.** Sail will ensure that all personnel authorized to Process Customer Personal Data are subject to an appropriate duty of confidentiality. ## 4. Subprocessors 1. **Authorization.** Customer provides general authorization for Sail to engage the subprocessors as described at [https://trust.sailresearch.com/?tab=subprocessors](https://trust.sailresearch.com/?tab=subprocessors) (“**Subprocessors**”). Sail will (a) enter into an agreement with each Subprocessor that imposes data protection obligations that are substantially as protective as Sail’s obligations under this DPA to the extent applicable to the nature of the services provided by such Subprocessor and (b) remain responsible for the acts and omissions of the Subprocessors’ Processing of Customer Personal Data under this DPA. 2. **Notice of New Subprocessors.** Sail shall make available on its Subprocessors webpage a mechanism to subscribe to notifications of new Subprocessors, and Sail will provide reasonable advance notice prior to appointing any new Subprocessor through such mechanism. Customer may object to the appointment of such new Subprocessor within 15 days of the date of such notice on reasonable privacy or security grounds by providing Sail written notice of its objection. In the event that Customer objects to Sail’s appointment of a new Subprocessor, Customer and Sail will work together in good faith to address any such objection. ## 5. Assistance 1. **Data Subject Rights.** Sail will (a) promptly forward to Customer any request it receives from “data subjects” or “consumers” (as such terms are defined under applicable Data Protection Law) to exercise their rights under applicable Data Protection Law relating to Customer Personal Data, (b) advise such data subjects and consumers to submit such requests directly to Customer, and (c) provide Customer with reasonable assistance as necessary for Customer to fulfil its obligations under applicable Data Protection Laws in responding to such requests. 2. **Cooperation.** Taking into account the nature of the Processing, Sail will provide Customer with reasonable assistance as necessary for Customer to fulfil its obligations under applicable Data Protection Laws, including to conduct data protection impact assessments and consultations with regulatory authorities. Sail may charge Customer a reasonable fee for such assistance under this Section 5.2. ## 6. Security 1. **Security Measures.** Sail will maintain reasonable and appropriate security measures designed to protect Customer Data in its possession and control as described on Sail’s Trust Center at [https://trust.sailresearch.com](https://trust.sailresearch.com) (“**Security Measures**”). Customer acknowledges that the Security Measures provide an appropriate level of security for the risks of the Processing of Customer Personal Data under the Agreement. Sail may update or modify the Security Measures provided that such updates and modifications do not materially decrease the overall security of the Services. 2. **Security Incident.** Sail will notify Customer without undue delay and in any case within 72 hours after becoming aware of any unauthorized access to, or disclosure or use of, Customer Personal Data (“**Security Incident**”). Sail will use reasonable efforts to investigate the Security Incident and mitigate the effects and remediate the causes of the Security Incident. Sail will assist Customer in complying with Customer’s obligations under applicable Data Protection Law by making reasonable efforts to provide Customer with information relating to the Security Incident. 3. **Audits.** Upon Customer’s written request, no more than once every 12 months, Sail will permit Customer to audit Sail’s controls applicable to its Processing of Customer Personal Data and compliance with this DPA (“**Audit**”), provided that such Audit is conducted at Customer’s sole cost, during normal business hours, in a manner that causes minimal disruption, and in accordance with mutually agreed upon scope and terms. ## 7. International Data Transfers 1. **Data Transfers.** Customer authorizes Sail to conduct transfers of Customer Personal Data to countries deemed to have an adequate level of data protection by the European Commission or the applicable competent regulatory authority on the basis of adequate safeguards in accordance with Data Protection Law or pursuant to (a) the contractual clauses annexed to the European Commission’s Implementing Decision 2021/914 of 4 June 2021 on standard contractual clauses for the transfer of Personal Data to third countries pursuant to Regulation (EU) 2016/679 of the European Parliament and of the Council, as amended, superseded, or replaced from time to time (“**EU SCCs**”) or (b) the International Data Transfer Addendum to the EU Commission Standard Contractual Clauses issued by the UK Information Commissioner, Version B1.0, in force 21 March 2022, as amended, superseded or replaced from time to time (“**UK Addendum**”). 2. **EU Data Transfers.** For transfers of Customer Personal Data from the European Union, Sail and Customer conclude Module 2 (controller-to-processor) of the EU SCCs and, if Customer is a processor on behalf of a third-party controller, Module 3 (Processor-to-Subprocessor) of the EU SCCs, which are incorporated herein and completed as follows: (a) the “data exporter” is Customer; (b) the “data importer” is Sail; (c) the optional docking clause in Clause 7 is implemented; (d) option 2 of Clause 9(a) is implemented and the time period therein is specified in Section 3.2; (e) the optional redress clause in Clause 11(a) is struck; (f) option 1 in Clause 17 is implemented; (g) the governing law is the law of Ireland and the courts in Clause 18(b) are the Courts of Dublin, Ireland; and (h) Annex I and Annex II to Module 2 and 3 of the EU SCCs are Schedule I and the Security Measures, respectively. For transfers of Customer Personal Data from Switzerland, any dispute arising from these EU SCCs relating to Swiss Data Protection Laws will be resolved by the courts of Switzerland and data subjects who have their habitual residence in Switzerland may bring claims under the EU SCCs before the courts of Switzerland. 3. **UK Data Transfers.** For transfers of Customer Personal Data from the United Kingdom, Sail and Customer conclude the UK Addendum, which is incorporated herein and completed as follows: (a) in Table 1, the “Exporter” is Customer and the “Importer” is Sail, their details are set forth in this DPA and the Agreement; (b) in Table 2, the first option is selected and the “Approved EU SCCs” are the EU SCCs referred to in Section 7.2; (c) in Table 3, Annexes 1 (A and B) and II to the “Approved EU SCCs” are Schedule I and the Security Measures respectively; and (d) in Table 4, both the “Importer” and the “Exporter” can terminate the UK Addendum. ## 8. Storage, Deletion, and Retention 1. **Persistent Storage.** Customer Data is stored only temporarily in Amazon S3 buckets (unless Customer chooses to use Customer-owned S3 buckets). 2. **Transient Processing.** All other Processing is transient in-memory only for the duration of the job. 3. **Automatic Deletion.** Sail’s production S3 buckets are configured with an automated deletion rule (implemented via S3 bucket lifecycle policy) that deletes Customer Data shortly after Processing. Precise deletion timing can vary in practice due to job retries, failures, or other operational conditions. Sail will not retain Customer Data for longer than 48 hours, except to the extent (a) required by Data Protection Laws or other applicable legal or regulatory requirements, (b) necessary to resolve a dispute between the parties, or (c) such Customer Personal Data is retained in accordance with Sail’s or its Subprocessors’ standard policies and procedures. *** ## Annex I ### List of Parties **Data exporter:** * **Name:** Customer * **Activities relevant to the data transferred under these Clauses:** Customer receives the Services as described in the Agreement and provides Customer Personal Data to Sail in that context. * **Role (controller/processor):** Controller. **Data importer:** * **Name:** Sail. * **Activities relevant to the data transferred under these Clauses:** Sail provides the Services to Customer as described in the Agreement and DPA and Processes Customer Personal Data on behalf of Customer in that context. * **Role (controller/processor):** Processor on behalf of Customer. ### Categories of Data Subjects Customer and Customer’s users. ### Categories of Personal Data Transferred As determined and controlled by Customer. ### Sensitive Data Transferred (If Applicable) Sensitive data transferred (if applicable) and applied restrictions or safeguards that fully take into consideration the nature of the data and the risks involved, such as for instance strict purpose limitation, access restrictions (including access only for staff having followed specialized training), keeping a record of access to the data, restrictions for onward transfers or additional security measures: N/A. ### Frequency of the Transfer The frequency of the International Data Transfer (e.g. whether the Personal Data is transferred on a one-off or continuous basis): On a continuous basis. ### Nature of the Processing The Customer Personal Data will be processed and transferred as described in the Agreement and DPA. ### Purpose(s) of the International Data Transfer and Further Processing The Customer Personal Data will be transferred and further processed for the provision of the Services as described in the Agreement and DPA. ### Duration of Processing The period for which personal data will be retained, or, if that is not possible, the criteria used to determine that period: Customer Personal Data will be retained for as long as necessary taking into account the purpose of the Processing, and in compliance with applicable laws, including laws on the statute of limitations and Data Protection Law. ### Sub-Processor Transfers For International Data Transfer to (Sub)Processors, also specify subject matter, nature and duration of the Processing: For the subject matter and nature of the Processing, reference is made to the Agreement and DPA. The Processing will take place for the duration of the Agreement. ### Competent Supervisory Authority The competent authority for the Processing of Customer Personal Data relating to data subjects located in the EEA is the Supervisory Authority of Ireland. The competent authority for the Processing of Customer Personal Data relating to data subjects located in the UK is the UK Information Commissioner. The competent authority for the Processing of Customer Personal Data relating to data subjects located in Switzerland is the Swiss Federal Data Protection and Information Commissioner. ### Technical and Organizational Measures Sail will implement security safeguards designed to protect the security, confidentiality and integrity of Personal Data as described on Sail’s [Trust Center](https://trust.sailresearch.com/). # Run Harbor tasks on Sailboxes Source: https://docs.sailresearch.com/harbor Run every Harbor trial in its own Sailbox with one flag [Harbor](https://harborframework.com) is a framework for evaluating and training agents on sandboxed tasks. Each trial runs an agent against one task inside an isolated environment, then a verifier scores the result. Harbor ships environments for Docker on your own machine and for several cloud sandboxes, and loads any other environment by import path. Sail provides one: ```text theme={null} sail.harbor:SailboxEnvironment ``` Pass that import path to Harbor and every trial's environment becomes a Sailbox. Tasks, agents, verifiers, and datasets are unchanged. ## Quickstart Install `sail` and `harbor` in the same Python environment (Harbor needs Python 3.12 or newer) and set your Sail API key. Then run Harbor's `hello-world` dataset with the `oracle` agent, which runs each task's reference solution and needs no model key: ```bash theme={null} pip install sail harbor export SAIL_API_KEY=sk_your_key_here harbor run -d hello-world@1.0 --agent oracle \ --env sail.harbor:SailboxEnvironment -o ./jobs ``` Harbor creates a Sailbox, runs the task's solution in it, runs the verifier, writes the trial's results under `./jobs`, and terminates the Sailbox when the trial ends. A real evaluation is the same command with a real agent, a model, and more trials in flight. Each trial gets its own Sailbox, so the machine running Harbor is no longer the limit on concurrency: ```bash theme={null} harbor run -d terminal-bench@2.0 --agent --model \ --env sail.harbor:SailboxEnvironment -n 32 ``` A job config file (`harbor run -c job.yaml`) selects the environment the same way: ```yaml theme={null} environment: import_path: sail.harbor:SailboxEnvironment ``` ## Credentials and app The process that runs Harbor needs a Sail credential: set `SAIL_API_KEY`, or run `sail auth login` once on that machine. Sailboxes are created in the `harbor` [app](/sailbox-sdk-apps) in your organization, which is created on first use; set `SAIL_APP` to use a different app. Each Sailbox is named `harbor-` followed by the trial's session id, and `sail box list --app harbor` (or the app you chose) lists the ones a run created. ## Task images Harbor decides where an environment's image comes from, and Sail follows the same rules as Harbor's other cloud providers. * A task that declares a prebuilt `docker_image` in its `task.toml` runs on that image, pulled from its registry. Docker-style short references work: `python:3.11` means `docker.io/library/python:3.11`. The registry must be one Sail supports (`docker.io`, `ghcr.io`, `public.ecr.aws`, or `quay.io`), and the image must be Debian or Ubuntu based; see [Bring your own base image](/sailboxes-images#bring-your-own-base-image) for the full requirements. * A task that ships an `environment/Dockerfile` instead has it built into a Sailbox image. The build happens once per organization: the first trial of that task waits for it, and later trials start from the cached image. Harbor's `--force-build` rebuilds it. See [Build from a Dockerfile](/sailboxes-images#build-from-a-dockerfile) for what a Dockerfile can contain. * A task that ships an `environment/docker-compose.yaml` runs as a Docker Compose project, with the Sailbox as the Docker host: the services' images are pulled or built inside it, commands run in the task's `main` service, and Harbor's per-service operations (exec, download, stop) reach the other services. ## Sizing A task's CPU, memory, and storage requests pick the smallest [Sailbox size](/sailboxes-pricing) that covers them: `s` (1 vCPU), `m` (4 vCPU), or `l` (8 vCPU). A request above the size's default memory or disk ceiling raises that ceiling, up to the size's maximum. A task that declares no resources gets an `s` Sailbox. A task that requests more than 8 CPUs fails up front, since no Sailbox provides more. The size is a ceiling, not a reservation: a Sailbox is billed for the CPU, memory, and disk it actually uses, so a task that requests 8 CPUs and uses one pays for one. ## Cleanup By default, `harbor run` deletes each environment when its trial ends, which terminates the Sailbox. A termination that fails is reported in that trial's result, like any other trial error. With `--no-delete`, Harbor stops each environment without deleting it. The Sailbox goes to sleep with its filesystem and processes intact, and billing stops until it is resumed. Terminate those Sailboxes yourself when you are done with them, with `sail box terminate ` or from the dashboard. A trial whose environment fails to start terminates its Sailbox before the error is reported, even with `--no-delete`. If that termination fails, the trial's normal cleanup stops the Sailbox the same way it would after a completed trial. ## Unsupported task features Harbor checks a task's needs against the environment before starting it, so a task that needs something unsupported fails up front with a clear error instead of running without the feature. Sailboxes do not offer: * GPUs, TPUs, or Windows environments. * IPv6 allowlist entries. A Sailbox does not reach the internet over IPv6. * A no-network or allowlist policy for a Compose task, because bringing the project up pulls images over the network. Both are enforced for a task that does not use Compose. A task with the default public networking is unaffected. The [network policy guide](/sailboxes-network-policy) has the allowlist entry rules. Outside Compose mode, host mount specs are accepted and unused, the same as Harbor's other cloud providers. A Compose task's mounts are bound into its `main` service. # Images Source: https://docs.sailresearch.com/images Send images to multimodal models Sail accepts image inputs on multimodal base models. Images can be supplied as base64 data URIs or as URLs. ## Supported models Multimodal support is per-model, and detailed in the [models](/models) page. Requesting image input on a non-multimodal model returns `400` with `model '' does not support image input`. ## Limits * Maximum of 20 images per request. * Maximum of 20 MB per image. This is the size of the image bytes, not the base64-encoded length. There is no pixel-dimension limit. * Must be a JPEG, PNG, WebP, or GIF. * URL images can use `http://` or `https://` (`https://` recommended) and must be reachable from the public internet. If Sail can't fetch a URL within 10 seconds, the request fails with `400`. ## Responses API Pass an `input_image` block inside a message's `content` array. The `image_url` value can be a data URI or a public URL. ```python theme={null} from openai import OpenAI client = OpenAI(base_url="https://api.sailresearch.com/v1", api_key="YOUR_KEY") response = client.responses.create( model="moonshotai/Kimi-K2.6", input=[ { "role": "user", "content": [ {"type": "input_text", "text": "What's in this image?"}, { "type": "input_image", "image_url": "https://example.com/cat.jpg", }, ], } ], ) print(response.output_text) ``` Equivalent with a base64 data URI: ```python theme={null} import base64, pathlib b64 = base64.b64encode(pathlib.Path("cat.jpg").read_bytes()).decode() data_uri = f"data:image/jpeg;base64,{b64}" response = client.responses.create( model="moonshotai/Kimi-K2.6", input=[ { "role": "user", "content": [ {"type": "input_text", "text": "What's in this image?"}, {"type": "input_image", "image_url": data_uri}, ], } ], ) ``` `detail` (`"auto"`, `"low"`, `"high"`) is supported. ## Chat Completions API Use OpenAI's standard `image_url` content part. ```python theme={null} response = client.chat.completions.create( model="moonshotai/Kimi-K2.6", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, { "type": "image_url", "image_url": { "url": "https://example.com/cat.jpg", "detail": "auto", }, }, ], } ], ) ``` Data URIs are accepted in the same `url` field: ```python theme={null} "image_url": {"url": f"data:image/jpeg;base64,{b64}"} ``` ## Messages API (Anthropic) Use the Anthropic `image` content block. Both `base64` and `url` source types are supported. ```python theme={null} import anthropic client = anthropic.Anthropic( base_url="https://api.sailresearch.com", api_key="YOUR_KEY", ) # URL source response = client.messages.create( model="moonshotai/Kimi-K2.6", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "image", "source": {"type": "url", "url": "https://example.com/cat.jpg"}, }, {"type": "text", "text": "What's in this image?"}, ], } ], ) # Base64 source response = client.messages.create( model="moonshotai/Kimi-K2.6", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": b64, # raw base64 string, no data: prefix }, }, {"type": "text", "text": "What's in this image?"}, ], } ], ) ``` ## Error cases | Condition | Status | Message | | -------------------------------------------------- | ------ | ---------------------------------------------------- | | `model` is not multimodal | 400 | `model '' does not support image input` | | More than 20 images in one request | 400 | `too many images: maximum 20 images per request` | | Image larger than 20 MB decoded | 400 | `image too large: exceeds maximum of ... bytes` | | Unsupported MIME type | 400 | `unsupported image type ...` | | URL scheme not http/https | 400 | `unsupported URL scheme ...` | | URL not reachable from the public internet | 400 | `blocked: ...` | | URL returns non-200 or times out (10 s) | 400 | `failed to download image: ...` | | Data URI declared MIME does not match actual bytes | 400 | `declared type ... does not match detected type ...` | ## Notes * If you already have the image bytes, sending them as a base64 data URI is typically faster than a URL. * Image bytes are not cached across requests. * LoRAs and image inputs can be combined on a multimodal base model that also supports LoRA (see [LoRAs](/loras)). # Overview Source: https://docs.sailresearch.com/index The most cost-efficient serverless inference and agent sandboxes. Sail serves trillions of tokens at unbeatable prices, with support for the best open-source models and your own LoRA fine-tunes. To achieve maximum efficiency for long-horizon agents, we allow you to express latency tolerance with [completion windows](/completion-windows). ```python Python theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.sailresearch.com/v1", api_key="YOUR_SAIL_API_KEY", ) completion = client.chat.completions.create( model="zai-org/GLM-5.3", messages=[{"role": "user", "content": "What are the top 3 things to do in San Francisco?"}], ) print(completion.choices[0].message.content) ``` ```typescript TypeScript theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.sailresearch.com/v1", apiKey: process.env.SAIL_API_KEY, }); const completion = await client.chat.completions.create({ model: "zai-org/GLM-5.3", messages: [ { role: "user", content: "What are the top 3 things to do in San Francisco?", }, ], }); console.log(completion.choices[0].message.content); ``` ```bash cURL theme={null} curl https://api.sailresearch.com/v1/chat/completions \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "messages": [ { "role": "user", "content": "What are the top 3 things to do in San Francisco?" } ] }' ``` ```python Python theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.sailresearch.com/v1", api_key="YOUR_SAIL_API_KEY", ) response = client.responses.create( model="zai-org/GLM-5.3", input="What are the top 3 things to do in San Francisco?", ) print(response.output_text) ``` ```typescript TypeScript theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.sailresearch.com/v1", apiKey: process.env.SAIL_API_KEY, }); const response = await client.responses.create({ model: "zai-org/GLM-5.3", input: "What are the top 3 things to do in San Francisco?", }); console.log(response.output_text); ``` ```bash cURL theme={null} curl https://api.sailresearch.com/v1/responses \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "What are the top 3 things to do in San Francisco?" }' ``` ```python Python theme={null} from anthropic import Anthropic client = Anthropic( base_url="https://api.sailresearch.com", api_key="YOUR_SAIL_API_KEY", ) message = client.messages.create( model="zai-org/GLM-5.3", max_tokens=1024, messages=[{"role": "user", "content": "What are the top 3 things to do in San Francisco?"}], ) for block in message.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript theme={null} import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.sailresearch.com", apiKey: process.env.SAIL_API_KEY, }); const message = await client.messages.create({ model: "zai-org/GLM-5.3", max_tokens: 1024, messages: [ { role: "user", content: "What are the top 3 things to do in San Francisco?", }, ], }); for (const block of message.content) { if (block.type === "text") console.log(block.text); } ``` ```bash cURL theme={null} curl https://api.sailresearch.com/v1/messages \ -H "x-api-key: $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "max_tokens": 1024, "messages": [ { "role": "user", "content": "What are the top 3 things to do in San Francisco?" } ] }' ``` ## Intelligence at scale More agents thinking longer and harder, with space to act and explore, can do incredible things:
  • Detail uses Sail inference to deeply scan codebases for their most consequential yet hard-to-catch bugs
  • Jack & Jill runs large-scale deep research with Sail inference, matching job seekers' resumes with job descriptions from thousands of employers
  • We won Browsecomp-Plus , the AI deep research benchmark, using open models running on Sail inference
  • We built Redis in Rust with a swarm of 4 long-horizon coding agents running on Sailboxes with Sail inference over 27 hours
## Security & privacy Sail has Zero Data Retention (ZDR) by default, is HIPAA and SOC 2-compliant, and does not train on any customer data without consent. Read more about our security and privacy commitments [here](/security), or visit our Trust Center here. # LoRAs Source: https://docs.sailresearch.com/loras Bring your own PEFT-trained LoRA adapters and run them on supported models Upload PEFT-trained LoRAs for supported base models. After upload, give the LoRA a name and pass that name or ID in request metadata. If you train LoRAs with Tinker, you can also sample directly from Tinker checkpoints without uploading. See [Tinker](/tinker). ## Supported base models LoRA serving is available for: | Base model | Max rank | Target modules | | ---------------------- | -------- | -------------- | | `moonshotai/Kimi-K2.6` | 32 | all | If a LoRA is incompatible with the base model or exceeds the rank limit, validation or inference fails. ## Adapter requirements Train with [PEFT](https://huggingface.co/docs/peft) and export the two standard files: * `adapter_config.json` * `adapter_model.safetensors` Use adapter config values compatible with the base model: * **`base_model_name_or_path`** should identify the base model you register in `supported_models` (e.g. `moonshotai/Kimi-K2.6`). * **`peft_type`** should be `"LORA"`. * **`task_type`** should be `"CAUSAL_LM"`. * **`r`** (rank) must be no more than the base model's max rank. * **`target_modules`** can include any modules supported by the base model and runtime. Sail does not restrict Kimi K2.6 LoRAs to a fixed target-module allowlist. Other adapter-config fields (`lora_alpha`, `lora_dropout`, bias settings, etc.) are preserved as-is. The `adapter_config.json` and `adapter_model.safetensors` files can each be up to 100 GiB. ## Add a LoRA Create a LoRA in three steps: upload the config file, upload the weights file, then call `POST /v1/loras`. The response includes validation records for each requested model. ### 1. Upload the two adapter files Use `POST /v1/files` (multipart): ```python theme={null} from openai import OpenAI client = OpenAI(base_url="https://api.sailresearch.com/v1", api_key="YOUR_KEY") with open("my-adapter/adapter_config.json", "rb") as f: cfg = client.files.create(file=f, purpose="lora") with open("my-adapter/adapter_model.safetensors", "rb") as f: wts = client.files.create(file=f, purpose="lora") ``` ### 2. Create the LoRA ```python theme={null} import requests resp = requests.post( "https://api.sailresearch.com/v1/loras", headers={"Authorization": "Bearer YOUR_KEY"}, json={ "name": "funnier-v1", "supported_models": ["moonshotai/Kimi-K2.6"], "config_file_id": cfg.id, "weights_file_id": wts.id, # optional "display_name": "Funnier v1", "description": "Fine-tuned on a standup-comedy corpus to write punchier, joke-forward replies.", }, timeout=30, ) resp.raise_for_status() lora = resp.json() print(lora["id"], lora["status"]) # e.g. 3fa85f64-... pending_validation ``` Each `supported_models` entry must be a known Sail model ID. Naming rules for the `name` field: * 2–64 characters * lowercase alphanumeric or dashes (`[a-z0-9-]`) * must start and end with an alphanumeric character * unique within your organization (duplicate → `409`) The file IDs you pass must belong to the same organization as your API key. ### Validation flow `POST /v1/loras` creates one validation record per `supported_models` entry. Each record appears in the response under `validations`: ```json theme={null} { "id": "3fa85f64-...", "name": "funnier-v1", "status": "pending_validation", "supported_models": ["moonshotai/Kimi-K2.6"], "validations": [ { "model": "moonshotai/Kimi-K2.6", "status": "pending", "response_id": "resp_..." } ] } ``` Sail validates the LoRA on each model in `supported_models`. Poll `GET /v1/loras/{name}` until validation finishes for the model you want to use. | Validation status | Meaning | | ----------------- | ----------------------------------------------------------------------------------------------- | | `pending` | The validation task has been created. | | `running` | The validation request is in progress. | | `succeeded` | The LoRA loaded and completed a validation request for that model. | | `failed` | The LoRA is not usable for that model. `result_code` and `result_message` describe the failure. | | `unverified` | Sail could not complete validation automatically. | Read `validations` for model-specific status. Requests using a LoRA are rejected only when the latest validation for that model is `failed`; `pending`, `running`, and `unverified` records remain usable. If you later add a model with `PATCH /v1/loras/{name}`, Sail creates validation records for newly added models that have not already succeeded validation. ### 3. Fetch LoRAs ```bash theme={null} # by name curl -H "Authorization: Bearer $SAIL_API_KEY" https://api.sailresearch.com/v1/loras/funnier-v1 # by id curl -H "Authorization: Bearer $SAIL_API_KEY" https://api.sailresearch.com/v1/loras/3fa85f64-... # list all loras for your org curl -H "Authorization: Bearer $SAIL_API_KEY" https://api.sailresearch.com/v1/loras ``` ## Use a LoRA Pass the LoRA's name (or its UUID) as `metadata.lora` on any Responses, Chat Completions, or Messages request. `model` must be one of the LoRA's `supported_models`: ```python theme={null} response = client.responses.create( model="moonshotai/Kimi-K2.6", input=[{"role": "user", "content": "Write a one-liner about a centrifuge that's having a bad day."}], metadata={ "lora": "funnier-v1", "completion_window": "balanced", }, background=True, ) ``` Chat Completions: ```python theme={null} response = client.chat.completions.create( model="moonshotai/Kimi-K2.6", messages=[{"role": "user", "content": "Tell me a joke about Go channels."}], extra_body={"metadata": {"lora": "funnier-v1", "completion_window": "balanced"}}, ) ``` ### Constraints on LoRA requests * **Use the `balanced` or `flex` completion window for Kimi K2.6 LoRA requests.** `SailTokenCompleter` uses `balanced` by default. `asap` is not available for LoRA requests. For more on completion windows, see [Completion Windows](/completion-windows). * **`model` must be in the LoRA's `supported_models` list.** Requesting a different base model returns `400`. * **A failed model validation blocks that model.** `GET /v1/loras/{name}` includes `validations[].result_message` when validation fails. * **The LoRA must belong to your organization.** Names are scoped per-org; two orgs can independently own a LoRA called `funnier-v1`. * **You can use either the LoRA's name or its UUID in `metadata.lora`.** # Connect the Docs MCP server Source: https://docs.sailresearch.com/mcp-server Connect your agents to Sail's documentation over MCP. ## Install ```bash theme={null} claude mcp add --transport http sail-docs https://docs.sailresearch.com/mcp ``` Or add it to `.mcp.json` in your project: ```json theme={null} { "mcpServers": { "sail-docs": { "url": "https://docs.sailresearch.com/mcp" } } } ``` ```bash theme={null} codex mcp add sail-docs --url https://docs.sailresearch.com/mcp ``` [Install in Cursor](https://cursor.com/en/install-mcp?name=sail-docs\&config=eyJ1cmwiOiJodHRwczovL2RvY3Muc2FpbHJlc2VhcmNoLmNvbS9tY3AifQ%3D%3D) (one click), or add it to `.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "sail-docs": { "url": "https://docs.sailresearch.com/mcp" } } } ``` Add it to `.vscode/mcp.json`: ```json theme={null} { "servers": { "sail-docs": { "type": "http", "url": "https://docs.sailresearch.com/mcp" } } } ``` Settings → Connectors → Add custom connector, then paste `https://docs.sailresearch.com/mcp`. Where your workspace allows Developer mode and custom connectors, go to Settings → Apps & Connectors → Developer mode → Add new connector, then paste `https://docs.sailresearch.com/mcp`. Any MCP client that supports streamable HTTP works. Point it at `https://docs.sailresearch.com/mcp`. ## Try it Once connected, ask your agent things like: * "Which Sail models support tool calling, and what do they cost?" * "What completion window should I use for an overnight batch job?" * "Set up a Sailbox for my agent and exec a command in it." ## Workflow skills The MCP server answers questions about Sail. To give your agent step-by-step workflows on top of it, such as migrating an existing app to Sail or building an observable agent, install the skills from the [AI Quickstart](/ai-quickstart). # Migrate to the Sail API Source: https://docs.sailresearch.com/migrate Drop-in migration from OpenAI and Anthropic-compatible providers to Sail. Sail is a drop-in replacement for OpenAI and Anthropic-compatible inference providers, supporting the OpenAI Responses (`/v1/responses`), OpenAI Chat Completions (`/v1/chat/completions`), and Anthropic Messages (`/v1/messages`) APIs. Switching from OpenAI, Anthropic, or any compatible provider is just a configuration change.
Want an agent to do it? Copy a migration prompt, paste it into your coding agent, and then use the guide below to review the changes.
Using Claude Code or Codex? Install the [Sail skills](/ai-quickstart) instead and ask your agent to "Migrate this app to Sail". The `sail-migrate` skill guides the full migration, including moving sandboxed execution to Sail.
```text Migration prompt theme={null} Migrate this project's LLM inference to Sail (https://sailresearch.com). Sail docs to consult as you work: - MCP server: https://docs.sailresearch.com/mcp (connect to it if you support MCP) - Full docs as plain text: https://docs.sailresearch.com/llms-full.txt - Key pages: https://docs.sailresearch.com/models (catalog), https://docs.sailresearch.com/pricing (per-window rates), https://docs.sailresearch.com/completion-windows, https://docs.sailresearch.com/support (API feature matrix) Sail is drop-in compatible with the OpenAI Responses and Chat Completions APIs and the Anthropic Messages API, all served from https://api.sailresearch.com. Keep whichever request shape this code already uses. Sail's Messages API supports system prompts, tool calling, and streaming for agentic use; one caveat is that prompt caching (`cache_control`) is accepted but not yet applied (see /support), so if an Anthropic call site relies on cache hits, expect full-price reads until that lands. The exact base URL differs by SDK (see step 4). If you can ask the user questions, ask whenever a step below is ambiguous instead of guessing. If you can't, make the best-supported choice and flag it in your final report. 1. Survey the current setup. Find every place this project calls an LLM: SDK clients, raw HTTP calls, framework configs, env vars, and docs. For each call site record the provider, API shape, model, and features used (streaming, tool calls, structured outputs, images). 2. Choose replacement model(s). For each model currently in use, pick the closest match from https://docs.sailresearch.com/models, comparing capability tags, context window, and what the model is known to be good at. If a current model is not one Sail serves, research it (web search if available) to understand its strengths before choosing. If multiple Sail models are plausible, ask the user. Otherwise pick the best fit and explain the choice in your report. Check the /support page for any features this code uses that Sail doesn't serve, and flag them. 3. Choose a completion window per call site. Omitting completion_window gives low-latency inference by default, at lower prices than traditional inference providers for many models. Workloads that can wait save more with balanced or flex. The live https://docs.sailresearch.com/completion-windows and https://docs.sailresearch.com/pricing pages are the source of truth for public windows, token prices, and scheduling behavior. If you can't fetch them, use this summary and decide how long each workload can wait: - asap: low-latency serving that is cheaper than traditional inference providers - balanced: more tokens per dollar for autonomous agents and pipelines; use it for workloads that can tolerate more latency - flex: the lowest prices for batch jobs, evals, and offline processing; it has no latency target and requires background=True on the Responses API If the workload's latency tolerance isn't obvious from the code, ask the user. Omit metadata.completion_window when the default low-latency behavior is acceptable. Set it explicitly for balanced or flex, or pin asap when the request must fail instead of using a fallback window. Confirm the chosen window is available for the chosen model on https://docs.sailresearch.com/pricing. Some models are flex-only. 4. Make the changes wherever the client is configured or called: - base URL: use https://api.sailresearch.com/v1 for OpenAI-compatible clients (Responses and Chat Completions). For the Anthropic SDK, use the bare host https://api.sailresearch.com (e.g. set ANTHROPIC_BASE_URL=https://api.sailresearch.com). The SDK appends /v1/messages itself, so a /v1 base URL would resolve to /v1/v1/messages and 404 - API key: read from the SAIL_API_KEY environment variable. Never hardcode a key or paste a literal key value into the code (the Anthropic SDK can pass it as `api_key`; `auth_token` also works) - model: the Sail model(s) chosen in step 2 - metadata.completion_window: the window(s) chosen in step 3 - add background=True for flex or very long-running requests Update env var names, .env.example files, config templates, and any README/docs references. Do not change prompts, tools, or business logic. 5. Estimate the savings. Compare the published per-1M-token list prices (input, cached input, and output) of the previous model(s) against the chosen Sail model(s) at the chosen completion window(s) from https://docs.sailresearch.com/pricing. Research current provider list prices if you don't know them. State the comparison as a simple table and an approximate overall multiplier (e.g. "roughly 6x cheaper per token"). Do not present this as a precise bill forecast. 6. Verify. Run the project's tests. Then make one real smoke request through the new configuration: if SAIL_API_KEY is already set, use it; if not, walk the user through creating a key at https://app.sailresearch.com/api-keys and setting SAIL_API_KEY, then run the smoke request once they have. Don't ask them to paste the key to you. Have them export it in their own shell. Finish with a short migration report: call sites changed; model mapping with rationale; completion window(s) with rationale; the price comparison from step 5; and anything that needs human follow-up (unsupported features, ambiguous choices, untested paths). Close by telling the user exactly where to set SAIL_API_KEY for their setup (locally and in their production/deployment environment) so the migrated code can authenticate. ```
## 1. Get your API key Sign up at the [Sail dashboard](https://app.sailresearch.com/api-keys) and create an API key. Export it where your agent and app can read it: ```bash theme={null} export SAIL_API_KEY="YOUR_SAIL_API_KEY" ``` ## 2. See what changes Already calling the OpenAI Responses API? The request and response are identical: ```python Sail Responses API theme={null} import os from openai import OpenAI client = OpenAI( base_url="https://api.your-provider.com/v1", # [!code --] base_url="https://api.sailresearch.com/v1", # [!code ++] api_key=os.environ["PROVIDER_API_KEY"], # [!code --] api_key=os.environ["SAIL_API_KEY"], # [!code ++] ) response = client.responses.create( model="", # [!code --] model="", # [!code ++] input="Explain the key ideas behind transformers.", ) print(response.output_text) ``` Already calling the Anthropic Messages API? Keep the Anthropic SDK and point it at the bare Sail host. The SDK appends `/v1/messages` itself: ```python Sail Messages API theme={null} import os from anthropic import Anthropic client = Anthropic( base_url="https://api.sailresearch.com", # [!code ++] api_key=os.environ["ANTHROPIC_API_KEY"], # [!code --] api_key=os.environ["SAIL_API_KEY"], # [!code ++] ) message = client.messages.create( model="", # [!code --] model="", # [!code ++] max_tokens=1024, messages=[{"role": "user", "content": "Explain the key ideas behind transformers."}], ) ``` ## Notes * **Synchronous by default.** `responses.create` blocks and returns the completed response, exactly like OpenAI. For long-running work, pass `background=True` to get an ID back immediately and poll, avoiding HTTP timeouts. See the [Quickstart](/quickstart). * **Pick a completion window** for each call site. Omit `completion_window` for the default low-latency behavior, which pairs low latency with Sail's cost efficiency. `balanced` buys more tokens per dollar for autonomous work, and `flex` offers the lowest prices for background batches. See [Completion windows](/completion-windows). * **Messages API caveat.** System prompts, tool calling, and streaming are supported. Prompt caching (`cache_control`) is accepted but not yet applied, so expect full-price input reads for now. See the [API support matrix](/support). ## Next steps Set your coding agent up with Sail's docs and skills. Make your first request against Sail. Browse supported models and pick a replacement. How the latency-for-price tradeoff works. Per-token rates by model and completion window. Estimate the cost of running your agent on Sail vs traditional inference providers. Email us if you hit anything unexpected. # Models Source: https://docs.sailresearch.com/models All models currently served by Sail ## Core models
Model Slug Image LoRA Reasoning
Kimi K3
Moonshot AI
Context 1M
Params 2.8T; 104B active (MoE)
Popular for
coding agentic long context
moonshotai/Kimi-K3
GLM-5.3
Z.ai
Context 1M
Params 753B; 40B active (MoE)
Popular for
coding agentic multilingual
zai-org/GLM-5.3
Setting reasoning effort to none returns HTTP 400. Use low for GLM-5.3's lowest native reasoning tier.
DeepSeek V4 Flash 0731
DeepSeek
Context 1M
Params 284B; 13B active (MoE)
Popular for
coding agentic long context
deepseek-ai/DeepSeek-V4-Flash-0731
Kimi-K2.6
Moonshot AI
Context 262K
Params 1T; 32B active (MoE)
Popular for
coding agentic vision long context
moonshotai/Kimi-K2.6
DeepSeek V4 Pro 0813
DeepSeek
Context 1M
Params 1.65T; 49B active (MoE)
Popular for
coding agentic math long context
deepseek-ai/DeepSeek-V4-Pro-0813
gpt-oss-120b
OpenAI
Context 131K
Params 117B; 5.1B active (MoE)
Popular for
coding math cost-efficiency
openai/gpt-oss-120b
Gemma 4 31B IT
Google
Context 256K
Params 31B
Popular for
vision multilingual general chat
google/gemma-4-31B-it
Gemma 4 12B IT
Google
Context 16K
Params 12B
Popular for
multilingual general chat
google/gemma-4-12B-it
Gemma 4 31B IT (NVFP4)
Google
Context 262K
Params 31B
Popular for
vision multilingual general chat
nvidia/Gemma-4-31B-IT-NVFP4
## Flex-only models Models served with the [`flex`](/completion-windows#flex) completion window exclusively.
Model Slug Image LoRA Reasoning
Qwen3.6 35B A3B
Qwen
Context 262K
Params 35B; 3B active (MoE)
Popular for
coding agentic vision multilingual
Qwen/Qwen3.6-35B-A3B
Nemotron 3 Super 120B A12B BF16
NVIDIA
Context 1M
Params 120B; 12B active (MoE)
Popular for
coding agentic multilingual long context general chat
nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16
## Notes * Each row links the exact Hugging Face checkpoint Sail currently serves. If we offer multiple quantizations, we list them as separate model IDs. * This table and [`GET /v1/models`](/api-reference/models-api/list-supported-models) return canonical model IDs only. Sail may continue accepting an older ID as a compatibility alias, but new requests should use the canonical slug shown here. * Use [`GET /v1/models`](/api-reference/models-api/list-supported-models) to confirm runtime availability for your API key. * For per-model rates by completion window, see [Pricing](/pricing). # Using OpenCode with Sail Source: https://docs.sailresearch.com/opencode Use OpenCode with Sail as your LLM provider. [OpenCode](https://opencode.ai) is an open-source coding agent. Sail is OpenAI-compatible, so plugging it in is just a config change. ## Install Follow the install instructions at [opencode.ai](https://opencode.ai). The most common path is: ```bash theme={null} curl -fsSL https://opencode.ai/install | bash ``` ## Get your API key Sign up for [Sail](https://app.sailresearch.com) and create an API key. ## Configure Create your OpenCode config with Sail as a provider: ```bash theme={null} mkdir -p ~/.config/opencode ``` Then put this in `~/.config/opencode/opencode.jsonc`: ```jsonc theme={null} { "$schema": "https://opencode.ai/config.json", "provider": { "Sail": { "npm": "@ai-sdk/openai-compatible", "options": { "baseURL": "https://api.sailresearch.com/v1", "apiKey": "YOUR_SAIL_API_KEY", }, "models": { "glm-5.3": { "id": "zai-org/GLM-5.3", "name": "GLM-5.3", "variants": { "minimal": { "reasoningEffort": "minimal" }, "high": { "reasoningEffort": "high" }, "xhigh": { "reasoningEffort": "xhigh" }, }, }, "glm-5.3-balanced": { "id": "zai-org/GLM-5.3", "name": "GLM-5.3 (balanced)", "options": { "metadata": { "completion_window": "balanced" } }, "variants": { "minimal": { "reasoningEffort": "minimal" }, "high": { "reasoningEffort": "high" }, "xhigh": { "reasoningEffort": "xhigh" }, }, }, "glm-5.3-flex": { "id": "zai-org/GLM-5.3", "name": "GLM-5.3 (flex)", "options": { "metadata": { "completion_window": "flex" } }, "variants": { "minimal": { "reasoningEffort": "minimal" }, "high": { "reasoningEffort": "high" }, "xhigh": { "reasoningEffort": "xhigh" }, }, }, }, }, }, } ``` Replace `YOUR_SAIL_API_KEY` with the key you created above. ## Run ```bash theme={null} opencode ``` Once OpenCode is up, run `/models` and search "Sail" to choose a GLM-5.3 completion window. See [Completion windows](/completion-windows) for latency and cost tradeoffs. ## Control reasoning The GLM-5.3 entries above define `minimal`, `high`, and `xhigh` reasoning variants. Press `Ctrl+T` in the OpenCode terminal UI to switch variants. The selected variant sets Sail's reasoning effort until you switch again. Run `/thinking` to show the reasoning blocks returned by the model: ```text theme={null} /thinking ``` `/thinking` only controls whether OpenCode displays those blocks. It does not change the model's reasoning effort. See OpenCode's [reasoning variant](https://opencode.ai/docs/models/#variants) and [`/thinking`](https://opencode.ai/docs/tui/#thinking) documentation for more detail. GLM-5.3 accepts `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. `max` selects the same top tier as `xhigh`. See the [API support matrix](/support) for the request and streaming response fields. The example includes the minimal, high, and maximum settings. You can add the other effort levels as variants if you need them. To make high reasoning the model default, add `reasoningEffort` to the model's `options`: ```jsonc theme={null} "options": { "reasoningEffort": "high", }, ``` OpenCode uses the camelCase `reasoningEffort` config key and sends it to Sail as `reasoning_effort`. For a non-interactive request, select the variant and show its reasoning with: ```bash theme={null} opencode run \ --model Sail/glm-5.3 \ --variant high \ --thinking \ "Explain this code." ``` ## Tips * **What's next:** see OpenCode's [Usage guide](https://opencode.ai/docs/#usage). * **New models:** add any other model from Sail's [model catalog](/models) under `provider.Sail.models`. # Pricing Source: https://docs.sailresearch.com/pricing Per-token pricing for Sail inference ## Core models
USD · per 1M tokens
Model Window Input Cached Output
Kimi K3
moonshotai/Kimi-K3
Default (ASAP) \$ 3.00 \$ 0.30 \$ 15.00
GLM-5.3
zai-org/GLM-5.3
Default (ASAP) \$ 1.40 \$ 0.26 \$ 4.40
Balanced \$ 0.50 \$ 0.12 \$ 2.50
Flex \$ 0.40 \$ 0.08 \$ 1.80
DeepSeek V4 Flash 0731
deepseek-ai/DeepSeek-V4-Flash-0731
Default (ASAP) \$ 0.09 \$ 0.02 \$ 0.18
Kimi-K2.6
moonshotai/Kimi-K2.6
Default (ASAP) \$ 1.00 \$ 0.20 \$ 4.00
Balanced \$ 0.45 \$ 0.20 \$ 3.00
Flex \$ 0.35 \$ 0.10 \$ 2.00
DeepSeek V4 Pro 0813
deepseek-ai/DeepSeek-V4-Pro-0813
Default (ASAP) \$ 1.32 \$ 0.044 \$ 3.96
Flex \$ 0.66 \$ 0.022 \$ 1.98
gpt-oss-120b
openai/gpt-oss-120b
Default (ASAP) \$ 0.06 \$ 0.03 \$ 0.40
Gemma 4 31B IT
google/gemma-4-31B-it
Default (ASAP) \$ 0.40 \$ 0.20 \$ 0.60
Balanced \$ 0.12 \$ 0.08 \$ 0.60
Flex \$ 0.06 \$ 0.02 \$ 0.30
Gemma 4 12B IT
google/gemma-4-12B-it
Default (ASAP) \$ 0.30 \$ 0.15 \$ 2.00
Balanced \$ 0.10 \$ 0.07 \$ 2.00
Flex \$ 0.05 \$ 0.02 \$ 1.00
Gemma 4 31B IT (NVFP4)
nvidia/Gemma-4-31B-IT-NVFP4
Default (ASAP) \$ 0.14 \$ 0.07 \$ 0.40
Balanced \$ 0.07 \$ 0.05 \$ 0.40
## Flex-only models Models served with the [`flex`](/completion-windows#flex) completion window exclusively.
USD · per 1M tokens
Model Window Input Cached Output
Qwen3.6 35B A3B
Qwen/Qwen3.6-35B-A3B
Flex \$ 0.05 \$ 0.015 \$ 0.40
Nemotron 3 Super 120B A12B BF16
nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16
Flex \$ 0.25 \$ 0.15 \$ 0.75
## Notes * See [Completion Windows](/completion-windows) for how to use `balanced` and `flex` for lower token prices. * Not all core models support all windows yet. We regularly bring up new models and expand completion window support for existing ones based on demand. If you have a need that's not represented above, get in touch. * Prompt caching is implicit, based on prefix matching. Optionally, you may use [`prompt_cache_key`](/api-reference/responses-api/create-a-response#body-prompt-cache-key) as a routing hint to help maximize cache hit rates. * See [Models](/models) for capabilities and other details on supported models. * To see what these rates add up to on a full agent workload, use the [agent cost calculator](/cost-calculator). # Quickstart Source: https://docs.sailresearch.com/quickstart Start using Sail with OpenAI or Anthropic clients Sail supports the OpenAI Chat Completions (`/v1/chat/completions`), OpenAI Responses (`/v1/responses`), and Anthropic Messages (`/v1/messages`) APIs; see the [API support matrix](/support) for the full picture. If you are already using these APIs, switching to Sail is just a base URL and API key change. ## 1. Get your API key Sign up at the [Sail dashboard](https://app.sailresearch.com) and create an API key. ## 2. Make a request Point the SDK you already use at Sail. The OpenAI SDK works with the Chat Completions and Responses APIs, and the Anthropic SDK works with the Messages API. ```python Python theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.sailresearch.com/v1", api_key="YOUR_SAIL_API_KEY", ) completion = client.chat.completions.create( model="zai-org/GLM-5.3", messages=[{"role": "user", "content": "Explain the key ideas behind transformers."}], ) print(completion.choices[0].message.content) ``` ```typescript TypeScript theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.sailresearch.com/v1", apiKey: process.env.SAIL_API_KEY, }); const completion = await client.chat.completions.create({ model: "zai-org/GLM-5.3", messages: [ { role: "user", content: "Explain the key ideas behind transformers.", }, ], }); console.log(completion.choices[0].message.content); ``` ```bash cURL theme={null} URL=https://api.sailresearch.com/v1/chat/completions AUTH="Authorization: Bearer YOUR_SAIL_API_KEY" curl -s $URL \ -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "messages": [ { "role": "user", "content": "Explain the key ideas behind transformers." } ] }' | jq ``` ```python Python theme={null} from openai import OpenAI client = OpenAI( base_url="https://api.sailresearch.com/v1", api_key="YOUR_SAIL_API_KEY", ) response = client.responses.create( model="zai-org/GLM-5.3", input="Explain the key ideas behind transformers.", max_output_tokens=1000, ) # Incomplete responses still expose their partial output. print(response.output_text) ``` ```typescript TypeScript theme={null} import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.sailresearch.com/v1", apiKey: process.env.SAIL_API_KEY, }); const response = await client.responses.create({ model: "zai-org/GLM-5.3", input: "Explain the key ideas behind transformers.", max_output_tokens: 1000, }); // Incomplete responses still expose their partial output. console.log(response.output_text); ``` ```bash cURL theme={null} URL=https://api.sailresearch.com/v1/responses AUTH="Authorization: Bearer YOUR_SAIL_API_KEY" curl -s $URL \ -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "Explain the key ideas behind transformers.", "max_output_tokens": 1000 }' | jq ``` ```python Python theme={null} from anthropic import Anthropic client = Anthropic( base_url="https://api.sailresearch.com", api_key="YOUR_SAIL_API_KEY", ) message = client.messages.create( model="zai-org/GLM-5.3", max_tokens=1000, messages=[{"role": "user", "content": "Explain the key ideas behind transformers."}], ) # Reasoning models can return a thinking block first, so select text by type. for block in message.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript theme={null} import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.sailresearch.com", apiKey: process.env.SAIL_API_KEY, }); const message = await client.messages.create({ model: "zai-org/GLM-5.3", max_tokens: 1000, messages: [ { role: "user", content: "Explain the key ideas behind transformers.", }, ], }); // Reasoning models can return a thinking block first, so select text by type. for (const block of message.content) { if (block.type === "text") console.log(block.text); } ``` ```bash cURL theme={null} URL=https://api.sailresearch.com/v1/messages AUTH="x-api-key: YOUR_SAIL_API_KEY" curl -s $URL \ -H "$AUTH" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "max_tokens": 1000, "messages": [ { "role": "user", "content": "Explain the key ideas behind transformers." } ] }' | jq ``` ## Next steps * Look at our full list of supported [models](/models) and [pricing](/pricing). * Check the [API support matrix](/support) for what each API supports today, and browse the full [API reference](/api-reference) for endpoints and request fields. * Using a coding agent? Connect it to the [Sail docs MCP server](/mcp-server) to give it a live connection to these docs. * [Email us](mailto:support@sailresearch.com) if you have questions. # Rate limits Source: https://docs.sailresearch.com/rate-limits Rate limits for inference requests Sail applies input-token rate limits to individual inference requests, separately for each public model and organization. Batch API requests are not included. If your organization needs higher limits, [contact support](mailto:support@sailresearch.com). ## How limits work Each rate-limited model has a limit for your organization and a shared global limit. Each holds at most one minute of its configured token allowance and refills continuously. For each request subject to these limits, Sail counts input tokens before inference starts: * Cached input reduces the count using the larger of a known Supercache read and Sail's prefix-cache estimate. The two values are not added together. * A Supercache write counts its full input before any completion-window discount. It does not receive a cache-read deduction. * A configured completion-window discount reduces the remaining count, rounded up to a whole token. Windows share the same limits for the model and your organization. * Final inference usage does not revise the rate-limit count. ## When a request is limited Sail returns: * `429` when your organization reaches its limit for the model. * `529` when the model reaches its global limit. Both responses include `Retry-After` in seconds. If retrying, wait at least that long. This does not guarantee that the next attempt will succeed. A rejected request does not start inference. If you sent an idempotency key, reuse it when retrying. See [Idempotency](/idempotency) for examples. ### Requests larger than the token allowance A request whose counted input exceeds either limit's entire one-minute allowance cannot pass that limit, even when it is full. Sail returns `Retry-After: 60` for this case, but waiting alone does not make the request eligible. Reduce its counted input or contact support about the limit. ## Temporary model availability Some models can be temporarily unavailable even when you have not reached a rate limit. For non-streaming requests and errors detected before a stream begins, Sail returns `503` with a `Retry-After` header. OpenAI-compatible endpoints also return error code `model_capacity_unavailable`. This response has no reset window. Wait at least the stated time, then retry with backoff. The rejected request does not start inference. # CLI Source: https://docs.sailresearch.com/reference/cli Install the sail command-line tool, plus every command grouped by area The `sail` CLI manages Sailboxes and apps from the terminal. It is a single native binary with no runtime dependencies. Run `sail --help` or `sail --help` for the same information at the prompt, and see the [Sailboxes guide](/sailboxes) for what you can do with it. ## Install ```bash theme={null} curl -fsSL https://cli.sailresearch.com/install.sh | sh ``` Installs the latest `sail` into `~/.sail/bin`. If that directory is not on your `PATH`, the installer adds it to your shell startup files and prints the line to run in the current shell. ```powershell theme={null} irm https://cli.sailresearch.com/install.ps1 | iex ``` Installs `sail.exe` into `%LOCALAPPDATA%\sail\bin` and puts that directory at the front of your user `PATH`. Restart your shell afterward. ```bash theme={null} pip install sail ``` The Python SDK ships the CLI: `sail` is on your `PATH` in that environment, and it stays current with `pip install -U sail`. To pin a version, set `SAIL_CLI_VERSION` for the installer, substituting the release you want for `X.Y.Z`: ```bash theme={null} curl -fsSL https://cli.sailresearch.com/install.sh | SAIL_CLI_VERSION=X.Y.Z sh ``` `SAIL_HOME` relocates sail's home directory, and `SAIL_INSTALL_DIR` installs a copy at an exact path for provisioning scripts. ### Update ```bash theme={null} sail update ``` Downloads the latest release and replaces the binary you ran. Sail warns when your CLI version is nearing the end of its support window, and rejects one past it with an update error before any operation runs. Upgrading a Sailbox is a different operation: `sail box upgrade `. ## Interactive shell For interactive use, `sail shell` is usually the most convenient option. It opens a REPL on your machine that accepts every command below without the leading `sail` (so `box list`, `box create ...`), and adds touches the one-shot commands do not: line editing and history, a picker menu when you omit a Sailbox id, and confirmation prompts. ```bash theme={null} sail shell ``` The individual `sail ` subcommands take their arguments up front, which suits scripts and agents (add `--json` for machine-readable output). Mind the naming: `sail shell` is a shell for *managing* Sailboxes from your machine. It is not a shell *inside* a box. To open a shell inside a running Sailbox, use `sail box shell` (below). ## Authentication ```bash theme={null} sail auth login [--api-key ] # log in via the browser, or store a key with --api-key (or piped to stdin) sail auth whoami # show the active key sail auth logout # remove the stored key ``` Every command reads `SAIL_API_KEY`, falling back to the credential stored by `sail auth login`. Add `--json` to any command for machine-readable output. See [Configuration](/reference/sdk-configuration). ## Apps ```bash theme={null} sail app find # find an app by name sail app create # create an app (returns the existing one if present) sail app list # list apps in the current org ``` ## Sailbox lifecycle | Command | Description | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `sail box show ` | Show a single Sailbox. | | `sail box top` | Live top-style view of Sailbox usage. | | `sail box list` | List Sailboxes in the current org (see flags below). | | `sail box terminate ` | Permanently terminate a Sailbox. | | `sail box sleep ` | Checkpoint and release compute (`--wake-at` to schedule a wake). | | `sail box pause ` | Freeze in place. | | `sail box resume ` | Resume a paused or sleeping Sailbox. | | `sail box checkpoint ` | Checkpoint a running Sailbox (`--name`, `--ttl-seconds`). | | `sail box from-checkpoint --name ` | Create a new Sailbox from a checkpoint, called `name`. | | `sail box upgrade ` | Upgrade the runtime (now if running, else at next wake). | | `sail box auto-sleep ` | Change when Sail may sleep it: `never`, `auto`, or 1 through 3600 idle seconds (`0` is the same as `auto`; other numeric values are rejected). | ### `sail box create` ```bash theme={null} sail box create --app --name [options] ``` | Option | Description | | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--app ` | App name (created if missing). Required. | | `--name ` | Sailbox name within the app. Required. | | `--arch ` | Base image architecture (default `amd`). | | `--port ` | HTTP ingress port to expose. Repeatable. | | `--size ` | Resource size (default `m`): `s` = 1 vCPU, 16 GiB memory, 32 GiB disk; `m` = 4 vCPU, 32 GiB memory, 128 GiB disk; `l` = 8 vCPU, 64 GiB memory, 256 GiB disk. Ongoing billing is by usage; each size also has a one-time creation charge. `s` gives the fastest cold starts and resumes. | | `--memory-limit-gib ` | Memory ceiling in whole GiB, within the size's range: 2-64 for `s`, 8-128 for `m`, 16-256 for `l`. The size's default when omitted. | | `--disk-limit-gib ` | Disk ceiling in whole GiB, within the size's range: 8-128 for `s`, 32-512 for `m`, 64-1024 for `l`. The size's default when omitted. | | `--visibility ` | Who may operate the Sailbox: `org` (the default) lets anyone in your org; `private` restricts all access to you (creator-only) and requires a user-minted API key. An org admin can override some operations on a private Sailbox with a recorded reason. | | `--enable-ssh` | Expose port 22, trust your org's CA, and start sshd. | | `--identity-file ` | Local SSH key to authenticate with (implies `--enable-ssh`). | | `--auto-sleep ` | When Sail may sleep it on its own: `auto` (sleep when fully idle, wake the moment anything needs it; the default), `never` (stays running unless you stop it yourself), or a whole number of idle seconds from 1 through 3600 that replaces the default (`0` is the same as `auto`; other numeric values are rejected). | | `--no-network` | Create the Sailbox cut off from other hosts and the internet: no outbound connections, no name resolution, and no exposed ingress or SSH. Running commands is unaffected (`sail box exec` and `sail box shell` reach the Sailbox over a Sail-internal path, not its network), and mounted volumes still work. | | `--allow-host ` | Allow outbound access only to this destination (repeatable, up to 128): a hostname, a `*.` wildcard hostname, an IPv4 address, or an IPv4 range such as `203.0.113.0/24`. Only connections the Sailbox opens are limited, so `--enable-ssh` and `--port` still work. Conflicts with `--no-network`. See [Network policy](/sailboxes-network-policy) for the entry rules and what each entry allows. | ### `sail box list` | Option | Description | | ------------------- | ---------------------------- | | `--app ` | Filter by app name. | | `--status ` | Filter by status. | | `--search ` | Filter by id/name substring. | | `--limit ` | Maximum rows. | | `--offset ` | Rows to skip. | ### `sail box top` | Option | Description | | --------------------- | ------------------------------------------------------------- | | `--app ` | Filter by app name. | | `--status ` | Filter by status. | | `--search ` | Filter by id/name substring. | | `--refresh ` | Refresh interval in seconds, 1 through 60 (default `2`). | | `--limit ` | Maximum active rows to display, 1 through 200 (default `50`). | ## Run commands and connect ### `sail box exec` Run a command in a Sailbox, streaming its output. ```bash theme={null} sail box exec [options] -- [args...] ``` | Option | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--cwd ` | Working directory inside the guest. | | `--timeout ` | Kill the command after this long (e.g. `30s`, `5m`). | | `-e`, `--env ` | Environment variable for the command (repeatable). | | `--user ` | Run as this user: a name or numeric uid, optionally with a group after a colon (`alice`, `1000`, `alice:staff`). Default: the image's `USER`, else root; pass `0:0` to force root. | | `-i`, `--stdin` | Pipe local stdin to the guest command. | | `-t`, `--tty` | Run under a pseudo-terminal driven by your terminal. | | `--no-forward` | With `--tty`, turn off all local forwarding (browser, ports, paste/clipboard). | | `--background` | Start the command and leave it running in the Sailbox, returning once the Sailbox has accepted it. Output is not captured. | ### `sail box run` Create an ephemeral Sailbox, run a command, then terminate it. A shortcut for `create` + `exec` + `terminate`; for workflows that reuse a Sailbox, use those directly. ```bash theme={null} sail box run --app [options] -- [args...] ``` Takes the same `create` flags (`--arch`, sizing, `--port`) plus `--name` (default `run-`), `--cwd`, `--timeout`, `--env`, `--user`, `--auto-sleep` (as on create; meaningful with `--keep`), `--no-network` and `--allow-host` (as on create), and `--keep` (leave the Sailbox running instead of terminating it). An exposed port is reachable while the command runs and stays reachable afterwards only with `--keep`. ### `sail box shell` ```bash theme={null} sail box shell [--shell ] [--user ] [--env ]... [--no-forward] ``` Open an interactive shell inside a running Sailbox. This is the simplest and preferred way in: it runs a PTY over `exec`, so it opens no port and does not count against your org's raw-TCP endpoint limit. (Not to be confused with `sail shell`, the local REPL for managing boxes.) The session runs as the image's `USER`, or as root when the image sets none: the same identity `sail box exec` uses. `--user` opens it as someone else (a name or numeric uid, optionally with a group after a colon, like `alice`, `1000`, or `alice:staff`); `--user 0:0` forces root. `--env K=V` (or `-e`, repeatable) adds environment variables to the session, as on `sail box exec`. While the shell is open, the session forwards to your machine: * **Browser opens.** When a program in the box opens a browser, the page opens in your local browser instead. This covers logins like `claude login`, `codex login`, and `gh auth login`. A login that redirects to a `localhost` callback completes end to end. * **Localhost servers.** A server the box starts on `localhost` (say a dev server on port 3000) becomes reachable at `http://localhost:3000` on your machine. The same port is used on your machine, so if it is already in use locally that server is not forwarded. * **Paste and drag-and-drop.** Files dragged onto the terminal upload to `/tmp/sail-drops` in the box and paste as their guest paths. Press Ctrl+V to forward your clipboard. On devbox images an image or text lands on the box's clipboard, so pasting a screenshot into `claude` or `codex` works as it does locally, and text copied inside the box is copied back to yours. On other images a Ctrl+V image uploads as a file and pastes its path, while text uses your terminal's own paste. Large uploads show a progress line; press Esc to cancel one. Pass `--no-forward` to turn all of it off, for example for an untrusted or automated session. `sail box exec --tty` forwards the same way and takes the same flag. ### `sail box cp` ```bash theme={null} sail box cp [--recursive] [--user ] ``` Copy a file or directory to or from a Sailbox; `:` denotes the remote side. Uploaded files belong to the image's `USER`, or to root when the image sets none: the same identity `sail box exec` runs commands as. `--user` names a different owner for the file and any directories the upload creates (a name or numeric uid, optionally with a group after a colon, like `alice`, `1000`, or `alice:staff`); `--user 0:0` forces root. `--user` applies to uploads only; downloaded files are owned by whoever runs the CLI. Pass `--recursive` (`-r`) to copy a directory: the source directory's contents land inside the destination directory, which is created if needed. Copying a directory requires the flag. With `--recursive`, `--user` owns the copied entries, the destination directory, and any missing parents the copy creates. ## Networking ```bash theme={null} sail box expose [--tcp] [--allowlist ]... # expose a guest port at runtime sail box unexpose # remove a runtime ingress port sail box listeners # list a Sailbox's ingress listeners sail box address # print the external address for one port ``` `--tcp` exposes raw TCP instead of HTTP. `--allowlist` restricts sources to an address or a range (e.g. `203.0.113.0/24`), or, for HTTP listeners, a Sail app name. Re-exposing a port replaces its whole list; omitting `--allowlist` reopens the port. See [Access Control](/sailboxes-access-control) for the full model. ## Secrets and HTTP policies Store a secret for your organization, create an HTTP policy that uses it, then attach that policy to a Sailbox. See [Credential injection](/sailboxes-credentials) for a complete example and [HTTP policies](/sailboxes-credentials#policies) for the policy document format. ```bash theme={null} sail secret set [--from-env ] # prompt for a value, read an environment variable, or read piped input sail secret show # show the name and timestamps, never the value sail secret list # list secret names and timestamps sail secret delete sail http-policy create --file # use --file - to read JSON from standard input sail http-policy show sail http-policy list [--search ] [--limit ] [--offset ] sail http-policy rename sail http-policy delete sail box http-policy show sail box http-policy set sail box http-policy clear ``` A Sailbox holds at most one policy; `set` replaces any policy already attached. ## Custom domains `sail box domain` serves a Sailbox HTTP listener on a hostname you own, with TLS certificates obtained and renewed for you. ```bash theme={null} sail box domain target # print the custom-domain and wildcard certificate targets sail box domain attach --port # attach a domain to an exposed HTTP listener sail box domain list # list the domains attached to a Sailbox sail box domain detach # stop routing a domain to the Sailbox ``` `attach` requires `--port` naming an exposed HTTP listener, and checks that your DNS record points at your target first. See [Custom domains](/sailboxes-custom-domains) for the DNS setup, apex-domain options, and certificate behavior. ## SSH `sail box ssh` sets up SSH access so you can reach a box as `ssh .sail`. For a quick interactive shell, prefer [`sail box shell`](#sail-box-shell): it needs no open port. Reach for SSH when you need a real SSH endpoint rather than a PTY over `exec`, such as `scp`/`rsync`, an editor's remote mode, or a devbox you work in day to day. Enabling it exposes port 22 as a TCP ingress port, which counts against your org's raw-TCP endpoint limit. ```bash theme={null} sail box ssh enable [--identity-file ] [--allowlist ]... [--no-wait] [--timeout ] sail box ssh alias ... [--identity-file ] sail box ssh disable ``` * **enable**: turn on SSH for a box (expose port 22, install your org's CA, start sshd), certify your key on this machine, and add the `.sail` shortcut. `--allowlist ` restricts the sources allowed to reach port 22 to an address or a range (repeatable). Passing `--allowlist` replaces the port's current restriction. Omitting it on a first enable leaves port 22 open to any source; on a re-enable it keeps the existing restriction. * **alias**: add `ssh .sail` shortcuts for boxes already SSH-enabled elsewhere (e.g. from the SDK), without waking them. * **disable**: stop SSH on a box and drop its local shortcut. Only the public half of your key is certified; the private key is referenced in your SSH config, never read. ## Configuration Manage `~/.sail/config.toml`. ```bash theme={null} sail config get [key] # print one value, or the whole file sail config set ... # set one or more entries sail config unset ... # remove one or more keys sail config reset # reset user-settable settings (run 'sail auth logout' to remove the stored key) ``` # Python SDK Source: https://docs.sailresearch.com/reference/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 ```bash pip theme={null} pip install sail ``` ```bash uv theme={null} uv add sail ``` 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.
## 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. | ### 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 -p `). 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. ### 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. ### 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. ### 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. ### 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. ### 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. ### 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. ### pause Checkpoint and pause this Sailbox until it is explicitly resumed. ```python theme={null} def pause() -> None ``` ### 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. ### 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. ### 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. ### 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. ### resume Resume this paused or sleeping Sailbox, returning it to running. ```python theme={null} def resume() -> Sailbox ``` ### listener Fetch one listener by guest port without waking the Sailbox. ```python theme={null} def listener(guest_port: int) -> Listener ``` ### listeners List this Sailbox's listeners without waking it. ```python theme={null} def listeners() -> list[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. ### 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. ### 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`. ### 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. ### http\_policy The HTTP policy attached to this Sailbox, or `None` when no policy is attached. ```python theme={null} def http_policy() -> Optional[HttpPolicy] ``` ### 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. ### 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. ### 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`. ### fs Filesystem operations on this Sailbox's guest: read and write files (buffered or streaming), and directory helpers. ```python theme={null} fs: SailboxFs ``` ### 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. ### 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. ### 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. ## 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. | ### 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 ``` ### 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. ## ImageDefinition ### apt\_install Add an `apt-get install` step for `packages`. ```python theme={null} def apt_install(*packages: str) -> ImageDefinition ``` ### pip\_install Add a `pip install` step for `packages`. ```python theme={null} def pip_install(*packages: str) -> ImageDefinition ``` ### run\_commands Add shell commands to the build, each as its own step. ```python theme={null} def run_commands(*cmd: str) -> ImageDefinition ``` ### 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. ### 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. ### env Bake environment variables into the image. ```python theme={null} def env(env: Dict[str, str]) -> ImageDefinition ``` ### 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. ## 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. ### 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")`. ### debian\_amd64 Debian base for x86-64; shorthand for `debian("amd64")`. ```python theme={null} debian_amd64: ImageDefinition ``` ### debian\_arm64 Debian base for arm64; shorthand for `debian("arm64")`. ```python theme={null} debian_arm64: ImageDefinition ``` ### 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")`. ### devbox\_amd64 Devbox base for x86-64; shorthand for `devbox("amd64")`. ```python theme={null} devbox_amd64: ImageDefinition ``` ### devbox\_arm64 Devbox base for arm64; shorthand for `devbox("arm64")`. ```python theme={null} devbox_arm64: ImageDefinition ``` ### 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") ``` ### 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) ``` ## 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. ### 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 ``` ### 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] ``` ### 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] ``` ### 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. ### 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] ``` ### 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] ``` ### output\_bytes Raw `bytes` twin of `output` (alias of `stdout_bytes`). ```python theme={null} output_bytes: Generator[bytes, None, None] ``` ### stdin Stdin writer for an `open_stdin=True` exec; raises `sail.InvalidArgumentError` otherwise. ```python theme={null} stdin: StdinWriter ``` ### 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. ### poll Alias of `exit_code`; never blocks. ```python theme={null} def poll() -> Optional[int] ``` ### 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. ### 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. ### 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. ### 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. ### 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. ## 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. ### 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 ``` ### 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] ``` ### 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] ``` ### 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. ### 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] ``` ### 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] ``` ### output\_bytes Raw `bytes` twin of `output` (alias of `stdout_bytes`). ```python theme={null} output_bytes: AsyncGenerator[bytes, None] ``` ### stdin Stdin writer for an `open_stdin=True` exec; raises `sail.InvalidArgumentError` otherwise. ```python theme={null} stdin: AsyncStdinWriter ``` ### exit\_code Exit code once the streams end, else None (see the sync handle's note). ```python theme={null} exit_code: Optional[int] ``` ### poll Alias of `exit_code`; never blocks. ```python theme={null} def poll() -> Optional[int] ``` ### cancel Signal the guest command: SIGINT by default, SIGKILL if force=True. ```python theme={null} async def cancel(*, force: bool = False) -> None ``` ### 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 ``` ### 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 ``` ### 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. ### 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. ## 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`. ### close Send EOF; the guest closes the pipe once the backlog drains. ```python theme={null} def close() -> None ``` ## 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. ### close Send EOF; the guest closes the pipe once the backlog drains. ```python theme={null} async def close() -> None ``` ## function Decorate a Python function so it can run through `Sailbox.exec`. ```python theme={null} def function(func: Optional[F] = None) ``` ## SailFunction A Python function that can be executed inside a Sailbox. ## 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. ### 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. ### 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. ### 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. ### 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. ### 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. ### 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 ``` ### 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 ``` ### 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 ``` ### 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] ``` ### 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. ### 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. ## 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. ### 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 ``` ### finish Confirm the write, creating an empty file when nothing was written. ```python theme={null} def finish() -> None ``` ### abort Cancel the write so it is never committed. Idempotent; a no-op after `finish`. ```python theme={null} def abort() -> None ``` ## 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. ### 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 ``` ### finish Confirm the write, creating an empty file when nothing was written. ```python theme={null} async def finish() -> None ``` ### abort Cancel the write so it is never committed. Idempotent; a no-op after `finish`. ```python theme={null} async def abort() -> None ``` ## 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. ### close Stop the stream and release its resources; safe to call twice. ```python theme={null} def close() -> None ``` ### aclose Async twin of `close`, run off the event loop. ```python theme={null} async def aclose() -> None ``` ## 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. | ### 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 ``` ### list List active NFS volumes for the current organization, newest first. ```python theme={null} @staticmethod def list(*, max_objects: Optional[int] = None) -> List["Volume"] ``` ### 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`. ### 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. ### 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. ### 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. | #### 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. #### 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. #### list List your organization's secret names and timestamps, sorted by name. ```python theme={null} @staticmethod def list() -> List["Secret"] ``` #### 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`. #### delete\_by\_name Delete the named secret. Same contract as `delete`. ```python theme={null} @staticmethod def delete_by_name(name: str) -> None ``` ### 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. | #### 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. #### 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. #### 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`. #### 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. #### 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. ## 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. ### 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. ### 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. ### 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. ### 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()`. ### 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`. ### voyage\_id The current Voyage's id, or `None` when no Voyage is current. ```python theme={null} def voyage_id() -> Optional[str] ``` ### 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] ``` ### 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. ### 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. ### 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__`. ### 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. ### 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 ``` ### 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 ``` ### cancel Mark the current Voyage canceled. A no-op when no Voyage is active. ```python theme={null} def cancel() -> None ``` ### 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 ``` ### Voyage A Sail Voyage attached to the current process. #### 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. #### 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. #### 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(...)`. #### 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. #### 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. #### 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. ### 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. ## SailTokenCompleter Tinker TokenCompleter backed by Sail's raw-token Responses path. Extends `TokenCompleter`. ## 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. ### sandbox\_id The backing Sailbox's id. ```python theme={null} sandbox_id: str ``` ### 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`. ### 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. ### 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`. ### 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. ### 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 ``` ## 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. ## 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. ### 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. ### 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. ### 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 ``` ### 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 ``` ### 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 ``` ### 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 ``` ### 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. ## 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 `/_sailbox/{id}/{port}`, `"subdomain"` addresses `-.`. ### from\_env Load SDK config, raising `ValueError` when no API key is configured. ```python theme={null} @classmethod def from_env() -> Config ``` ### 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. ### 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 -p `). `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. | ### HttpEndpoint The routable HTTPS address of an `"http"` listener. **Attributes:** | Attribute | Type | Description | | --------- | ----- | ----------------------- | | `url` | `str` | The routable HTTPS URL. | ### 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. | ### 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. | ### 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. | ### 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. | ### 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. | ### 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. | ### 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. | ### 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. | #### default Let Sail decide, on its own timing. The default. ```python theme={null} @classmethod def default() -> AutoSleep ``` #### 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. #### never Stop Sail sleeping a Sailbox on its own. ```python theme={null} @classmethod def never() -> AutoSleep ``` ### 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. ### 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. | ### 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. | ### 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. | ### 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. | ### 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. | ### 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. | ### 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. | ### GuestPath A path inside the guest: a str or a `PurePosixPath`. Guest paths are remote POSIX paths, independent of the local platform. ### 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. ### 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. ### 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. | ### NotFoundError Raised when a requested Sailbox, app, volume, checkpoint, secret, or HTTP policy is not found. Extends `SailError`, `LookupError`. ### PermissionDeniedError Raised for a missing or invalid API key, or insufficient scope. Extends `SailError`, `PermissionError`. ### InvalidArgumentError Raised when an argument or the SDK configuration is rejected as invalid. Extends `SailError`, `ValueError`. ### InternalError Raised for an unexpected internal SDK failure. Extends `SailError`, `RuntimeError`. ### FileNotFoundError Raised when a guest file operation references a path that does not exist. Extends `SailError`, `builtins.FileNotFoundError`. ### BrokenPipeError Raised when a stdin write hits a command that already finished. Extends `SailError`, `builtins.BrokenPipeError`. ### TimeoutError Raised when a transport attempt exceeds its deadline. Extends `SailError`, `builtins.TimeoutError`. ### TransportError Raised when the transport cannot establish or maintain a connection. Extends `SailError`, `ConnectionError`. ### 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`. ### SecretInUseError Raised when deleting a secret that HTTP policies still refer to. Extends `ApiError`. Delete those policies first, then delete the secret. ### 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. ### SailDeprecationWarning Warning that a Sail client or Sailbox runtime should be upgraded. Extends `UserWarning`. ### SailboxError Base class for Sailbox-specific SDK errors. Extends `SailError`. ### SailboxCreationError Raised when Sailbox creation fails. Extends `SailboxError`. ### ImageBuildError Raised when a custom image build fails. Extends `SailboxError`. ### SailboxExecutionError Base class for Sailbox exec-related SDK errors. Extends `SailboxError`. ### SailboxTerminatedError Raised when the Sailbox no longer exists. Extends `SailboxExecutionError`. ### SailboxExecRequestNotFoundError Raised when a wait references an unknown exec request. Extends `SailboxExecutionError`. ### 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. ### SailboxFunctionError Raised when a Python function fails while running in a Sailbox. Extends `SailboxExecutionError`. ### SailboxFunctionSerializationError Raised when a Python function payload or result cannot be serialized. Extends `SailboxExecutionError`. ### CommandFailedError Raised by `run(check=True)` when the command exits nonzero or times out. Extends `SailboxExecutionError`. Carries the completed `sail.ExecResult` as `result`. ### VoyageError Base class for Voyage SDK errors. Extends `SailError`. ### VoyageHTTPError Raised when the Voyage API returns an HTTP error. Extends `VoyageError`. ### VoyageNotFoundError Raised when a Voyage cannot be found for the current API key. Extends `VoyageHTTPError`. ### InferenceError Base class for Sail inference wrapper errors. Extends `SailError`. ### InferenceHTTPError Raised when a Sail inference endpoint returns an HTTP error. Extends `InferenceError`.
# Rust SDK Source: https://docs.sailresearch.com/reference/rust-sdk Rust SDK installation and API reference on docs.rs The Sail Rust SDK (`sail-rs` on crates.io) creates and drives Sailboxes from Rust: lifecycle, streaming exec, file transfer, ingress, and credential injection. Behavior matches the [Python](/reference/python-sdk) and [TypeScript](/reference/typescript-sdk) SDKs. ## Install ```toml theme={null} [dependencies] sail-rs = "0.11.2" # tokio must be a direct dependency to write #[tokio::main]: tokio = { version = "1", features = ["macros", "rt-multi-thread"] } ``` The crate is published as `sail-rs` and imported as `sail`: ```rust theme={null} use sail::Client; ``` Adding the crate does not install the `sail` CLI. Install the CLI separately with `curl -fsSL https://cli.sailresearch.com/install.sh | sh`, or see [Install the CLI](/reference/cli). ## Configure Set `SAIL_API_KEY` in the environment; the SDK also reads the credential `sail auth login` stores under `~/.sail`. Construct with `Client::from_env()`, or use `Client::builder(api_key)` for explicit configuration. See [Configuration](/reference/sdk-configuration). ## Quickstart ```rust theme={null} use sail::{Client, CreateSailboxRequest, RunOptions, SailError}; #[tokio::main] async fn main() -> Result<(), SailError> { let client = Client::from_env()?; // Look up (or create) the app your Sailboxes belong to. let app = client .find_app("rust-quickstart", /* mint_if_missing */ true) .await?; // Create a Sailbox; the default request uses the prebuilt Debian base image. let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "quickstart".into(), ..Default::default() }, /* timeout */ None, ) .await?; // Run a command, then terminate the Sailbox whether or not the run failed. let run = sb .run_shell("echo hello from the guest", RunOptions::default()) .await; sb.terminate().await?; print!("{}", run?.stdout); Ok(()) } ``` ## Runtime Every method is `async`, driven on a Tokio runtime. Async hosts await the methods directly. Synchronous code can drive any method with `sail::block_on`, which runs it to completion on a shared internal runtime. `Client` is cheap to clone (it shares its connection pools and config behind an `Arc`), so clone it to share across tasks. ## Surface Voyages (agent tracing) and inference calls are Python-only; the Rust SDK covers the full Sailbox surface. See the [Voyages reference](/voyages-sdk). `create_sailbox` and `create_from_checkpoint` return a `Sailbox`, and `client.sailbox(id)` binds an existing id without a network call. Every per-Sailbox operation is a method on it: lifecycle (`info` / `terminate` / `pause` / `sleep` / `set_auto_sleep` / `resume` / `upgrade`), `checkpoint`, one-shot `run` / `run_shell`, streaming `exec` / `exec_shell`, filesystem helpers under `fs()` (one-shot `read` / `write`, streaming `read_stream` / `write_stream`, directory transfer `upload_dir` / `download_dir`, and `mkdir` / `remove` / `exists` / `ls`), an interactive `shell`, listeners (`expose` / `unexpose` / `listeners` / `listener` / `wait_for_listener` / `ingress_auth_headers`), SSH (`enable_ssh`), and HTTP policy attachment (`set_http_policy` / `http_policy` / `clear_http_policy`). `run` and `run_shell` return only the most recent `output_buffer_bytes` of each stream (1 MiB by default), with the `*_truncated` flags set when older output was dropped. To get every byte of a stream, use `exec` or `exec_shell` and take its reader (`reader` or `reader_async`) right after the call returns; Sail then pauses the command if you fall behind (until a cancel or the timeout ends the pauses; see `OutputMode`), and dropping the reader releases the stream. A stream without a reader never pauses the command, and Sail keeps only its most recent bytes. `ExecOptions::output_mode` changes that: `OutputMode::Pipe` holds both streams until you take their readers, and `OutputMode::Tail` never pauses the command. You can read one stream without holding the other. If you hold both readers, read them at the same time from two tasks. The [Exec process](/sailbox-sdk#sailboxexecprocess) section has the details. Org-scoped operations live on the `Client`: `list_sailboxes`, `create_from_checkpoint`, volumes, apps, the image build pipeline, and secrets and HTTP policies for [credential injection](/sailboxes-credentials). 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). ## API reference The full API reference is generated by rustdoc and published on docs.rs. It covers every module, type, and method in the crate, with source links and cross-references. Open the complete `sail-rs` reference. # Configuration Source: https://docs.sailresearch.com/reference/sdk-configuration How the SDK resolves endpoints and retries: environment variables and sail.Config Most applications configure the SDKs entirely through environment variables. Set `SAIL_API_KEY` (or run `sail auth login` once) and you're done; the endpoint variables below matter only for custom or self-hosted deployments. Every SDK reads the same variables and the same credential store. Use `export SAIL_API_KEY=sk_...` for SDK scripts, CI jobs, and background agents. If `SAIL_API_KEY` is unset, the SDKs fall back to the credential `sail auth login` stores under `~/.sail`. The environment variable always wins. ## Endpoint overrides Each of these overrides one endpoint, for custom or self-hosted stacks: * `SAIL_API_URL`: the main Sail API. Default `https://api.sailresearch.com`. * `SAILBOX_API_URL`: the Sailbox API. Default `https://sailbox-api.sailresearch.com`. * `SAILBOX_INGRESS_URL`: the base URL used to build an exposed listener's public address when the service does not return one. Configuration is read when a client is created. To pick up changed variables in a long-lived process: in Python, call `sail.reset_transports()` (the SDK resolves once per process); in TypeScript, construct a new client with `Client.fromEnv()` and repoint the object model with `setDefaultClient`; in Rust, construct a new client with `Client::from_env()`. ## Worker threads The Python and TypeScript SDKs run their network calls on a small pool of background threads shared by the whole process, and the Rust SDK uses the same pool for its blocking calls. The pool is sized to the machine: one thread per CPU, with at least 2 and at most 8. That is plenty for most applications, including ones that drive many Sailboxes concurrently, because the threads spend nearly all of their time waiting on the network. Rust code that awaits the async API does not use this pool. Those calls run on your application's own async runtime, so the setting below does not affect them. Set `SAIL_RUNTIME_THREADS` to override the pool size, for example to give a large fan-out workload more headroom or to keep a constrained process at exactly one thread. Values from 1 to 256 are accepted; anything else is ignored and the default applies. The variable is read once per process, when the SDK first performs work, so set it before your program starts using the SDK. ## Inspecting the configuration Each SDK exposes the configuration it resolved from the environment: ```python Python theme={null} import sail config = sail.Config.from_env() print(config.api_url) # https://api.sailresearch.com ``` ```typescript TypeScript theme={null} import { resolveConfig } from "@sailresearch/sdk"; const config = resolveConfig(); console.log(config.apiUrl); // https://api.sailresearch.com ``` ```rust Rust theme={null} use sail::Client; let client = Client::from_env()?; println!("{}", client.config().api_url); // https://api.sailresearch.com ``` ### Python constructors ```python theme={null} @classmethod def from_env() -> Config @classmethod def from_env_optional_api_key() -> Config ``` * `Config.from_env()` resolves the configuration and **requires** an API key (from `SAIL_API_KEY` or the stored `sail auth login` credential), raising `ValueError` if none is found. * `Config.from_env_optional_api_key()` is the same, but does not require an API key. Use it when you want a `Config` without a key, for example so `sail.voyage` can run in no-op mode. For the Voyage and agent attribution environment variables, see the [Voyages environment table](/voyages-sdk#environment-variables). ## Retries The SDKs retry transient failures automatically. By default they make up to 3 attempts with exponential backoff and full jitter, honoring a server `Retry-After` when one is present: * **502 / 503 / 504** are retried. * **429** is retried only when the response includes a valid `Retry-After`. * **500** and other statuses are surfaced immediately, without a retry. Mutations that are not safe to repeat skip these retries and make a single attempt. `exec` retries transient failures against a waking or migrating Sailbox for up to ten minutes, reusing its idempotency key so a retried launch still runs the command once. # TypeScript SDK Source: https://docs.sailresearch.com/reference/typescript-sdk TypeScript SDK installation and full reference The Sail TypeScript SDK (`@sailresearch/sdk` on npm) runs on Node 22+ and Bun. Sail also provides [Python](/reference/python-sdk) and [Rust](/reference/rust-sdk) SDKs. ## Install ```bash npm theme={null} npm install @sailresearch/sdk ``` ```bash pnpm theme={null} pnpm add @sailresearch/sdk ``` ```bash bun theme={null} bun add @sailresearch/sdk ``` The SDK supports Linux x64/arm64 (glibc and musl), macOS x64/arm64, and Windows x64. The Sail API warns when your SDK version is nearing the end of its support window. The SDK prints that warning to stderr once per process. A version past the end of its support window is rejected with an upgrade error before any operation runs. Upgrade with `npm install @sailresearch/sdk@latest`. ## Configure Set `SAIL_API_KEY` in the environment; the SDK also reads the credential `sail auth login` stores under `~/.sail`. The statics on `Sailbox`, `App`, and `Volume` use this configuration by default, or construct a `Client` explicitly with `Client.fromConfig({ apiKey })`. See [Configuration](/reference/sdk-configuration). ## Quickstart ```ts theme={null} import { App, Sailbox } from "@sailresearch/sdk"; // Look up (or create) the app your sandboxes belong to. const app = await App.find("example-app", { mintIfMissing: true }); // Boot a sandbox. const sb = await Sailbox.create({ app, name: "worker-1" }); // Run a command and stream its output. const proc = await sb.exec("echo hello && ls /"); for await (const chunk of proc.stdout) process.stdout.write(chunk); const result = await proc.wait(); console.log("exit code:", result.exitCode); // Move files. await sb.fs.write("/tmp/note.txt", "hi\n"); const contents = await sb.fs.read("/tmp/note.txt"); // Expose a port and wait until it is reachable. await sb.expose(8080, { protocol: "http" }); const listener = await sb.waitForListener(8080); if (listener.endpoint?.kind === "http") { console.log("reachable at:", listener.endpoint.url); } // Clean up (see also pause / sleep / resume / checkpoint). await sb.terminate(); ``` ## Errors Every failure the SDK recognizes extends `SailError`, so one `catch (e) { if (e instanceof SailError) }` handles them; a truly unexpected error is rethrown unchanged. Subclasses like `NotFoundError` and `SailboxExecutionError` match specific failures, every error carries an advisory `retryable` flag, and `isSailError()` is the realm-safe check. See [Errors](/sailbox-sdk-errors). ## Reference The docs below are auto-generated.
## Sailbox A sandbox (Sailbox): the primary object agent harnesses work with. Create one with [Sailbox.create](#create-1), run commands with [exec](#exec-1), move files with [fs](#fs), expose ports with [expose](#expose), and manage its lifecycle. The statics use a default env-configured client unless you pass one. ### Example ```ts theme={null} import { App, Sailbox } from "@sailresearch/sdk"; const app = await App.find("example-app", { mintIfMissing: true }); const box = await Sailbox.create({ app, name: "worker-1" }); const proc = await box.exec(["bash", "-lc", "echo hello"]); console.log(await proc.stdout.text()); await box.terminate(); ``` ### Accessors #### appId ##### Get Signature > **get** **appId**(): `string` | `undefined` Identifier of the owning app. ##### Returns `string` | `undefined` #### appName ##### Get Signature > **get** **appName**(): `string` | `undefined` Name of the owning app. ##### Returns `string` | `undefined` #### architecture ##### Get Signature > **get** **architecture**(): `string` | `undefined` CPU architecture (for example `arm64`). ##### Returns `string` | `undefined` #### autoSleep ##### Get Signature > **get** **autoSleep**(): [`AutoSleep`](#autosleep-4) | `undefined` When Sail may sleep this Sailbox on its own: from the latest [Sailbox.get](#get-1) snapshot, or your own last [Sailbox.setAutoSleep](#setautosleep) through this object. `undefined` before either; a Sailbox created by [Sailbox.fromCheckpoint](#fromcheckpoint) inherits its source's preference, so call [Sailbox.get](#get-1) to learn an inherited value. ##### Returns [`AutoSleep`](#autosleep-4) | `undefined` #### checkpointGeneration ##### Get Signature > **get** **checkpointGeneration**(): `number` | `undefined` Checkpoint generation counter as of the snapshot. ##### Returns `number` | `undefined` #### client ##### Get Signature > **get** **client**(): [`Client`](#client) The underlying [Client](#client). ##### Returns [`Client`](#client) #### cpuRequestedVcpu ##### Get Signature > **get** **cpuRequestedVcpu**(): `number` | `undefined` Requested CPU, in vCPUs. ##### Returns `number` | `undefined` #### cpuUsedVcpu ##### Get Signature > **get** **cpuUsedVcpu**(): `number` | `undefined` Current CPU usage, in vCPUs, as of the snapshot. ##### Returns `number` | `undefined` #### createdAt ##### Get Signature > **get** **createdAt**(): `Date` | `undefined` When the Sailbox was created. ##### Returns `Date` | `undefined` #### createdByUserId ##### Get Signature > **get** **createdByUserId**(): `string` | `undefined` The user whose credential created this Sailbox (for a restore, the user who ran it). `undefined` for service-key creates. ##### Returns `string` | `undefined` #### deprecation ##### Get Signature > **get** **deprecation**(): `SailboxDeprecation` | `undefined` Actionable runtime deprecation notice, when an upgrade is needed. ##### Returns `SailboxDeprecation` | `undefined` #### diskRequestedBytes ##### Get Signature > **get** **diskRequestedBytes**(): `number` | `undefined` Requested disk, in bytes. ##### Returns `number` | `undefined` #### diskUsedBytes ##### Get Signature > **get** **diskUsedBytes**(): `number` | `undefined` Current disk usage, in bytes, as of the snapshot. ##### Returns `number` | `undefined` #### errorMessage ##### Get Signature > **get** **errorMessage**(): `string` | `undefined` Failure detail when the status is `failed`. ##### Returns `string` | `undefined` #### fs ##### Get Signature > **get** **fs**(): [`SailboxFs`](#sailboxfs-1) Filesystem operations on this Sailbox's guest: read and write files (buffered or streaming), and directory helpers. ##### Returns [`SailboxFs`](#sailboxfs-1) #### guestSchemaVersion ##### Get Signature > **get** **guestSchemaVersion**(): `number` | `undefined` The Sailbox runtime schema version the Sailbox last booted with. ##### Returns `number` | `undefined` #### imageId ##### Get Signature > **get** **imageId**(): `string` | `undefined` Identifier of the image the Sailbox was created from. ##### Returns `string` | `undefined` #### lastCheckpointedAt ##### Get Signature > **get** **lastCheckpointedAt**(): `Date` | `undefined` When the most recent checkpoint was taken. ##### Returns `Date` | `undefined` #### memoryMib ##### Get Signature > **get** **memoryMib**(): `number` | `undefined` Configured memory, in MiB. ##### Returns `number` | `undefined` #### memoryRequestedBytes ##### Get Signature > **get** **memoryRequestedBytes**(): `number` | `undefined` Requested memory, in bytes. ##### Returns `number` | `undefined` #### memoryUsedBytes ##### Get Signature > **get** **memoryUsedBytes**(): `number` | `undefined` Current memory usage, in bytes, as of the snapshot. ##### Returns `number` | `undefined` #### name ##### Get Signature > **get** **name**(): `string` The Sailbox name. ##### Returns `string` #### networkPolicy ##### Get Signature > **get** **networkPolicy**(): [`NetworkPolicyInfo`](#networkpolicyinfo) | `undefined` The Sailbox's network policy, frozen at creation, from the latest [Sailbox.get](#get-1) or [Sailbox.list](#list-2) snapshot. `undefined` means public (unrestricted outbound access) or that no snapshot has been taken yet; a present value carries the restrictive mode so you can verify what is enforced, including on a Sailbox created from a checkpoint. ##### Returns [`NetworkPolicyInfo`](#networkpolicyinfo) | `undefined` #### sailboxId ##### Get Signature > **get** **sailboxId**(): `string` The Sailbox's stable identifier. ##### Returns `string` #### startedAt ##### Get Signature > **get** **startedAt**(): `Date` | `undefined` When the Sailbox first started running. A resume does not rewrite it. ##### Returns `Date` | `undefined` #### stateDiskSizeGib ##### Get Signature > **get** **stateDiskSizeGib**(): `number` | `undefined` Configured state-disk size, in GiB. ##### Returns `number` | `undefined` #### status ##### Get Signature > **get** **status**(): [`SailboxStatus`](#sailboxstatus-1) The lifecycle status as of the call that produced this handle (updated by lifecycle calls on this instance). Use [Sailbox.get](#get-1) for a fresh snapshot. ##### Returns [`SailboxStatus`](#sailboxstatus-1) #### updatedAt ##### Get Signature > **get** **updatedAt**(): `Date` | `undefined` When the Sailbox last changed. ##### Returns `Date` | `undefined` #### vcpuCount ##### Get Signature > **get** **vcpuCount**(): `number` | `undefined` Configured number of vCPUs. ##### Returns `number` | `undefined` #### visibility ##### Get Signature > **get** **visibility**(): `string` | `undefined` `"private"` when access is restricted to the creator; `undefined`/`"org"` is the default org-wide access. ##### Returns `string` | `undefined` #### volumeMounts ##### Get Signature > **get** **volumeMounts**(): `SailboxVolumeMount`\[] | `undefined` Volumes attached to this Sailbox and the paths they are mounted at. ##### Returns `SailboxVolumeMount`\[] | `undefined` ### Methods #### checkpoint() > **checkpoint**(`options?`): `Promise`\<[`SailboxCheckpoint`](#sailboxcheckpoint-1)> Take a checkpoint of this Sailbox and prepare its clean start state. The returned handle carries `expiresAt`, after which starting a Sailbox from it fails. Sailboxes with volume mounts are not supported. Upgrade a Sailbox that uses an older guest payload before creating a checkpoint handle. ##### Parameters | Parameter | Type | | --------- | ----------------------------------------- | | `options` | [`CheckpointOptions`](#checkpointoptions) | ##### Returns `Promise`\<[`SailboxCheckpoint`](#sailboxcheckpoint-1)> #### clearHttpPolicy() > **clearHttpPolicy**(): `Promise`\<`void`> Clear this Sailbox's attached HTTP policy. The change applies to HTTPS connections this Sailbox opens after the call; connections already open keep the previous policy until they close. This also resolves when no policy is attached. ##### Returns `Promise`\<`void`> #### enableSsh() > **enableSsh**(`options?`): `Promise`\<[`SshEndpoint`](#sshendpoint) | `null`> Enable SSH on this Sailbox: trust the org SSH CA, start `sshd`, and expose guest port 22 as TCP once the CA-only daemon owns it. Org members connect with a short-lived certificate (fetched by the `sail box ssh` CLI); a private Sailbox accepts only its creator's certificates. Safe to re-run. With `wait` (the default), polls until the endpoint is reachable and returns it, throwing [TimeoutError](#timeouterror) if it is not within `timeoutSeconds`; with `wait: false`, skips the probe and resolves `null`. ##### Parameters | Parameter | Type | | --------- | --------------------------------------- | | `options` | [`EnableSshOptions`](#enablesshoptions) | ##### Returns `Promise`\<[`SshEndpoint`](#sshendpoint) | `null`> #### exec() > **exec**(`command`, `options?`): `Promise`\<[`ExecProcess`](#execprocess)> Run a command and return a handle to the live process. A `string` command is run via `/bin/sh -lc`; a `string[]` is exec'd directly. By default a stream you are consuming 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 consuming keeps only its most recent 1 MiB; start consuming right after this call returns to get every byte. `outputMode` and `outputBufferBytes` in `options` change that (see [ExecProcess](#execprocess)). `options` can also set a working directory or detach the command (see [ExecOptions](#execoptions)). Stopping the command is the caller's job via [ExecProcess.cancel](#cancel). ##### Parameters | Parameter | Type | | ---------- | -------------------------------- | | `command` | `string` \| readonly `string`\[] | | `options?` | [`ExecOptions`](#execoptions) | ##### Returns `Promise`\<[`ExecProcess`](#execprocess)> #### expose() > **expose**(`guestPort`, `options?`): `Promise`\<[`Listener`](#listener-1)> Expose a guest port at runtime. 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. The returned listener carries the resolved endpoint but an `"unknown"` route status: the response confirms configuration, not reachability; [waitForListener](#waitforlistener-1) confirms the route is live. ##### Parameters | Parameter | Type | | ----------- | --------------------------------- | | `guestPort` | `number` | | `options` | [`ExposeOptions`](#exposeoptions) | ##### Returns `Promise`\<[`Listener`](#listener-1)> #### httpPolicy() > **httpPolicy**(): `Promise`\<[`HttpPolicy`](#httppolicy) | `null`> The HTTP policy attached to this Sailbox, or `null` when no policy is attached. ##### Returns `Promise`\<[`HttpPolicy`](#httppolicy) | `null`> #### ingressAuthHeaders() > **ingressAuthHeaders**(): `Promise`\<`Record`\<`string`, `string`>> Ingress-identity headers for this Sailbox, as a name→value map. ##### Returns `Promise`\<`Record`\<`string`, `string`>> #### listener() > **listener**(`guestPort`): `Promise`\<[`Listener`](#listener-1)> Fetch one listener by guest port without waking the Sailbox. ##### Parameters | Parameter | Type | | ----------- | -------- | | `guestPort` | `number` | ##### Returns `Promise`\<[`Listener`](#listener-1)> #### listeners() > **listeners**(): `Promise`\<[`Listener`](#listener-1)\[]> List this Sailbox's listeners without waking it. ##### Returns `Promise`\<[`Listener`](#listener-1)\[]> #### pause() > **pause**(): `Promise`\<`void`> Pause this Sailbox in memory. ##### Returns `Promise`\<`void`> #### resume() > **resume**(): `Promise`\<`void`> Resume this Sailbox (updates [status](#status-2)). ##### Returns `Promise`\<`void`> #### run() > **run**(`command`, `options?`): `Promise`\<[`ExecResult`](#execresult)> Run a command to completion and return its buffered result: a one-shot convenience over [exec](#exec-1) followed by [ExecProcess.wait](#wait). A `string` command runs via `/bin/sh -lc`; a `string[]` is exec'd directly. Set `env` in `options` to add environment variables; `cwd` sets the working directory (string commands only, like [exec](#exec-1)); `signal` force-cancels the command on abort (see [RunOptions](#runoptions)). The result's stdout and stderr hold only the most recent `outputBufferBytes` of each stream (1 MiB by default, up to 64 MiB), with `stdoutTruncated` and `stderrTruncated` set when older output was dropped; the command never pauses for unread output. While the first call is still running, a second call with the same `idempotencyKey` takes over its output stream, and the earlier call's result may come back truncated. To get every byte, use [exec](#exec-1) and consume the stream (see [ExecProcess](#execprocess)). `openStdin`, `pty`, `background`, and `outputMode: "pipe"` are excluded from [RunOptions](#runoptions) and rejected at runtime: run() waits for the command to finish and buffers its output, so an interactive command would hang, a backgrounded one would return the launcher's result, not the command's, and a pipe would pause forever with nobody consuming it; use [exec](#exec-1) for those. ##### Parameters | Parameter | Type | | ---------- | -------------------------------- | | `command` | `string` \| readonly `string`\[] | | `options?` | [`RunOptions`](#runoptions) | ##### Returns `Promise`\<[`ExecResult`](#execresult)> #### setAutoSleep() > **setAutoSleep**(`autoSleep`): `Promise`\<`void`> Replace when Sail may sleep this Sailbox on its own. Each call replaces the whole setting: switching to `{ automatic: false }` clears any minimum wait set earlier, and switching back does not restore it. Calling [Sailbox.sleep](#sleep) yourself is unaffected, and so are `pause`, `resume`, and scheduled wakes. ##### Parameters | Parameter | Type | | ----------- | --------------------------- | | `autoSleep` | [`AutoSleep`](#autosleep-4) | ##### Returns `Promise`\<`void`> #### setHttpPolicy() > **setHttpPolicy**(`policy`): `Promise`\<`void`> Attach an HTTP policy to this Sailbox, replacing any policy already attached (a Sailbox has at most one). Accepts an [HttpPolicy](#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. ##### Parameters | Parameter | Type | | --------- | ----------------------------------- | | `policy` | [`HttpPolicyLike`](#httppolicylike) | ##### Returns `Promise`\<`void`> #### shell() > **shell**(`command?`, `options?`): `Promise`\<`number`> Open an interactive pty session on this Sailbox, bridged to the local terminal. With no `command`, runs a login shell; pass a command to run that under a pty instead (e.g. a REPL or an editor). Blocks until the remote process exits and resolves with its exit code. Requires an interactive terminal (stdin and stdout TTYs) on a Unix machine. The session runs as the image's `USER` when the image sets one, root otherwise; see [ShellOptions.user](#user-3). While the session is open, browser opens, localhost servers, paste, drag-and-drop, and clipboard are forwarded to your machine (the clipboard is two-way on devbox images); see [ShellOptions.noForward](#noforward). ##### Parameters | Parameter | Type | | ---------- | ------------------------------- | | `command?` | `string` | | `options?` | [`ShellOptions`](#shelloptions) | ##### Returns `Promise`\<`number`> #### sleep() > **sleep**(`wakeAt?`): `Promise`\<`Date` | `undefined`> Sleep this Sailbox to disk (wakes on traffic), optionally scheduling a wall-clock wake first. `wakeAt`, when given, records the wake before the sleep starts and the returned value is 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. Sleeping an already-sleeping Sailbox succeeds and just updates the scheduled wake. ##### Parameters | Parameter | Type | | --------- | ------ | | `wakeAt?` | `Date` | ##### Returns `Promise`\<`Date` | `undefined`> #### terminate() > **terminate**(): `Promise`\<`void`> Terminate (delete) this Sailbox (updates [status](#status-2)). ##### Returns `Promise`\<`void`> #### unexpose() > **unexpose**(`guestPort`): `Promise`\<`void`> Remove a runtime ingress port. ##### Parameters | Parameter | Type | | ----------- | -------- | | `guestPort` | `number` | ##### Returns `Promise`\<`void`> #### upgrade() > **upgrade**(): `Promise`\<[`UpgradeResult`](#upgraderesult)> Upgrade this Sailbox's runtime. ##### Returns `Promise`\<[`UpgradeResult`](#upgraderesult)> #### waitForListener() > **waitForListener**(`guestPort`, `options?`): `Promise`\<[`Listener`](#listener-1)> Block until the listener on `guestPort` is reachable end to end and return it, or throw [TimeoutError](#timeouterror) after `timeoutSeconds`. An HTTP listener is ready once the guest server answers; a TCP listener once the guest sends bytes or holds the connection open. A connectivity check, not an application health check. ##### Parameters | Parameter | Type | | ----------- | --------------------------------------------------- | | `guestPort` | `number` | | `options` | [`WaitForListenerOptions`](#waitforlisteneroptions) | ##### Returns `Promise`\<[`Listener`](#listener-1)> #### create() > `static` **create**(`options`): `Promise`\<[`Sailbox`](#sailbox)> Create a new Sailbox. A custom image definition passed as `image` is built first. Sail may sleep a fully idle Sailbox; it wakes transparently on traffic or the next operation. ##### Parameters | Parameter | Type | | --------- | ----------------------------------------------- | | `options` | [`CreateSailboxOptions`](#createsailboxoptions) | ##### Returns `Promise`\<[`Sailbox`](#sailbox)> #### fromCheckpoint() > `static` **fromCheckpoint**(`options`): `Promise`\<[`Sailbox`](#sailbox)> Create a new running Sailbox from a durable checkpoint handle. 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 [Sailbox.exec](#exec-1) 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 [Sailbox.get](#get-1). ##### Parameters | Parameter | Type | | --------- | ------------------------------------------------- | | `options` | [`FromCheckpointOptions`](#fromcheckpointoptions) | ##### Returns `Promise`\<[`Sailbox`](#sailbox)> #### fromId() > `static` **fromId**(`sailboxId`, `options?`): [`Sailbox`](#sailbox) Bind a handle to an existing Sailbox id without a network call. The returned handle carries no snapshot fields (its [name](#name-2) and [status](#status-2) are empty), just the operable surface. The id is not verified to exist: operations on an unknown or inaccessible id reject with [NotFoundError](#notfounderror). Use [get](#get-1) to validate the id and fetch a fresh snapshot instead. ##### Parameters | Parameter | Type | | ----------- | --------------------------------- | | `sailboxId` | `string` | | `options` | [`ClientOptions`](#clientoptions) | ##### Returns [`Sailbox`](#sailbox) #### get() > `static` **get**(`sailboxId`, `options?`): `Promise`\<[`Sailbox`](#sailbox)> Fetch an existing Sailbox by id. ##### Parameters | Parameter | Type | | ----------- | --------------------------------- | | `sailboxId` | `string` | | `options` | [`ClientOptions`](#clientoptions) | ##### Returns `Promise`\<[`Sailbox`](#sailbox)> #### list() > `static` **list**(`params?`): `Promise`\<[`Sailbox`](#sailbox)\[]> List the Sailboxes that match the filters, fetching pages internally until every match (or `limit` of them) is collected; use [listPage](#listpage) to page through results manually instead. `limit` caps the total returned, bounding the fetch for large orgs. A `client` can ride along in the query object. ##### Parameters | Parameter | Type | | --------- | ----------------------------------------------- | | `params` | [`ListSailboxesOptions`](#listsailboxesoptions) | ##### Returns `Promise`\<[`Sailbox`](#sailbox)\[]> #### listPage() > `static` **listPage**(`params?`): `Promise`\<[`SailboxPage`](#sailboxpage)> List one page of Sailboxes alongside the pagination envelope (`total`/`hasMore`). Takes the same filters as [list](#list-2), plus `limit` and `offset` to select the page. ##### Parameters | Parameter | Type | | --------- | ------------------------------------------------------- | | `params` | [`ListSailboxesPageOptions`](#listsailboxespageoptions) | ##### Returns `Promise`\<[`SailboxPage`](#sailboxpage)> *** ## App An app: the billing/ownership scope a Sailbox belongs to. Look one up (or mint it) with [App.find](#find), then pass it (or its [App.id](#id)) to [Sailbox.create](#create-1). ### Properties | Property | Modifier | Type | Description | | ------------ | ---------- | -------- | -------------- | | `createdAt` | `readonly` | `Date` | Creation time. | | `id` | `readonly` | `string` | Stable app id. | | `name` | `readonly` | `string` | App name. | ### Methods #### find() > `static` **find**(`name`, `options?`): `Promise`\<[`App`](#app)> Find an app by name, optionally minting it if missing. ##### Parameters | Parameter | Type | | --------- | ----------------------------------- | | `name` | `string` | | `options` | [`FindAppOptions`](#findappoptions) | ##### Returns `Promise`\<[`App`](#app)> #### list() > `static` **list**(`options?`): `Promise`\<[`App`](#app)\[]> Every app the current org owns, newest first. ##### Parameters | Parameter | Type | | --------- | --------------------------------- | | `options` | [`ClientOptions`](#clientoptions) | ##### Returns `Promise`\<[`App`](#app)\[]> *** ## Image A Sailbox image: a base, registry, or Dockerfile image plus ordered build steps. Immutable and fluent: each method returns a new `Image`. Local files/dirs are recorded here and hashed and uploaded when the image is resolved to a spec (at [Sailbox.create](#create-1), or via [toSpec](#tospec)), so chaining stays synchronous. ### Example ```ts theme={null} const image = Image.debian() .aptInstall("git") .pipInstall("numpy") .addLocalDir("./app", "/app", { ignore: ["*.pyc", "__pycache__/"] }) .runCommand("pip install -e /app"); const box = await Sailbox.create({ app, name: "w", image }); ``` ### Methods #### addLocalDir() > **addLocalDir**(`localPath`, `remotePath`, `options?`): [`Image`](#image) Bake a local directory tree into the image at `path`. Each regular file is hashed + uploaded at resolve; symlinks are skipped and file modes preserved. `ignore` takes gitignore-style patterns. ##### Parameters | Parameter | Type | | ------------ | ------------------------------------------- | | `localPath` | `string` | | `remotePath` | `string` | | `options` | [`AddLocalDirOptions`](#addlocaldiroptions) | ##### Returns [`Image`](#image) #### addLocalFile() > **addLocalFile**(`localPath`, `remotePath`, `options?`): [`Image`](#image) Bake one local file into the image at `path` (absolute POSIX path; a trailing `/` appends the source basename). Hashed + uploaded at resolve. ##### Parameters | Parameter | Type | | ------------ | --------------------------------------------- | | `localPath` | `string` | | `remotePath` | `string` | | `options` | [`AddLocalFileOptions`](#addlocalfileoptions) | ##### Returns [`Image`](#image) #### aptInstall() > **aptInstall**(...`packages`): [`Image`](#image) Install system packages with apt. ##### Parameters | Parameter | Type | | ------------- | ----------- | | ...`packages` | `string`\[] | ##### Returns [`Image`](#image) #### build() > **build**(`options?`): `Promise`\<[`ImageSpec`](#imagespec)> Upload any local files and build the image, waiting until it is ready. Returns the resolved [ImageSpec](#imagespec). [Sailbox.create](#create-1) calls this for a custom image before creating the Sailbox (the backend serves the content-addressed built image); a bare base image skips the build. Local files are re-hashed on every call, so edits always reach the build, and rebuilding an unchanged, already-built image returns quickly. Creating Sailboxes from the returned spec needs no further build. For an image imported with [Image.fromRegistry](#fromregistry) through a tag, the spec is also pinned to the exact version the build resolved the tag to, even if the tag later moves upstream. [ImageBuildOptions.forceBuild](#forcebuild) looks the tag up again and moves the tag's meaning for your whole organization. For an image built with [Image.fromDockerfile](#fromdockerfile), the returned spec is likewise pinned to the versions the build resolved for its `FROM` and `COPY --from` images; [ImageBuildOptions.forceBuild](#forcebuild) moves those pins for your whole organization, while specs built earlier keep their pinned versions. ##### Parameters | Parameter | Type | | --------- | ----------------------------------------- | | `options` | [`ImageBuildOptions`](#imagebuildoptions) | ##### Returns `Promise`\<[`ImageSpec`](#imagespec)> #### env() > **env**(`env`): [`Image`](#image) Bake environment variables into the image (keys are trimmed). ##### Parameters | Parameter | Type | | --------- | ------------------------------------------ | | `env` | `Readonly`\<`Record`\<`string`, `string`>> | ##### Returns [`Image`](#image) #### pipInstall() > **pipInstall**(...`packages`): [`Image`](#image) Install Python packages with pip. ##### Parameters | Parameter | Type | | ------------- | ----------- | | ...`packages` | `string`\[] | ##### Returns [`Image`](#image) #### runCommand() > **runCommand**(`command`): [`Image`](#image) Run a shell command during the build. ##### Parameters | Parameter | Type | | --------- | -------- | | `command` | `string` | ##### Returns [`Image`](#image) #### toSpec() > **toSpec**(`client?`): `Promise`\<[`ImageSpec`](#imagespec)> Resolve to an [ImageSpec](#imagespec): walks local files/dirs (honoring gitignore), hashes them, and uploads their content via `client` (defaults to the env client). [Sailbox.create](#create-1) calls this for you; use it directly only if you need the raw spec. ##### Parameters | Parameter | Type | | --------- | ------------------- | | `client?` | [`Client`](#client) | ##### Returns `Promise`\<[`ImageSpec`](#imagespec)> #### debian() > `static` **debian**(`architecture?`): [`Image`](#image) A Debian base image (defaults to amd64). ##### Parameters | Parameter | Type | Default value | | -------------- | ----------------------------------------- | ------------- | | `architecture` | [`ImageArchitecture`](#imagearchitecture) | `"amd64"` | ##### Returns [`Image`](#image) #### devbox() > `static` **devbox**(`architecture?`): [`Image`](#image) The devbox base image (defaults to amd64): a prebuilt Debian base with a baked development layer. 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. Prebuilt-only, so it does not support build steps or env; start from [Image.debian](#debian) to customize. ##### Parameters | Parameter | Type | Default value | | -------------- | ----------------------------------------- | ------------- | | `architecture` | [`ImageArchitecture`](#imagearchitecture) | `"amd64"` | ##### Returns [`Image`](#image) #### fromDockerfile() > `static` **fromDockerfile**(`dockerfile`, `options?`): [`Image`](#image) Build your own Dockerfile into a Sailbox image, with everything Sail needs layered on top. Pass the path to a Dockerfile on this machine, or its literal text wrapped as `{ contents }`. The result behaves like any other image: build steps, env, and pip/apt installs work the same as on [Image.debian](#debian). Pass `contextDir` to give the Dockerfile's `COPY` and `ADD` instructions a build context; if omitted, the build runs without one. A `.dockerignore` in that directory is honored, and `ignore` patterns are applied after it, so they take precedence on conflict. A file named after the 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 the image is built, so edits up to that point reach the build. File modes, empty directories, and symbolic links are carried into the build. 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`. Each named image is pinned to the version its tag pointed at the first time your organization used it, and those pinned versions become part of the built image's identity, so rebuilding the same spec reuses the same image even after a tag moves. Pass `forceBuild` to [Image.build](#build) to look the tags up again and build what they point at now. The Dockerfile must produce a Debian- or Ubuntu-based filesystem. 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. 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. The built image's environment variables, working directory, and `USER` become the defaults for commands you run with [Sailbox.exec](#exec-1) or [Sailbox.run](#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 `aptInstall`) and SSH sessions still run as root. ##### Parameters | Parameter | Type | | ------------ | ------------------------------------------------- | | `dockerfile` | `string` \| \{ `contents`: `string`; } | | `options` | [`FromDockerfileOptions`](#fromdockerfileoptions) | ##### Returns [`Image`](#image) ##### Example ```ts theme={null} const image = Image.fromDockerfile("./envs/task1/Dockerfile", { contextDir: "./envs/task1", }); ``` #### fromRegistry() > `static` **fromRegistry**(`ref`, `options?`): [`Image`](#image) Your own image as the Sailbox root filesystem, with everything Sail needs layered on top. The result behaves like any other image: build steps, env, and pip/apt installs work the same as on [Image.debian](#debian). 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({ forceBuild: true })` to look the tag up again and build the version it points at now for your whole organization; see [ImageBuildOptions.forceBuild](#forcebuild) 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](#exec-1) or [Sailbox.run](#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 `aptInstall`) and SSH sessions still run as root. ##### Parameters | Parameter | Type | | --------- | --------------------------------------------- | | `ref` | `string` | | `options` | [`FromRegistryOptions`](#fromregistryoptions) | ##### Returns [`Image`](#image) ##### Example ```ts theme={null} const image = Image.fromRegistry( "docker.io/library/python:3.13" ).aptInstall("git"); ``` *** ## ExecProcess A live command running in a Sailbox. Stream [stdout](#stdout)/[stderr](#stderr), write to [writeStdin](#writestdin), and [wait](#wait) for the result. Not killed on GC; call [close](#close) to detach, or [cancel](#cancel) to stop the command. Each stream has a buffer, 1 MiB by default (`outputBufferBytes`), and the `outputMode` says what happens when it fills. With the default `"auto"`: if you are not consuming a stream, the command never pauses and the stream keeps only its most recent bytes; if you are consuming a stream and fall behind, the command pauses when the buffer fills and resumes as you read, like a pipe. Consuming 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 consumer that starts late still gets every byte; `"tail"` never pauses the command for you. See `OutputMode`. With `"auto"`, start consuming right after `exec()` returns to get every byte. You can consume 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, consume them at the same time (`Promise.all`). The exit code is available from [poll](#poll) once the streams end and from [wait](#wait). Accessing `proc.stdout` or `proc.stderr` claims nothing. A stream is claimed when you start iterating it (`for await`, [ExecStream.raw](#raw), [ExecStream.text](#text), [ExecStream.bytes](#bytes)) or call [ExecStream.toReadable](#toreadable), and released when the iteration finishes or you leave it (`break`, `return`, a thrown error), when `.text()` or `.bytes()` reaches the end, or when the `Readable` is destroyed. Each stream can be claimed once; a second attempt rejects with `InvalidArgumentError`. [wait](#wait) returns each stream's buffer, its most recent output, with `stdoutTruncated` / `stderrTruncated` set when older output was dropped. [close](#close), or your process exiting, releases both streams; the command keeps running, and `wait()` rejects 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](#resync) requests a fresh screen. ### Example ```ts theme={null} const proc = await box.exec(["bash", "-lc", "echo hi"]); for await (const chunk of proc.stdout) process.stdout.write(chunk); const result = await proc.wait(); console.log(result.exitCode, result.stderr); ``` ### Accessors #### execRequestId ##### Get Signature > **get** **execRequestId**(): `string` The durable exec request id: 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. ##### Returns `string` #### output ##### Get Signature > **get** **output**(): [`ExecStream`](#execstream) Alias for [stdout](#stdout): under a pty the two output streams merge onto stdout, and `output` names that merged terminal stream. ##### Returns [`ExecStream`](#execstream) #### stderr ##### Get Signature > **get** **stderr**(): [`ExecStream`](#execstream) The sole stderr stream: string iteration by default, `.raw()` for bytes. Claimed and released the same way as [stdout](#stdout). ##### Returns [`ExecStream`](#execstream) #### stdout ##### Get Signature > **get** **stdout**(): [`ExecStream`](#execstream) The sole stdout stream: string iteration by default, `.raw()` for bytes. Accessing this property claims nothing; the stream is claimed when you start consuming it and released when the iteration finishes or you leave it (see [ExecStream](#execstream)). ##### Returns [`ExecStream`](#execstream) ### Methods #### \[asyncDispose]\() > **\[asyncDispose]**(): `Promise`\<`void`> `await using` support: detaches on scope exit. ##### Returns `Promise`\<`void`> #### \[dispose]\() > **\[dispose]**(): `void` `using` support: detaches on scope exit. ##### Returns `void` #### cancel() > **cancel**(`options?`): `Promise`\<`void`> Cancel the command (SIGINT by default, SIGKILL with `force`). Transient failures are retried briefly, covering the window right after the command starts when the guest cannot accept signals for it yet. ##### Parameters | Parameter | Type | | --------- | --------------------------------- | | `options` | [`CancelOptions`](#canceloptions) | ##### Returns `Promise`\<`void`> #### close() > **close**(): `void` Abandon the handle without killing the command. It releases both streams. The command keeps running and never pauses, and Sail keeps only the most recent output of each stream. Call [cancel](#cancel) instead if the command should stop. [wait](#wait) rejects after `close()` unless it already resolved a result. ##### Returns `void` #### closeStdin() > **closeStdin**(): `Promise`\<`void`> Close the command's stdin (send EOF). ##### Returns `Promise`\<`void`> #### poll() > **poll**(): `number` | `null` The exit code once the output stream has ended, else `null`. Never blocks and never drops output. If the connection was lost for good mid-command, the stream ends early with the outcome still unknown: `poll()` stays `null` and [wait](#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](#wait) always throws `SailboxHostLostError` for one, and `poll()` throws it when that loss is what ended the stream. ##### Returns `number` | `null` #### resize() > **resize**(`cols`, `rows`): `Promise`\<`void`> Resize the pty (no-op without one). ##### Parameters | Parameter | Type | | --------- | -------- | | `cols` | `number` | | `rows` | `number` | ##### Returns `Promise`\<`void`> #### resync() > **resync**(): `Promise`\<`void`> Ask a pty exec to repaint its current screen (no-op without a pty). 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. ##### Returns `Promise`\<`void`> #### wait() > **wait**(): `Promise`\<[`ExecResult`](#execresult)> Wait for the command to finish and return its result. `stdout` and `stderr` on the result hold each stream's buffer, its most recent output (1 MiB by default), with `stdoutTruncated` / `stderrTruncated` set when older output was dropped; to get every byte, consume the stream (see [ExecProcess](#execprocess)). `wait()` itself never pauses the command and may run alongside an active consumer. With `outputMode: "pipe"`, a stream nobody consumes pauses the command when its buffer fills, and `wait()` then waits for as long as the command stays paused. It rejects after [close](#close) unless it already resolved a result. ##### Returns `Promise`\<[`ExecResult`](#execresult)> #### writeStdin() > **writeStdin**(`data`): `Promise`\<`void`> Write to the command's stdin (requires `openStdin`). ##### Parameters | Parameter | Type | | --------- | ---------------------------------------------------------------------------- | | `data` | `string` \| `Buffer`\<`ArrayBufferLike`> \| `Uint8Array`\<`ArrayBufferLike`> | ##### Returns `Promise`\<`void`> *** ## ExecStream An async-iterable view of one exec stream (stdout or stderr). Default iteration yields `string` chunks, incrementally decoded as UTF-8 (a multibyte character split across chunks is carried until complete); use [raw](#raw) for the unmodified byte stream. Iteration ends once the command finishes and its remaining output has been delivered; if the connection was lost for good mid-command, it ends early with the outcome still unknown, and [ExecProcess.wait](#wait) fetches the result Sail recorded. An `ExecStream` can be consumed once: string iteration, [raw](#raw), [toReadable](#toreadable), [text](#text), or [bytes](#bytes); a second consumer rejects with `InvalidArgumentError`. Accessing `proc.stdout` claims nothing. The stream is claimed when you start consuming it, and from then on nothing is lost: the command pauses when you fall behind. That does not hold for pty output, for `outputMode: "tail"`, or after `cancel()` or the exec timeout has ended the pauses (see [ExecProcess](#execprocess)). It is released when the iteration finishes or you leave it (`break`, `return`, a thrown error), when [text](#text) or [bytes](#bytes) reaches the end, or when the `Readable` from [toReadable](#toreadable) is destroyed. While nobody is consuming, Sail keeps only the stream's most recent bytes (1 MiB by default) unless the exec runs with `outputMode: "pipe"`, so start consuming right after `exec()` returns when you need every byte. Pty output never pauses the command and keeps only its most recent bytes. ### Example ```ts theme={null} for await (const chunk of proc.stdout) process.stdout.write(chunk); ``` ### Implements * `AsyncIterable`\<`string`> ### Methods #### \[asyncIterator]\() > **\[asyncIterator]**(): `AsyncIterator`\<`string`> ##### Returns `AsyncIterator`\<`string`> ##### Implementation of `AsyncIterable.[asyncIterator]` #### bytes() > **bytes**(): `Promise`\<`Buffer`\<`ArrayBufferLike`>> Consume and collect the raw byte stream into a single `Buffer`. ##### Returns `Promise`\<`Buffer`\<`ArrayBufferLike`>> #### raw() > **raw**(): `AsyncIterableIterator`\<`Buffer`\<`ArrayBufferLike`>> Iterate the raw byte stream, exactly as the command wrote it (escape sequences and binary payloads included). This consumes the stream. ##### Returns `AsyncIterableIterator`\<`Buffer`\<`ArrayBufferLike`>> #### text() > **text**(): `Promise`\<`string`> Consume and collect the stream into a single string. ##### Returns `Promise`\<`string`> #### toReadable() > **toReadable**(): `Readable` Consume this stream as a Node `Readable` of string chunks. Calling this claims the stream at once; destroying the `Readable` releases it at once, even while a read is waiting for output. ##### Returns `Readable` *** ## SailboxFs Filesystem operations on a Sailbox's guest, reached via [Sailbox.fs](#fs). File I/O streams bytes to/from the guest; the directory helpers create, remove, test, and transfer paths. 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](#upgrade) is called. ### Methods #### downloadDir() > **downloadDir**(`dirs`): `Promise`\<`void`> Download a directory's contents from the Sailbox into a local directory. `guestDir`'s entries land inside `localDir`, 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. ##### Parameters | Parameter | Type | | --------------- | ------------------------------------------------ | | `dirs` | \{ `guestDir`: `string`; `localDir`: `string`; } | | `dirs.guestDir` | `string` | | `dirs.localDir` | `string` | ##### Returns `Promise`\<`void`> #### exists() > **exists**(`path`, `options?`): `Promise`\<`boolean`> Whether `path` exists in the guest. Follows symlinks (like `test -e`), so a dangling symlink reports `false` even though [ls](#ls) lists it. A `user` reports existence as observable by that user: a path the user lacks permission to reach also reports `false`. ##### Parameters | Parameter | Type | | ---------- | ------------------------- | | `path` | `string` | | `options?` | [`FsOptions`](#fsoptions) | ##### Returns `Promise`\<`boolean`> #### ls() > **ls**(`path`, `options?`): `Promise`\<[`DirEntry`](#direntry)\[]> List a directory's immediate entries as [DirEntry](#direntry) records (no recursion). A missing path throws, 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 fails with a permission error. ##### Parameters | Parameter | Type | | ---------- | ------------------------- | | `path` | `string` | | `options?` | [`FsOptions`](#fsoptions) | ##### Returns `Promise`\<[`DirEntry`](#direntry)\[]> #### mkdir() > **mkdir**(`path`, `options?`): `Promise`\<`void`> 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. ##### Parameters | Parameter | Type | | ---------- | ------------------------- | | `path` | `string` | | `options?` | [`FsOptions`](#fsoptions) | ##### Returns `Promise`\<`void`> #### read() > **read**(`path`): `Promise`\<`Buffer`\<`ArrayBufferLike`>> Read a guest file fully into memory (convenience over [readStream](#readstream-1)). ##### Parameters | Parameter | Type | | --------- | -------- | | `path` | `string` | ##### Returns `Promise`\<`Buffer`\<`ArrayBufferLike`>> #### readStream() > **readStream**(`path`): `Promise`\<[`FileStream`](#filestream)> Open a streaming read of a guest file. ##### Parameters | Parameter | Type | | --------- | -------- | | `path` | `string` | ##### Returns `Promise`\<[`FileStream`](#filestream)> #### remove() > **remove**(`path`, `options?`): `Promise`\<`void`> 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. ##### Parameters | Parameter | Type | | ---------- | ------------------------- | | `path` | `string` | | `options?` | [`FsOptions`](#fsoptions) | ##### Returns `Promise`\<`void`> #### uploadDir() > **uploadDir**(`dirs`): `Promise`\<`void`> Upload a local directory's contents into a directory on the Sailbox. `localDir`'s entries land inside `guestDir`, 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. `guestDir` 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. ##### Parameters | Parameter | Type | | --------------- | ------------------------------------------------------------------- | | `dirs` | \{ `guestDir`: `string`; `localDir`: `string`; `user?`: `string`; } | | `dirs.guestDir` | `string` | | `dirs.localDir` | `string` | | `dirs.user?` | `string` | ##### Returns `Promise`\<`void`> #### write() > **write**(`path`, `data`, `options?`): `Promise`\<`void`> Write `data` to a guest file: [writeFiles](#writefiles-1) with one entry. Strings use UTF-8. Missing parent directories are created unless `createParents` is false. Use [writeStream](#writestream-1) to stream a large source. ##### Parameters | Parameter | Type | | ---------- | ---------------------------------------------------------------------------- | | `path` | `string` | | `data` | `string` \| `Buffer`\<`ArrayBufferLike`> \| `Uint8Array`\<`ArrayBufferLike`> | | `options?` | [`WriteOptions`](#writeoptions) | ##### Returns `Promise`\<`void`> #### writeFiles() > **writeFiles**(`files`, `options?`): `Promise`\<`void`> Write several complete files in one call. `files` maps each absolute guest path to its contents (strings use UTF-8). Each file is its own request, up to eight at a time, and every file gets the same options. 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 [writeStream](#writestream-1) to stream a large source. ##### Parameters | Parameter | Type | | ---------- | ---------------------------------------------------------------------- | | `files` | `Readonly`\<`Record`\<`string`, `Buffer` \| `Uint8Array` \| `string`>> | | `options?` | [`WriteOptions`](#writeoptions) | ##### Returns `Promise`\<`void`> #### writeStream() > **writeStream**(`path`, `options?`): `Promise`\<[`FileWriter`](#filewriter)> Open a streaming upload to a guest file. ##### Parameters | Parameter | Type | | ---------- | ------------------------------- | | `path` | `string` | | `options?` | [`WriteOptions`](#writeoptions) | ##### Returns `Promise`\<[`FileWriter`](#filewriter)> *** ## FileWriter A streaming write to a guest file. Push chunks with [write](#write), then confirm with [finish](#finish); only `finish` commits the write. A writer that goes away without finishing ([abort](#abort), an error path, or garbage collection) cancels the transfer instead; the guest file state is then unspecified. ### Methods #### \[asyncDispose]\() > **\[asyncDispose]**(): `Promise`\<`void`> `await using` support; same semantics as the synchronous form. ##### Returns `Promise`\<`void`> #### \[dispose]\() > **\[dispose]**(): `void` `using` support: aborts the write if it was never finished, so leaving scope on an error path cancels instead of committing a partial file. `abort` is synchronous, so the plain form suffices. ##### Returns `void` #### abort() > **abort**(): `void` Abort the write: cancel the request so the server does not commit it. Idempotent. A later [finish](#finish) reports the abort instead of succeeding; the guest file state after an abort is unspecified. ##### Returns `void` #### finish() > **finish**(): `Promise`\<`void`> Confirm the write, creating an empty file if nothing was written. ##### Returns `Promise`\<`void`> #### toWritable() > **toWritable**(): `Writable` Adapt to a Node `Writable`: `end()` runs [finish](#finish) (only that commits the write), destroying the stream aborts it, and backpressure follows the underlying transfer since each chunk's callback fires when its [write](#write) resolves. ##### Returns `Writable` #### write() > **write**(`data`): `Promise`\<`void`> Write bytes (a `string` is encoded as UTF-8). The SDK splits them into transport-sized chunks. ##### Parameters | Parameter | Type | | --------- | ---------------------------------------------------------------------------- | | `data` | `string` \| `Buffer`\<`ArrayBufferLike`> \| `Uint8Array`\<`ArrayBufferLike`> | ##### Returns `Promise`\<`void`> *** ## FileStream An async-iterable download of a guest file. Chunks are `Buffer`s; iteration ends at end of file. The underlying stream is released when iteration finishes or is abandoned (via a generator `finally`), or explicitly via [close](#close-1). ### Implements * `AsyncIterable`\<`Buffer`> ### Methods #### \[asyncDispose]\() > **\[asyncDispose]**(): `Promise`\<`void`> `await using` support. ##### Returns `Promise`\<`void`> #### \[asyncIterator]\() > **\[asyncIterator]**(): `AsyncIterator`\<`Buffer`\<`ArrayBufferLike`>> ##### Returns `AsyncIterator`\<`Buffer`\<`ArrayBufferLike`>> ##### Implementation of `AsyncIterable.[asyncIterator]` #### bytes() > **bytes**(): `Promise`\<`Buffer`\<`ArrayBufferLike`>> Collect the whole file into a single `Buffer`. ##### Returns `Promise`\<`Buffer`\<`ArrayBufferLike`>> #### close() > **close**(): `Promise`\<`void`> Release the underlying download stream (idempotent). ##### Returns `Promise`\<`void`> #### toReadable() > **toReadable**(): `Readable` Adapt to a Node `Readable`. ##### Returns `Readable` *** ## Volume A managed NFS volume that can be mounted into Sailboxes. Look one up (or mint it) with [Volume.find](#find-1), then pass it (or its [Volume.id](#id-2)) in a Sailbox's `volumes` mapping. 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). ### Properties | Property | Modifier | Type | Default value | Description | | ------------ | ---------- | ----------------------- | ------------- | ----------------------------------------------------------------- | | `backend` | `readonly` | `string` | `undefined` | Storage backend serving the volume. | | `createdAt` | `readonly` | `Date` \| `undefined` | `undefined` | Creation time, if reported. | | `id` | `readonly` | `string` | `undefined` | Stable volume id. | | `mountPath` | `readonly` | `string` \| `undefined` | `undefined` | Guest mount path, when loaded via [Volume.fromMount](#frommount). | | `name` | `readonly` | `string` | `undefined` | Volume name. | | `status` | `readonly` | `string` | `undefined` | Lifecycle status. | | `updatedAt` | `readonly` | `Date` \| `undefined` | `undefined` | Last-update time, if reported. | ### Methods #### delete() > **delete**(`options?`): `Promise`\<`boolean`> Delete this volume. Resolves `true` if it was deleted, `false` if it was already gone (only possible with `allowMissing`). ##### Parameters | Parameter | Type | | --------- | --------------------------------------------- | | `options` | [`DeleteVolumeOptions`](#deletevolumeoptions) | ##### Returns `Promise`\<`boolean`> #### find() > `static` **find**(`name`, `options?`): `Promise`\<[`Volume`](#volume)> Look up an NFS volume by name, optionally minting it if missing. ##### Parameters | Parameter | Type | | --------- | ----------------------------------------- | | `name` | `string` | | `options` | [`FindVolumeOptions`](#findvolumeoptions) | ##### Returns `Promise`\<[`Volume`](#volume)> #### fromMount() > `static` **fromMount**(`path`): [`Volume`](#volume) Guest-side: load the volume handle for a path mounted into this Sailbox (reads the mount's metadata; only available inside a guest). ##### Parameters | Parameter | Type | | --------- | -------- | | `path` | `string` | ##### Returns [`Volume`](#volume) #### list() > `static` **list**(`options?`): `Promise`\<[`Volume`](#volume)\[]> List NFS volumes in the current org. ##### Parameters | Parameter | Type | | --------- | ------------------------------------------- | | `options` | [`ListVolumesOptions`](#listvolumesoptions) | ##### Returns `Promise`\<[`Volume`](#volume)\[]> *** ## HTTP policies See [Credential injection](/sailboxes-credentials) for setup, examples, and cleanup. The entries below list the available TypeScript calls. ### 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. #### Properties | Property | Modifier | Type | Description | | ------------ | ---------- | -------- | --------------------------------------------------- | | `createdAt` | `readonly` | `Date` | When the secret was first set. | | `name` | `readonly` | `string` | The secret's name, unique within your organization. | | `updatedAt` | `readonly` | `Date` | When the secret's value last changed. | #### Methods ##### delete() > **delete**(): `Promise`\<`void`> Delete this secret. A secret cannot be deleted while an HTTP policy refers to it; throws [SecretInUseError](#secretinuseerror) until every referencing policy is deleted. Summaries from [HttpPolicy.list](#list-1) include the secret names they use. ###### Returns `Promise`\<`void`> ##### deleteByName() > `static` **deleteByName**(`name`, `options?`): `Promise`\<`void`> Delete the named secret. Same contract as [Secret.delete](#delete-1). ###### Parameters | Parameter | Type | | --------- | --------------------------------- | | `name` | `string` | | `options` | [`ClientOptions`](#clientoptions) | ###### Returns `Promise`\<`void`> ##### get() > `static` **get**(`name`, `options?`): `Promise`\<[`Secret`](#secret)> Fetch one secret's name and timestamps. The value is never returned. Throws [NotFoundError](#notfounderror) when no secret has that name. ###### Parameters | Parameter | Type | | --------- | --------------------------------- | | `name` | `string` | | `options` | [`ClientOptions`](#clientoptions) | ###### Returns `Promise`\<[`Secret`](#secret)> ##### list() > `static` **list**(`options?`): `Promise`\<[`Secret`](#secret)\[]> List your organization's secret names and timestamps, sorted by name. ###### Parameters | Parameter | Type | | --------- | --------------------------------- | | `options` | [`ClientOptions`](#clientoptions) | ###### Returns `Promise`\<[`Secret`](#secret)\[]> ##### set() > `static` **set**(`name`, `value`, `options?`): `Promise`\<[`Secret`](#secret)> Set (create or update) the named secret's value. An HTTP policy inserts it with `${secrets.NAME}`. 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. ###### Parameters | Parameter | Type | | --------- | --------------------------------- | | `name` | `string` | | `value` | `string` | | `options` | [`ClientOptions`](#clientoptions) | ###### Returns `Promise`\<[`Secret`](#secret)> *** ### HttpPolicy Rules that shape the HTTPS requests your Sailboxes send. A policy is a named document owned by your organization. Obtain one from [HttpPolicy.create](#create) or [HttpPolicy.get](#get); do not construct it directly. The document cannot change after creation, but [HttpPolicy.rename](#rename) can change its name. #### Properties | Property | Modifier | Type | Description | | ------------ | ---------- | --------------------------------------------- | ------------------------------------------------------------------------------------ | | `createdAt` | `readonly` | `Date` | When the policy was created. | | `document` | `readonly` | [`HttpPolicyDocument`](#httppolicydocument-1) | The saved policy document: Sail's normalized form of the document given at creation. | | `id` | `readonly` | `string` | The policy's stable identifier. | | `name` | `readonly` | `string` | The policy's name (the only mutable field). | | `updatedAt` | `readonly` | `Date` | When the policy's name last changed. | #### Methods ##### delete() > **delete**(): `Promise`\<`void`> Delete the policy. A policy still attached to a Sailbox cannot be deleted; throws [HttpPolicyInUseError](#httppolicyinuseerror) until every Sailbox clears or replaces it. ###### Returns `Promise`\<`void`> ##### rename() > **rename**(`name`): `Promise`\<[`HttpPolicy`](#httppolicy)> Rename the policy and resolve the updated policy object. The document cannot change; create a new policy to change behavior. Names follow the same rules as [HttpPolicy.create](#create). ###### Parameters | Parameter | Type | | --------- | -------- | | `name` | `string` | ###### Returns `Promise`\<[`HttpPolicy`](#httppolicy)> ##### create() > `static` **create**(`name`, `document`, `options?`): `Promise`\<[`HttpPolicy`](#httppolicy)> Create a policy from `document`. Every `${secrets.NAME}` in the document must name a secret that already exists. An invalid document throws [InvalidArgumentError](#invalidargumenterror) naming 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. ###### Parameters | Parameter | Type | | ---------- | --------------------------------------------- | | `name` | `string` | | `document` | [`HttpPolicyDocument`](#httppolicydocument-1) | | `options` | [`ClientOptions`](#clientoptions) | ###### Returns `Promise`\<[`HttpPolicy`](#httppolicy)> ##### get() > `static` **get**(`policyIdentifier`, `options?`): `Promise`\<[`HttpPolicy`](#httppolicy)> Fetch one policy by id, document included. Throws [NotFoundError](#notfounderror) when no policy has that id. ###### Parameters | Parameter | Type | | ------------------ | --------------------------------- | | `policyIdentifier` | `string` | | `options` | [`ClientOptions`](#clientoptions) | ###### Returns `Promise`\<[`HttpPolicy`](#httppolicy)> ##### list() > `static` **list**(`options?`): `Promise`\<[`HttpPolicySummary`](#httppolicysummary)\[]> List your organization's policies as summaries, without documents. Fetch a policy's document with [HttpPolicy.get](#get). ###### Parameters | Parameter | Type | | --------- | ----------------------------------------------------- | | `options` | [`ListHttpPoliciesOptions`](#listhttppoliciesoptions) | ###### Returns `Promise`\<[`HttpPolicySummary`](#httppolicysummary)\[]> *** ## ingressAuthHeaders() > **ingressAuthHeaders**(): `Record`\<`string`, `string`> Guest-side: headers that authenticate this Sailbox as an ingress allowlist source (only available inside a Sailbox guest). ### Returns `Record`\<`string`, `string`> *** ## Client A configured Sail client: the low-level, one-method-per-operation surface (one config snapshot; env vars are read at construction). Every client operation is here. The object-model API ([Sailbox](#sailbox), [App](#app), [Volume](#volume)) is built on top of it. Construct with [Client.fromEnv](#fromenv) or [Client.fromConfig](#fromconfig). ### Methods #### buildImageDefinition() > **buildImageDefinition**(`def`, `timeoutSeconds`, `options?`): `Promise`\<[`ImageSpec`](#imagespec)> Resolve an image definition and build it to ready, returning the content-addressed [ImageSpec](#imagespec) to create Sailboxes from. A bare Debian or devbox base image skips the build; `timeoutSeconds` bounds the whole pipeline (hashing, uploads, and the build). By default, Sail may reuse an existing ready build for this definition. `forceBuild` builds it again and waits for the fresh build to become ready: 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 with [Image.fromRegistry](#fromregistry) through a 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 specs built earlier keep their pinned version. A forced build of an image built with [Image.fromDockerfile](#fromdockerfile) looks up the tags its `FROM` and `COPY --from` instructions name and moves those pins for your whole organization, while specs 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. ##### Parameters | Parameter | Type | | --------------------- | ------------------------------------- | | `def` | [`ImageDefinition`](#imagedefinition) | | `timeoutSeconds` | `number` | | `options` | \{ `forceBuild?`: `boolean`; } | | `options.forceBuild?` | `boolean` | ##### Returns `Promise`\<[`ImageSpec`](#imagespec)> #### buildSpecToReady() > **buildSpecToReady**(`spec`, `timeoutSeconds`, `options?`): `Promise`\<[`ImageBuild`](#imagebuild-1)> Build an already-resolved spec to ready (submit + poll), bounded by `timeoutSeconds`. `forceBuild` builds the image again even if a build already exists; see [Client.buildImageDefinition](#buildimagedefinition). ##### Parameters | Parameter | Type | | --------------------- | ------------------------------ | | `spec` | [`ImageSpec`](#imagespec) | | `timeoutSeconds` | `number` | | `options` | \{ `forceBuild?`: `boolean`; } | | `options.forceBuild?` | `boolean` | ##### Returns `Promise`\<[`ImageBuild`](#imagebuild-1)> #### checkpointSailbox() > **checkpointSailbox**(`sailboxId`, `options?`): `Promise`\<[`SailboxCheckpoint`](#sailboxcheckpoint-1)> Take a checkpoint of a Sailbox. `name` sets the handle's display name; `ttlSeconds`, when given, overrides the server's default retention window. ##### Parameters | Parameter | Type | | ----------- | ----------------------------------------- | | `sailboxId` | `string` | | `options` | [`CheckpointOptions`](#checkpointoptions) | ##### Returns `Promise`\<[`SailboxCheckpoint`](#sailboxcheckpoint-1)> #### clearSailboxHttpPolicy() > **clearSailboxHttpPolicy**(`sailboxId`): `Promise`\<`void`> Clear a Sailbox's attached HTTP policy. The change applies to HTTPS connections the Sailbox opens after the call; connections already open keep the previous policy until they close. This also succeeds when no policy is attached. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<`void`> #### createFromCheckpoint() > **createFromCheckpoint**(`params`): `Promise`\<[`SailboxHandle`](#sailboxhandle)> Create a new Sailbox from a checkpoint. ##### Parameters | Parameter | Type | | --------- | ------------------------------------------------- | | `params` | [`FromCheckpointRequest`](#fromcheckpointrequest) | ##### Returns `Promise`\<[`SailboxHandle`](#sailboxhandle)> #### createHttpPolicy() > **createHttpPolicy**(`name`, `document`): `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo)> Create an HTTP policy from JSON-encoded text. The document cannot change after creation. Most callers should use [HttpPolicy.create](#create), which accepts an object and documents the name rules. ##### Parameters | Parameter | Type | | ---------- | -------- | | `name` | `string` | | `document` | `string` | ##### Returns `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo)> #### createSailbox() > **createSailbox**(`req`, `timeoutSeconds?`): `Promise`\<[`SailboxHandle`](#sailboxhandle)> Create a Sailbox. `timeoutSeconds` bounds each create attempt (default 600s); pass `0` for no client-side timeout. A timed-out attempt is retried, and a retry usually reattaches to the Sailbox already coming up rather than starting another. When the overall budget is exhausted the Sailbox may still be coming up server-side: find or terminate it by `name`. `image` defaults to a plain Debian base. ##### Parameters | Parameter | Type | Default value | | ---------------- | ----------------------------------------------- | ------------- | | `req` | [`CreateSailboxRequest`](#createsailboxrequest) | `undefined` | | `timeoutSeconds` | `number` | `600` | ##### Returns `Promise`\<[`SailboxHandle`](#sailboxhandle)> #### deleteHttpPolicy() > **deleteHttpPolicy**(`policyId`): `Promise`\<`void`> Delete an HTTP policy by id. While the policy is attached to a Sailbox, the call fails with a 409 [ApiError](#apierror) ([HttpPolicy.delete](#delete) maps that to [HttpPolicyInUseError](#httppolicyinuseerror)). ##### Parameters | Parameter | Type | | ---------- | -------- | | `policyId` | `string` | ##### Returns `Promise`\<`void`> #### deleteSecret() > **deleteSecret**(`name`): `Promise`\<`void`> Delete a secret by name. While an HTTP policy refers to it, the call fails with a 409 [ApiError](#apierror) ([Secret.delete](#delete-1) maps that to [SecretInUseError](#secretinuseerror)). ##### Parameters | Parameter | Type | | --------- | -------- | | `name` | `string` | ##### Returns `Promise`\<`void`> #### deleteVolume() > **deleteVolume**(`volumeId`, `allowMissing?`): `Promise`\<[`VolumeInfo`](#volumeinfo) | `null`> Delete a volume by id. `allowMissing` tolerates an already-deleted volume, resolving `null` instead of throwing. ##### Parameters | Parameter | Type | Default value | | -------------- | --------- | ------------- | | `volumeId` | `string` | `undefined` | | `allowMissing` | `boolean` | `false` | ##### Returns `Promise`\<[`VolumeInfo`](#volumeinfo) | `null`> #### downloadDir() > **downloadDir**(`sailboxId`, `dirs`): `Promise`\<`void`> Download a guest directory's contents into a local directory, named in `dirs`. ##### Parameters | Parameter | Type | | --------------- | ------------------------------------------------ | | `sailboxId` | `string` | | `dirs` | \{ `guestDir`: `string`; `localDir`: `string`; } | | `dirs.guestDir` | `string` | | `dirs.localDir` | `string` | ##### Returns `Promise`\<`void`> #### enableSsh() > **enableSsh**(`sailboxId`, `options?`): `Promise`\<[`SshEndpoint`](#sshendpoint) | `null`> Enable SSH on a Sailbox: trust the org SSH CA, start `sshd`, and expose guest port 22 as TCP once the CA-only daemon owns it. A non-empty `allowlist` restricts port 22 to those source addresses or ranges, replacing any existing restriction. With `wait` (the default), polls until the endpoint is reachable and returns it, throwing [TimeoutError](#timeouterror) if it is not within `timeoutSeconds`; with `wait: false`, skips the probe and resolves `null`. ##### Parameters | Parameter | Type | | ----------- | --------------------------------------- | | `sailboxId` | `string` | | `options` | [`EnableSshOptions`](#enablesshoptions) | ##### Returns `Promise`\<[`SshEndpoint`](#sshendpoint) | `null`> #### exec() > **exec**(`sailboxId`, `command`, `options?`): `Promise`\<[`ExecProcess`](#execprocess)> Run a command in a Sailbox and return a handle to the live process. A `string` command is run via `/bin/sh -lc`; a `string[]` is exec'd directly. By default a stream you are consuming 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 consuming keeps only its most recent 1 MiB; `outputMode` and `outputBufferBytes` change that (see [ExecProcess](#execprocess) and [ExecOptions](#execoptions)). `cwd`/`background` apply to string commands (see [ExecOptions](#execoptions)). Stopping the command is the caller's job via [ExecProcess.cancel](#cancel). ##### Parameters | Parameter | Type | | ----------- | -------------------------------- | | `sailboxId` | `string` | | `command` | `string` \| readonly `string`\[] | | `options` | [`ExecOptions`](#execoptions) | ##### Returns `Promise`\<[`ExecProcess`](#execprocess)> #### exposeListener() > **exposeListener**(`sailboxId`, `guestPort`, `protocol?`, `allowlist?`): `Promise`\<[`Listener`](#listener-1)> Expose a guest port at runtime. Re-exposing a port under the same protocol sets its `allowlist` to what you pass, so pass the whole list every time; an empty one clears the restriction and reopens the port. The route status starts "unknown": the response confirms configuration, not reachability. ##### Parameters | Parameter | Type | Default value | | ----------- | ------------------------------------- | ------------- | | `sailboxId` | `string` | `undefined` | | `guestPort` | `number` | `undefined` | | `protocol` | [`IngressProtocol`](#ingressprotocol) | `"http"` | | `allowlist` | readonly `string`\[] | `[]` | ##### Returns `Promise`\<[`Listener`](#listener-1)> #### findApp() > **findApp**(`name`, `mintIfMissing?`): `Promise`\<[`AppInfo`](#appinfo)> Find an app by name; `mintIfMissing` creates it when absent. ##### Parameters | Parameter | Type | Default value | | --------------- | --------- | ------------- | | `name` | `string` | `undefined` | | `mintIfMissing` | `boolean` | `false` | ##### Returns `Promise`\<[`AppInfo`](#appinfo)> #### getHttpPolicy() > **getHttpPolicy**(`policyId`): `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo)> Fetch one HTTP policy by id, including its document. ##### Parameters | Parameter | Type | | ---------- | -------- | | `policyId` | `string` | ##### Returns `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo)> #### getListener() > **getListener**(`sailboxId`, `guestPort`): `Promise`\<[`Listener`](#listener-1)> Fetch one listener by guest port without waking the Sailbox. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `guestPort` | `number` | ##### Returns `Promise`\<[`Listener`](#listener-1)> #### getSailbox() > **getSailbox**(`sailboxId`): `Promise`\<[`SailboxInfo`](#sailboxinfo)> Fetch one Sailbox by id. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<[`SailboxInfo`](#sailboxinfo)> #### getSecret() > **getSecret**(`name`): `Promise`\<[`SecretInfo`](#secretinfo)> Fetch one secret's name and timestamps, never its value. ##### Parameters | Parameter | Type | | --------- | -------- | | `name` | `string` | ##### Returns `Promise`\<[`SecretInfo`](#secretinfo)> #### getVolume() > **getVolume**(`name`, `mintIfMissing?`): `Promise`\<[`VolumeInfo`](#volumeinfo)> Look up an NFS volume by name; `mintIfMissing` creates it when absent. ##### Parameters | Parameter | Type | Default value | | --------------- | --------- | ------------- | | `name` | `string` | `undefined` | | `mintIfMissing` | `boolean` | `false` | ##### Returns `Promise`\<[`VolumeInfo`](#volumeinfo)> #### ingressAuthHeaders() > **ingressAuthHeaders**(`sailboxId`): `Promise`\<`Record`\<`string`, `string`>> Ingress-identity headers for this Sailbox, as a name→value map. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<`Record`\<`string`, `string`>> #### listApps() > **listApps**(): `Promise`\<[`AppInfo`](#appinfo)\[]> Every app the current org owns, newest first. ##### Returns `Promise`\<[`AppInfo`](#appinfo)\[]> #### listDir() > **listDir**(`sailboxId`, `path`, `user?`): `Promise`\<[`DirEntry`](#direntry)\[]> List a directory's immediate entries as structured records. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `path` | `string` | | `user?` | `string` | ##### Returns `Promise`\<[`DirEntry`](#direntry)\[]> #### listHttpPolicies() > **listHttpPolicies**(`params`): `Promise`\<[`HttpPolicyPage`](#httppolicypage)> List HTTP policies with paging and an optional id or name search. ##### Parameters | Parameter | Type | | ---------------- | ---------------------------------------------------------------- | | `params` | \{ `limit`: `number`; `offset`: `number`; `search?`: `string`; } | | `params.limit` | `number` | | `params.offset` | `number` | | `params.search?` | `string` | ##### Returns `Promise`\<[`HttpPolicyPage`](#httppolicypage)> #### listListeners() > **listListeners**(`sailboxId`): `Promise`\<[`Listener`](#listener-1)\[]> List a Sailbox's listeners without waking it. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<[`Listener`](#listener-1)\[]> #### listSailboxes() > **listSailboxes**(`params?`): `Promise`\<[`SailboxInfoPage`](#sailboxinfopage)> List one page of Sailboxes in the current org. ##### Parameters | Parameter | Type | | --------- | ------------------------------------------- | | `params` | [`ListSailboxesQuery`](#listsailboxesquery) | ##### Returns `Promise`\<[`SailboxInfoPage`](#sailboxinfopage)> #### listSecrets() > **listSecrets**(): `Promise`\<[`SecretInfo`](#secretinfo)\[]> List the organization's secret names and timestamps. ##### Returns `Promise`\<[`SecretInfo`](#secretinfo)\[]> #### listVolumes() > **listVolumes**(`maxObjects?`): `Promise`\<[`VolumeInfo`](#volumeinfo)\[]> List NFS volumes in the current org. ##### Parameters | Parameter | Type | | ------------- | -------- | | `maxObjects?` | `number` | ##### Returns `Promise`\<[`VolumeInfo`](#volumeinfo)\[]> #### makeDir() > **makeDir**(`sailboxId`, `path`, `user?`): `Promise`\<`void`> Create a directory and any missing parents (like `mkdir -p`); a no-op if it already exists. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `path` | `string` | | `user?` | `string` | ##### Returns `Promise`\<`void`> #### orgSshCaPublicKey() > **orgSshCaPublicKey**(): `Promise`\<`string`> Fetch (creating on first use) the org SSH certificate authority public key. Used to preflight SSH before a Sailbox is provisioned. ##### Returns `Promise`\<`string`> #### pathExists() > **pathExists**(`sailboxId`, `path`, `user?`): `Promise`\<`boolean`> Whether `path` exists in the guest. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `path` | `string` | | `user?` | `string` | ##### Returns `Promise`\<`boolean`> #### pauseSailbox() > **pauseSailbox**(`sailboxId`): `Promise`\<`void`> Pause a Sailbox in memory. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<`void`> #### readStream() > **readStream**(`sailboxId`, `path`): `Promise`\<[`FileStream`](#filestream)> Open a streaming read of a guest file. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `path` | `string` | ##### Returns `Promise`\<[`FileStream`](#filestream)> #### removePath() > **removePath**(`sailboxId`, `path`, `user?`): `Promise`\<`void`> Remove a file or directory tree (like `rm -rf`); a no-op if it is already absent. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `path` | `string` | | `user?` | `string` | ##### Returns `Promise`\<`void`> #### renameHttpPolicy() > **renameHttpPolicy**(`policyId`, `name`): `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo)> Rename an HTTP policy. Its document stays unchanged. Names follow the same rules as [HttpPolicy.create](#create). ##### Parameters | Parameter | Type | | ---------- | -------- | | `policyId` | `string` | | `name` | `string` | ##### Returns `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo)> #### resolveImage() > **resolveImage**(`def`): `Promise`\<[`ImageSpec`](#imagespec)> Resolve an image definition into a content-addressed [ImageSpec](#imagespec): the SDK walks local directories (gitignore-style `ignore`), hashes every file, and uploads content the server does not already have. ##### Parameters | Parameter | Type | | --------- | ------------------------------------- | | `def` | [`ImageDefinition`](#imagedefinition) | ##### Returns `Promise`\<[`ImageSpec`](#imagespec)> #### resumeSailbox() > **resumeSailbox**(`sailboxId`): `Promise`\<[`SailboxHandle`](#sailboxhandle)> Resume a paused or sleeping Sailbox. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<[`SailboxHandle`](#sailboxhandle)> #### sailboxHttpPolicy() > **sailboxHttpPolicy**(`sailboxId`): `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo) | `null`> The HTTP policy attached to a Sailbox, or `null` when none is attached. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<[`HttpPolicyInfo`](#httppolicyinfo) | `null`> #### setSailboxAutoSleep() > **setSailboxAutoSleep**(`sailboxId`, `autoSleep`): `Promise`\<`void`> Replace when Sail may sleep a Sailbox on its own. Each call replaces the whole setting: switching to `{ automatic: false }` clears any minimum wait set earlier. Most callers use [Sailbox.setAutoSleep](#setautosleep). ##### Parameters | Parameter | Type | | ----------- | --------------------------- | | `sailboxId` | `string` | | `autoSleep` | [`AutoSleep`](#autosleep-4) | ##### Returns `Promise`\<`void`> #### setSailboxHttpPolicy() > **setSailboxHttpPolicy**(`sailboxId`, `policyId`): `Promise`\<`void`> Attach an HTTP policy to a Sailbox, replacing any previous one. The policy applies to HTTPS connections the Sailbox opens after the call; connections already open keep the previous policy until they close. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `policyId` | `string` | ##### Returns `Promise`\<`void`> #### setSecret() > **setSecret**(`name`, `value`): `Promise`\<[`SecretInfo`](#secretinfo)> Set (create or update) an organization secret. Sail never returns the stored value. After this resolves, the next matching request from a Sailbox whose attached HTTP policy uses the secret gets the new value. Names and values follow the same rules as [Secret.set](#set). ##### Parameters | Parameter | Type | | --------- | -------- | | `name` | `string` | | `value` | `string` | ##### Returns `Promise`\<[`SecretInfo`](#secretinfo)> #### shell() > **shell**(`sailboxId`, `command?`, `options?`): `Promise`\<`number`> Open an interactive pty session on a Sailbox, bridged to the local terminal: raw keystrokes reach the remote process, output renders locally, and resizes propagate. Resolves with the remote process's exit code. Requires an interactive terminal (stdin and stdout TTYs). ##### Parameters | Parameter | Type | | ----------- | ------------------------------- | | `sailboxId` | `string` | | `command?` | `string` | | `options?` | [`ShellOptions`](#shelloptions) | ##### Returns `Promise`\<`number`> #### sleepSailbox() > **sleepSailbox**(`sailboxId`, `wakeAt?`): `Promise`\<`string` | `null`> Sleep a Sailbox to disk (wakes on traffic), optionally scheduling a wall-clock wake first. `wakeAt` is an RFC 3339 timestamp; the returned value is the effective (sooner) wake time, or null when no wake was requested. Most callers use [Sailbox.sleep](#sleep), which takes and returns `Date`. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `wakeAt?` | `string` | ##### Returns `Promise`\<`string` | `null`> #### terminateSailbox() > **terminateSailbox**(`sailboxId`): `Promise`\<`void`> Terminate a Sailbox (idempotent). ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<`void`> #### unexposeListener() > **unexposeListener**(`sailboxId`, `guestPort`): `Promise`\<`void`> Remove a runtime ingress port. ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | | `guestPort` | `number` | ##### Returns `Promise`\<`void`> #### upgradeSailbox() > **upgradeSailbox**(`sailboxId`): `Promise`\<[`UpgradeResult`](#upgraderesult)> Upgrade a Sailbox's runtime (now if running, else at next wake). ##### Parameters | Parameter | Type | | ----------- | -------- | | `sailboxId` | `string` | ##### Returns `Promise`\<[`UpgradeResult`](#upgraderesult)> #### uploadDir() > **uploadDir**(`sailboxId`, `dirs`): `Promise`\<`void`> Upload a local directory's contents into a guest directory, named in `dirs`. `user` gives the uploaded entries to that user instead of the image's `USER`. ##### Parameters | Parameter | Type | | --------------- | ------------------------------------------------------------------- | | `sailboxId` | `string` | | `dirs` | \{ `guestDir`: `string`; `localDir`: `string`; `user?`: `string`; } | | `dirs.guestDir` | `string` | | `dirs.localDir` | `string` | | `dirs.user?` | `string` | ##### Returns `Promise`\<`void`> #### waitForListener() > **waitForListener**(`sailboxId`, `guestPort`, `timeoutSeconds`): `Promise`\<[`Listener`](#listener-1)> Block until the listener on `guestPort` is reachable end to end and return it, throwing [TimeoutError](#timeouterror) after `timeoutSeconds`. An HTTP listener is ready once the guest server answers; a TCP listener once the guest sends bytes or holds the connection open. A connectivity check, not an application health check. ##### Parameters | Parameter | Type | | ---------------- | -------- | | `sailboxId` | `string` | | `guestPort` | `number` | | `timeoutSeconds` | `number` | ##### Returns `Promise`\<[`Listener`](#listener-1)> #### writeFiles() > **writeFiles**(`sailboxId`, `files`, `options?`): `Promise`\<`void`> Write several complete files in one call, up to eight at a time, stopping at the first failure. ##### Parameters | Parameter | Type | | ----------- | ---------------------------------------------------------------------- | | `sailboxId` | `string` | | `files` | `Readonly`\<`Record`\<`string`, `Buffer` \| `Uint8Array` \| `string`>> | | `options` | [`WriteOptions`](#writeoptions) | ##### Returns `Promise`\<`void`> #### writeStream() > **writeStream**(`sailboxId`, `path`, `options?`): `Promise`\<[`FileWriter`](#filewriter)> Open a streaming upload to a guest file. ##### Parameters | Parameter | Type | | ----------- | ------------------------------- | | `sailboxId` | `string` | | `path` | `string` | | `options` | [`WriteOptions`](#writeoptions) | ##### Returns `Promise`\<[`FileWriter`](#filewriter)> #### fromConfig() > `static` **fromConfig**(`config`): [`Client`](#client) Build a client from an explicit [ClientConfig](#clientconfig). ##### Parameters | Parameter | Type | | --------- | ------------------------------- | | `config` | [`ClientConfig`](#clientconfig) | ##### Returns [`Client`](#client) #### fromEnv() > `static` **fromEnv**(): [`Client`](#client) Build a client from the environment (`SAIL_API_KEY`, ...). ##### Returns [`Client`](#client) *** ## defaultClient() > **defaultClient**(): [`Client`](#client) The process-wide client used by the object-model statics ([Sailbox](#sailbox), [App](#app), [Volume](#volume)) when no explicit `client` is passed. Created lazily from the environment on first use. ### Returns [`Client`](#client) *** ## setDefaultClient() > **setDefaultClient**(`client`): `void` Override (or clear, with `undefined`) the process-wide default client. Useful for tests or to point the object-model API at an explicitly configured client. ### Parameters | Parameter | Type | | --------- | ---------------------------------- | | `client` | [`Client`](#client) \| `undefined` | ### Returns `void` *** ## resolveConfig() > **resolveConfig**(): [`ResolvedConfig`](#resolvedconfig) Resolve the SDK config from the environment (`SAIL_API_KEY`, `SAIL_API_URL`, `SAILBOX_API_URL`, ...) and `~/.sail`, without requiring an API key. This is the same resolution a client performs at construction. ### Returns [`ResolvedConfig`](#resolvedconfig) *** ## isSailError() > **isSailError**(`err`): `err is SailError` Whether `err` is a [SailError](#sailerror), matched on the stable shape (`code` string plus `retryable` boolean) rather than the prototype chain. Use it where `instanceof` can lie: across realms (worker threads, `vm` contexts) or when two copies of the SDK are loaded. It does not survive `structuredClone` or `postMessage` serialization, which strip an Error's custom fields; send `{ name, message, code, retryable }` yourself when an error must cross a serialization boundary. ### Parameters | Parameter | Type | | --------- | --------- | | `err` | `unknown` | ### Returns `err is SailError` ## Types Plain data types accepted by and returned from the calls above. ### AddLocalDir A tree of local files copied into the image. #### Properties | Property | Type | Description | | -------------- | ---------------------------------------- | ------------------------------------------ | | `files?` | [`AddLocalDirFile`](#addlocaldirfile)\[] | The files to place under `remotePath`. | | `remotePath?` | `string` | Absolute guest path of the directory root. | *** ### AddLocalDirFile One file within an `addLocalDir` step. #### Properties | Property | Type | Description | | ----------------- | -------- | ----------------------------------------------- | | `contentSha256?` | `string` | SHA-256 of the (already uploaded) file content. | | `mode?` | `number` | Permission bits (low 9). | | `relativePath?` | `string` | Path relative to the directory root. | *** ### AddLocalDirOptions Options for [Image.addLocalDir](#addlocaldir). #### Properties | Property | Type | Description | | -------------- | -------------------- | -------------------------------------------------------------------- | | `ignore?` | readonly `string`\[] | Gitignore-style patterns to skip (e.g. `"*.pyc"`, `"__pycache__/"`). | | `ignoreFile?` | `string` | A gitignore-style file whose patterns to skip (e.g. `.gitignore`). | *** ### AddLocalFile One local file copied into the image, referenced by content hash. #### Properties | Property | Type | Description | | ----------------- | -------- | ------------------------------------------------------------ | | `contentSha256?` | `string` | SHA-256 of the (already uploaded) file content. | | `mode?` | `number` | Permission bits (low 9); 0 means the builder default (0644). | | `remotePath?` | `string` | Absolute guest path to place the file at. | *** ### AddLocalFileOptions Options for [Image.addLocalFile](#addlocalfile). #### Properties | Property | Type | Description | | -------- | -------- | ---------------------------------------------------------------- | | `mode?` | `number` | Unix mode bits (low 9); omitted uses the builder default (0644). | *** ### AppInfo A Sail app. #### Properties | Property | Type | Description | | ------------ | -------- | ------------------------- | | `createdAt` | `string` | Creation time (RFC 3339). | | `id` | `string` | Stable app id. | | `name` | `string` | App name. | *** ### AutoSleep > **AutoSleep** = [`AutomaticSleep`](#automaticsleep) | [`NeverSleep`](#neversleep) 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. 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 `sleep()` yourself is unaffected, and so are `pause`, `resume`, and scheduled wakes. The two forms are alternatives, so `minSecondsBeforeSleep` cannot be combined with turning automatic sleep off. *** ### AutomaticSleep Let Sail decide when to sleep a Sailbox, optionally after a minimum wait. #### Properties | Property | Modifier | Type | Description | | ------------------------- | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `automatic?` | `readonly` | `true` | Sail decides when to sleep it. | | `minSecondsBeforeSleep?` | `readonly` | `number` | Use this idle window instead of Sail's default. Once it passes, Sail may sleep the Sailbox only when it is fully idle. Whole-second values from 1 through 3600 are accepted; 0 restores Sail's default, and other numeric values are rejected. | *** ### BaseImage > **BaseImage** = `"debian"` | `"devbox"` *** ### CancelOptions Options for cancelling an exec. #### Properties | Property | Type | Description | | --------- | --------- | ------------------------------- | | `force?` | `boolean` | Send SIGKILL instead of SIGINT. | *** ### CheckpointOptions Options for [Sailbox.checkpoint](#checkpoint). #### Properties | Property | Type | Description | | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name?` | `string` | Display name for the checkpoint handle. | | `ttlSeconds?` | `number` | Retention override in whole seconds (must be positive). Set it when you keep a checkpoint to reuse as a template, so the handle does not expire while you still need it; omitted uses the server default. | *** ### ClientConfig Explicit client configuration (an alternative to environment resolution). #### Extends * `Omit`\<`native.ClientConfig`, `"mode"`> #### Properties | Property | Type | Description | Inherited from | | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `apiKey` | `string` | Bearer API key. Required. | `Omit.apiKey` | | `apiUrl?` | `string` | Override the Sail API URL. | `Omit.apiUrl` | | `ingressUrl?` | `string` | Override the listener ingress base URL (what `SAILBOX_INGRESS_URL` sets from the environment), for custom or self-hosted Sailbox stacks. | `Omit.ingressUrl` | | `sailboxApiUrl?` | `string` | Override the Sailbox-API URL. | `Omit.sailboxApiUrl` | *** ### ClientOptions Options for statics that select which [Client](#client) to use. #### Extended by * [`FindAppOptions`](#findappoptions) * [`FindVolumeOptions`](#findvolumeoptions) * [`ListVolumesOptions`](#listvolumesoptions) * [`ListHttpPoliciesOptions`](#listhttppoliciesoptions) * [`CreateSailboxOptions`](#createsailboxoptions) * [`FromCheckpointOptions`](#fromcheckpointoptions) * [`ListSailboxesOptions`](#listsailboxesoptions) * [`ListSailboxesPageOptions`](#listsailboxespageoptions) #### Properties | Property | Type | Description | | ---------- | ------------------- | ------------------------------------------------------- | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | *** ### CreateSailboxOptions Options for [Sailbox.create](#create-1): the create request plus a per-attempt timeout and an optional explicit client. `image` defaults to the prebuilt Debian base. #### Extends * `Omit`\<[`CreateSailboxRequest`](#createsailboxrequest), `"image"` | `"appId"` | `"volumeMounts"` | `"ingressPorts"` | `"visibility"`>.[`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Overrides | Inherited from | | ---------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------- | | `app` | `string` \| [`App`](#app) | The owning app, or its id. | - | - | | `autoSleep?` | [`AutoSleep`](#autosleep-4) | When Sail may sleep this Sailbox on its own. Omit for the default. | - | [`CreateSailboxRequest`](#createsailboxrequest).[`autoSleep`](#autosleep-2) | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | - | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `diskLimitGib?` | `number` | Disk limit in whole GiB within the size's range; the size's default when omitted. | - | `Omit.diskLimitGib` | | `image?` | [`ImageSpec`](#imagespec) \| [`Image`](#image) | Image spec, or an [Image](#image) builder (built and resolved at create). Defaults to the prebuilt Debian base, which needs no image build. | - | - | | `imageBuildTimeoutSeconds?` | `number` | Timeout in seconds for building a custom `Image` before create (1800). | `Omit.imageBuildTimeoutSeconds` | - | | `ingressPorts?` | readonly (`number` \| [`IngressPortInput`](#ingressportinput))\[] | Guest ports to expose: a bare number is HTTP shorthand. | - | - | | `memoryLimitGib?` | `number` | Memory limit in whole GiB within the size's range; the size's default when omitted. | - | `Omit.memoryLimitGib` | | `name` | `string` | The Sailbox name. | - | `Omit.name` | | `networkPolicy?` | [`NetworkPolicy`](#networkpolicy-4) | The Sailbox's network policy. Omit for `"public"`. | - | [`CreateSailboxRequest`](#createsailboxrequest).[`networkPolicy`](#networkpolicy-2) | | `size?` | [`SailboxSize`](#sailboxsize) | Resource size; `"m"` when omitted. | - | [`CreateSailboxRequest`](#createsailboxrequest).[`size`](#size-1) | | `timeoutSeconds?` | `number` | Bounds each create attempt (default 600s); pass `0` for no client-side timeout. A timed-out attempt is retried, and a retry usually reattaches to the Sailbox already coming up rather than starting another. When the overall budget is exhausted the Sailbox may still be coming up server-side: find or terminate it by `name`. | - | - | | `visibility?` | `"org"` \| `"private"` | 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 a private Sailbox 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. SSH is enabled after create with [Sailbox.enableSsh](#enablessh-1). | - | - | | `volumes?` | `Readonly`\<`Record`\<`string`, `string` \| [`Volume`](#volume)>> | Shared volumes to mount, mapping an absolute guest path to a [Volume](#volume) or volume id. 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). | - | - | *** ### CreateSailboxRequest #### Extends * `Omit`\<`native.CreateSailboxRequest`, `"image"` | `"ingressPorts"` | `"size"` | `"volumeMounts"` | `"autoSleep"` | `"networkPolicy"` | `"visibility"` | `"networkAllowedHosts"`> #### Properties | Property | Type | Description | Inherited from | | ---------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | `appId` | `string` | Identifier of the owning app. | `Omit.appId` | | `autoSleep?` | [`AutoSleep`](#autosleep-4) | When Sail may sleep this Sailbox on its own. Omit for the default. | - | | `diskLimitGib?` | `number` | Disk limit in whole GiB within the size's range; the size's default when omitted. | `Omit.diskLimitGib` | | `image?` | [`ImageSpec`](#imagespec) | Image to boot; defaults to a plain Debian base when omitted. | - | | `imageBuildTimeoutSeconds?` | `number` | Budget in seconds for rebuilding the image if Sail needs to rebuild it before the Sailbox is created; the default build budget applies when omitted. | `Omit.imageBuildTimeoutSeconds` | | `ingressPorts?` | readonly [`IngressPortInput`](#ingressportinput)\[] | Guest ports to reserve for ingress. | - | | `memoryLimitGib?` | `number` | Memory limit in whole GiB within the size's range; the size's default when omitted. | `Omit.memoryLimitGib` | | `name` | `string` | The Sailbox name. | `Omit.name` | | `networkPolicy?` | [`NetworkPolicy`](#networkpolicy-4) | The Sailbox's network policy. Omit for `"public"`. | - | | `size?` | [`SailboxSize`](#sailboxsize) | Resource size; `"m"` when omitted. | - | | `visibility?` | `"org"` \| `"private"` | Who may operate the Sailbox: `"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 the creating user and requires a user-scoped API key. Fixed for the Sailbox's life. | - | | `volumeMounts?` | readonly [`VolumeMountInput`](#volumemountinput)\[] | NFS volumes to mount. | - | *** ### DeleteVolumeOptions Options for [Volume.delete](#delete-2). #### Properties | Property | Type | Description | | ---------------- | --------- | ----------------------------------------------------------- | | `allowMissing?` | `boolean` | Tolerate a volume that is already gone instead of throwing. | *** ### DirEntry One entry in a directory listing from `Sailbox.fs.ls`, with `type` narrowed to [DirEntryType](#direntrytype-1). #### Extends * `Omit`\<`native.DirEntry`, `"type"`> #### Properties | Property | Type | Description | Inherited from | | --------------- | --------------------------------- | --------------------------------------------------------------------------- | ------------------- | | `mode` | `number` | Unix permission bits, e.g. `0o644`. The file-type bits are not included. | `Omit.mode` | | `modifiedTime` | `number` | Last-modified time as a Unix timestamp in seconds (with a fractional part). | `Omit.modifiedTime` | | `name` | `string` | The entry's base name, with no directory prefix. | `Omit.name` | | `size` | `number` | Size in bytes as reported by the guest. | `Omit.size` | | `type` | [`DirEntryType`](#direntrytype-1) | - | - | *** ### DirEntryType > **DirEntryType** = `"file"` | `"directory"` | `"symlink"` | `"other"` The kind of a directory entry, reported for the entry itself: a symlink is `"symlink"` regardless of what it points at. *** ### DockerfileContextDir One directory of a Dockerfile build context. #### Properties | Property | Type | Description | | ---------------- | -------- | ----------------------------------------------------------------- | | `mode?` | `number` | Directory mode permission bits; omitted means the default (0755). | | `relativePath?` | `string` | Slash-separated path relative to the context root. | *** ### DockerfileContextSymlink One symbolic link of a Dockerfile build context. #### Properties | Property | Type | Description | | ---------------- | -------- | ------------------------------------------------------------------------------------- | | `relativePath?` | `string` | Slash-separated path relative to the context root. | | `target?` | `string` | Raw link target, exactly as the link stores it; resolved only inside the built image. | *** ### DockerfileFromResolution What one external image reference in a Dockerfile resolved to when an image was built. #### Properties | Property | Type | Description | | ------------- | -------- | ------------------------------------------------------------------- | | `digestRef?` | `string` | The digest-pinned form of the same reference the build used. | | `reference?` | `string` | The reference as the Dockerfile's `FROM` or `COPY --from` names it. | *** ### DockerfileImage Your own Dockerfile built into an image, with its resolved build context. A `RUN --mount` of type `cache`, `secret`, or `ssh`, a `bind` mount reading from another image, mount options that are not literal text, and `ONBUILD` instructions are rejected. #### Properties | Property | Type | Description | | ------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `buildArgs?` | `Record`\<`string`, `string`> | Values for the Dockerfile's `ARG` instructions, like `--build-arg`. 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. | | `contextDirs?` | [`DockerfileContextDir`](#dockerfilecontextdir)\[] | Every directory in the context with its mode, so `COPY` of an empty directory works and directory modes survive like they do in a docker build. | | `contextFiles?` | [`AddLocalDirFile`](#addlocaldirfile)\[] | Content manifest of the build context the Dockerfile's `COPY` and `ADD` instructions read from; empty builds without a context. | | `contextSymlinks?` | [`DockerfileContextSymlink`](#dockerfilecontextsymlink)\[] | Symbolic links in the context, carried as links the way a docker build context carries them. | | `dockerfile?` | `string` | Full Dockerfile text. | | `pinnedFrom?` | [`DockerfileFromResolution`](#dockerfilefromresolution)\[] | The version each external image reference resolved to when this spec was built, filled in on the spec a completed build returns. A spec carrying these keeps naming the image its build produced, even after a forced build moves what the references mean for your organization; a forced build looks every reference up again instead. | *** ### DockerfileSourceInput A Dockerfile to build into the image, with its local build context. A `RUN --mount` of type `cache`, `secret`, or `ssh`, a `bind` mount reading from another image, mount options that are not literal text, and `ONBUILD` instructions are rejected. #### Properties | Property | Type | Description | | -------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `buildArgs?` | `Record`\<`string`, `string`> | Values for the Dockerfile's `ARG` instructions, like `--build-arg`. 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. | | `contextDir?` | `string` | Local directory the Dockerfile's `COPY` and `ADD` instructions read from; if omitted, the build runs without a context. | | `dockerfile` | `string` | Path to a Dockerfile on this machine, or with `isContents` its full contents. | | `ignore?` | `string`\[] | `.dockerignore`-style patterns to skip in the context directory, applied after the `.dockerignore` rules in effect so they take precedence on conflict. | | `isContents?` | `boolean` | Read `dockerfile` as literal Dockerfile text instead of a path. | *** ### EnableSshOptions Options for enabling SSH on a Sailbox. #### Properties | Property | Type | Description | | ------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowlist?` | readonly `string`\[] | Source addresses or ranges allowed to reach 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. | | `timeoutSeconds?` | `number` | Give up waiting after this many seconds (default 60; `Infinity` waits indefinitely). | | `wait?` | `boolean` | Poll until the SSH route is ready (default true). | *** ### ExecOptions #### Extends * `Omit`\<`native.ExecStartOptions`, `"env"` | `"outputMode"` | `"pty"` | `"term"` | `"cols"` | `"rows"`> #### Properties | Property | Type | Description | Inherited from | | --------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | `background?` | `boolean` | Detach the command so it keeps running and the call returns immediately; output is discarded (shell commands only, incompatible with `openStdin`/`pty`). | `Omit.background` | | `cwd?` | `string` | Working directory to run the command in (shell commands only). Unset starts the command in the image's working directory, or `/` when the image does not set one. | `Omit.cwd` | | `env?` | `Readonly`\<`Record`\<`string`, `string`>> | Extra environment for the command. Entries override the guest's defaults (including `LANG` and the `IS_SANDBOX=1` sandbox marker) and the image env, but a few reserved variables that identify the Sailbox (such as `SAILBOX_ID`) cannot be overridden. | - | | `idempotencyKey?` | `string` | Stable key so a reconnect reattaches to the same command. An exec has one live handle at a time: a second handle started with the same key takes over the stream, and the first stops receiving live output and resolves from a bounded recorded result. A first handle reconnecting after a dropped connection can race a handle that attached meanwhile, and either handle's result may come back incomplete; avoid overlapping same-key handles. The UTF-8 value can be up to 256 KiB. | `Omit.idempotencyKey` | | `openStdin?` | `boolean` | Leave stdin open for `writeStdin`. | `Omit.openStdin` | | `outputBufferBytes?` | `number` | Size of each stream's output buffer in bytes, 1 MiB by default. Must be between 64 KiB and 64 MiB. | `Omit.outputBufferBytes` | | `outputMode?` | [`OutputMode`](#outputmode-1) | What happens when a stream's output buffer fills; see [OutputMode](#outputmode-1). `"auto"` by default. | - | | `pty?` | `boolean` \| [`PtyConfig`](#ptyconfig) | Run the command under a pseudo-terminal: `true` for the default terminal, or a [PtyConfig](#ptyconfig) to set `term` and the initial window. `isatty()` is true, control bytes on stdin become signals, stdout and stderr merge onto one stream, and `resize` adjusts the window. Implies `openStdin`. | - | | `timeoutSeconds?` | `number` | Wall-clock limit in seconds before the server kills the command. | `Omit.timeoutSeconds` | | `user?` | `string` | Run the command as this guest user: a user name or numeric uid, optionally with a group appended after a colon (`"alice"`, `"1000"`, `"alice:staff"`, 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 unset, commands run as the image's `USER` if the image sets one, root otherwise; pass `"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 `"0:0"` needs no upgrade. | `Omit.user` | *** ### ExecResult The result of a finished exec. `stdout` and `stderr` hold only each stream's buffer, its most recent output (`outputBufferBytes`, 1 MiB by default). To get every byte, consume the live stream right after `exec()` returns; see `ExecProcess` for how consuming a stream affects the command. #### Properties | Property | Type | Description | | ------------------ | --------- | -------------------------------------------------------------------------------------------------------- | | `exitCode` | `number` | The command's exit code. | | `stderr` | `string` | The most recent standard error, up to the exec's buffer size (1 MiB by default; see `stderrTruncated`). | | `stderrTruncated` | `boolean` | The command wrote more stderr than `stderr` holds, so older bytes are missing. | | `stdout` | `string` | The most recent standard output, up to the exec's buffer size (1 MiB by default; see `stdoutTruncated`). | | `stdoutTruncated` | `boolean` | The command wrote more stdout than `stdout` holds, so older bytes are missing. | | `timedOut` | `boolean` | Whether the command was killed for exceeding its timeout. | *** ### ExposeOptions Options for [Sailbox.expose](#expose). #### Properties | Property | Type | Description | | ------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowlist?` | readonly `string`\[] | Sources allowed to reach the port: an address or a range, or a Sail app name on an `http` listener. 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. Empty allows all. | | `protocol?` | [`IngressProtocol`](#ingressprotocol) | Wire protocol to expose (default `"http"`). | *** ### FindAppOptions Options for [App.find](#find). #### Extends * [`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Inherited from | | ----------------- | ------------------- | ------------------------------------------------------- | ------------------------------------------------------- | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `mintIfMissing?` | `boolean` | Create the app when it does not exist yet. | - | *** ### FindVolumeOptions Options for [Volume.find](#find-1). #### Extends * [`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Inherited from | | ----------------- | ------------------- | ------------------------------------------------------- | ------------------------------------------------------- | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `mintIfMissing?` | `boolean` | Create the volume when it does not exist yet. | - | *** ### FromCheckpointOptions Options for [Sailbox.fromCheckpoint](#fromcheckpoint). #### Extends * [`FromCheckpointRequest`](#fromcheckpointrequest).[`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Inherited from | | ------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `checkpointId` | `string` | The checkpoint to restore from. | [`FromCheckpointRequest`](#fromcheckpointrequest).[`checkpointId`](#checkpointid-1) | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `name` | `string` | Name for the new Sailbox. | [`FromCheckpointRequest`](#fromcheckpointrequest).[`name`](#name-11) | | `timeoutSeconds?` | `number` | Whole seconds, positive when given. Bounds the call, since restoring a checkpoint can block for many minutes while the new Sailbox queues for capacity. A call that times out fails, and the restore may still finish in the background; the new Sailbox then shows up when you list Sailboxes. Unset waits without a bound. | [`FromCheckpointRequest`](#fromcheckpointrequest).[`timeoutSeconds`](#timeoutseconds-4) | *** ### FromCheckpointRequest The create-from-checkpoint request. #### Extended by * [`FromCheckpointOptions`](#fromcheckpointoptions) #### Properties | Property | Type | Description | | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `checkpointId` | `string` | The checkpoint to restore from. | | `name` | `string` | Name for the new Sailbox. | | `timeoutSeconds?` | `number` | Whole seconds, positive when given. Bounds the call, since restoring a checkpoint can block for many minutes while the new Sailbox queues for capacity. A call that times out fails, and the restore may still finish in the background; the new Sailbox then shows up when you list Sailboxes. Unset waits without a bound. | *** ### FromDockerfileOptions Options for [Image.fromDockerfile](#fromdockerfile). #### Properties | Property | Type | Description | | ---------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `architecture?` | [`ImageArchitecture`](#imagearchitecture) | Target CPU architecture the image is built for (default `amd64`). | | `buildArgs?` | `Readonly`\<`Record`\<`string`, `string`>> | Values for the Dockerfile's `ARG` instructions, like `--build-arg`. 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. | | `contextDir?` | `string` | Local directory the Dockerfile's `COPY` and `ADD` instructions read from; if omitted, the build runs without a context. | | `ignore?` | readonly `string`\[] | `.dockerignore`-style patterns to skip in the context directory, applied after the `.dockerignore` rules in effect so they take precedence on conflict. | *** ### FromRegistryOptions Options for [Image.fromRegistry](#fromregistry). #### Properties | Property | Type | Description | | ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `architecture?` | [`ImageArchitecture`](#imagearchitecture) | Require the image to have been built for this CPU architecture. Leave it unset to use the architecture the image was built for. | *** ### FsOptions Options for the directory helpers ([SailboxFs.mkdir](#mkdir), [SailboxFs.remove](#remove), [SailboxFs.exists](#exists), [SailboxFs.ls](#ls)). #### Properties | Property | Type | Description | | | | -------- | -------- | -------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `user?` | `string` | Run the operation as this user (Docker's `USER` syntax: \`name | uid\[:group | gid]`), with its permissions enforced by the guest kernel. Unset runs as root, so the helpers work on any path; `"0:0"`is always root. A`user`other than`"0:0"\` requires a Sailbox whose guest honors requested users; on older Sailboxes the operation fails until [Sailbox.upgrade](#upgrade) is called. | *** ### HttpEndpoint The routable HTTPS address of an `http` listener. #### Properties | Property | Type | Description | | -------- | -------- | ----------------------------------------- | | `kind` | `"http"` | - | | `url` | `string` | The HTTPS URL to reach the guest service. | *** ### HttpPolicyAddChange > **HttpPolicyAddChange** = \{ `headers`: [`HttpPolicyValueMap`](#httppolicyvaluemap); `query?`: [`HttpPolicyValueMap`](#httppolicyvaluemap); } | \{ `headers?`: [`HttpPolicyValueMap`](#httppolicyvaluemap); `query`: [`HttpPolicyValueMap`](#httppolicyvaluemap); } Headers or query parameters to append to a request. At least one of `headers` or `query` is required, and the union encodes that, so an empty `add` is a compile error. *** ### HttpPolicyDocument > **HttpPolicyDocument** = `object` A policy document: host patterns mapped to their rules. A key is an exact hostname (`api.example.com`), a single-label wildcard (`*.example.com`), or the catch-all `*`. Sail picks one host, most specific first, and host entries never combine. ```ts theme={null} const document: HttpPolicyDocument = { "api.example.com": { rules: [ { match: { path: { prefix: "/v1/" } }, request: { set: { headers: { authorization: "Bearer ${secrets.API_KEY}" } }, }, }, ], }, }; ``` Sail validates the document when you create the policy and identifies any field that needs to be fixed. See the [HTTP policy guide](/sailboxes-http-policies) for the accepted fields and examples. #### Index Signature \[`host`: `string`]: [`HttpPolicyHost`](#httppolicyhost) *** ### HttpPolicyForward Send the request to a different HTTPS host. `host` is an exact hostname; `port` defaults to 443. #### Properties | Property | Type | Description | | -------- | -------- | ---------------------------------------------------- | | `host` | `string` | Exact hostname, without a scheme, port, or wildcard. | | `port?` | `number` | HTTPS port. Defaults to 443. | *** ### HttpPolicyHost The rules for one host, in order. #### Properties | Property | Type | Description | | ---------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `missing_alpn?` | `"http/1.1"` | How to treat an HTTPS connection that does not announce an HTTP version when it connects. By default such a connection passes through with the policy not applied. Set `"http/1.1"` to treat it as HTTP/1.1 so the rules apply. Allowed only on an exact host. Almost every HTTP client announces a version, so most policies do not need this. | | `rules` | readonly [`HttpPolicyRule`](#httppolicyrule)\[] | Rules for this host, in the order Sail tests them. | *** ### HttpPolicyInfo A policy's id, name, and document. Timestamps are RFC 3339 strings. #### Properties | Property | Type | Description | | ------------ | -------- | ------------------------------------------------------------------------------------------------- | | `createdAt` | `string` | When the policy was created (RFC 3339). | | `document` | `string` | The saved policy document as JSON text: Sail's normalized form of the document given at creation. | | `id` | `string` | The policy's stable identifier. | | `name` | `string` | The policy's name (the only mutable field). | | `updatedAt` | `string` | When the policy's name last changed (RFC 3339). | *** ### HttpPolicyLike > **HttpPolicyLike** = [`HttpPolicy`](#httppolicy) | [`HttpPolicySummary`](#httppolicysummary) | `string` A policy object, listing summary, or policy id accepted by [Sailbox.setHttpPolicy](#sethttppolicy). *** ### HttpPolicyListItem One HTTP policy returned by [Client.listHttpPolicies](#listhttppolicies). #### Properties | Property | Type | Description | | ------------------------ | ----------- | ----------------------------------------------------------- | | `attachmentCount` | `number` | How many Sailboxes the policy is currently attached to. | | `createdAt` | `string` | When the policy was created, as an RFC 3339 string. | | `hostCount` | `number` | How many hosts the document covers. | | `id` | `string` | The policy's stable identifier. | | `name` | `string` | The policy's name. | | `referencedSecretNames` | `string`\[] | The secret names the document refers to. | | `ruleCount` | `number` | How many rules the document carries across every host. | | `updatedAt` | `string` | When the policy's name last changed, as an RFC 3339 string. | *** ### HttpPolicyMatcher > **HttpPolicyMatcher** = `string` | \{ `equals`: `string`; } | \{ `prefix`: `string`; } | \{ `one_of`: readonly `string`\[]; } Match one exact string, a prefix, or one string from a list. A plain string is an exact match. *** ### HttpPolicyNameValueMatcher > **HttpPolicyNameValueMatcher** = \{ `name`: [`HttpPolicyMatcher`](#httppolicymatcher); `present?`: `true`; `value?`: [`HttpPolicyMatcher`](#httppolicymatcher); } | \{ `name?`: [`HttpPolicyMatcher`](#httppolicymatcher); `present?`: `true`; `value`: [`HttpPolicyMatcher`](#httppolicymatcher); } | \{ `name`: [`HttpPolicyMatcher`](#httppolicymatcher); `present`: `false`; `value?`: `never`; } Match a header or query parameter by name, value, or both. Every condition needs a `name` or a `value`; `present` defaults to `true`, meaning a header or parameter matching the condition must be present. Set `present` to `false` with `name` alone to require that name to be absent. The union encodes those rules, so a condition with neither field, or a `value` combined with `present: false`, is a compile error. *** ### HttpPolicyPage One page returned by [Client.listHttpPolicies](#listhttppolicies). #### Properties | Property | Type | Description | | ---------- | ---------------------------------------------- | ------------------------------------------------- | | `hasMore` | `boolean` | Whether another page is available. | | `items` | [`HttpPolicyListItem`](#httppolicylistitem)\[] | The policies on this page. | | `limit` | `number` | The requested page size. | | `offset` | `number` | The requested zero-based offset. | | `total` | `number` | The number of matching policies across all pages. | *** ### HttpPolicyRemoveChange > **HttpPolicyRemoveChange** = \{ `headers`: readonly `string`\[]; `query?`: readonly `string`\[]; } | \{ `headers?`: readonly `string`\[]; `query`: readonly `string`\[]; } Header and query parameter names to delete from a request. At least one of `headers` or `query` is required, and the union encodes that, so an empty `remove` is a compile error. *** ### HttpPolicyRequestChange > **HttpPolicyRequestChange** = \{ `add?`: [`HttpPolicyAddChange`](#httppolicyaddchange); `remove?`: [`HttpPolicyRemoveChange`](#httppolicyremovechange); `set`: [`HttpPolicySetChange`](#httppolicysetchange); } | \{ `add`: [`HttpPolicyAddChange`](#httppolicyaddchange); `remove?`: [`HttpPolicyRemoveChange`](#httppolicyremovechange); `set?`: [`HttpPolicySetChange`](#httppolicysetchange); } | \{ `add?`: [`HttpPolicyAddChange`](#httppolicyaddchange); `remove`: [`HttpPolicyRemoveChange`](#httppolicyremovechange); `set?`: [`HttpPolicySetChange`](#httppolicysetchange); } Change the outbound request before it is sent. `set` replaces or creates values, `add` appends, `remove` deletes. At least one operation is required, and the union encodes that, so an empty change is a compile error. Only `set.headers` and `set.query` may contain a `${secrets.NAME}` reference. *** ### HttpPolicyRequestMatch > **HttpPolicyRequestMatch** = \{ `headers?`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; `method`: [`HttpPolicyStringList`](#httppolicystringlist); `path?`: [`HttpPolicyMatcher`](#httppolicymatcher); `query?`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; } | \{ `headers?`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; `method?`: [`HttpPolicyStringList`](#httppolicystringlist); `path`: [`HttpPolicyMatcher`](#httppolicymatcher); `query?`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; } | \{ `headers`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; `method?`: [`HttpPolicyStringList`](#httppolicystringlist); `path?`: [`HttpPolicyMatcher`](#httppolicymatcher); `query?`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; } | \{ `headers?`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; `method?`: [`HttpPolicyStringList`](#httppolicystringlist); `path?`: [`HttpPolicyMatcher`](#httppolicymatcher); `query`: readonly [`HttpPolicyNameValueMatcher`](#httppolicynamevaluematcher)\[]; } The conditions a request must meet for a rule to apply. At least one condition is required, and the union encodes that, so an empty `match` is a compile error. `method` is one or more case-sensitive HTTP methods, such as `GET`; `path` matches the absolute request path; `headers` and `query` are condition lists in which every entry must match. *** ### HttpPolicyResponse Return a response without sending an HTTP request to the destination. #### Properties | Property | Type | Description | | ----------- | ------------------------------------------- | ----------------------------------------------------------------------------------------- | | `body?` | `string` | Response body. Set a content-type header when needed. Not allowed with status 204 or 304. | | `headers?` | [`HttpPolicyValueMap`](#httppolicyvaluemap) | Headers to return. | | `status` | `number` | HTTP status code to return. | *** ### HttpPolicyRule > **HttpPolicyRule** = \{ `forward?`: `never`; `match?`: [`HttpPolicyRequestMatch`](#httppolicyrequestmatch); `request?`: `never`; `respond`: [`HttpPolicyResponse`](#httppolicyresponse); } | \{ `forward?`: [`HttpPolicyForward`](#httppolicyforward); `match?`: [`HttpPolicyRequestMatch`](#httppolicyrequestmatch); `request?`: [`HttpPolicyRequestChange`](#httppolicyrequestchange); `respond?`: `never`; } One rule. `match` narrows which requests it covers; omitting it matches every request, which is only allowed on the last rule. The first matching rule decides the outcome, so order matters. A rule can return a response without sending the request (`respond`), forward the request to another HTTPS host (`forward`), change the request before it is sent (`request`), or send it unchanged. `forward` and `request` combine; `respond` cannot be combined with either, and the union encodes that, so a rule that mixes them is a compile error. *** ### HttpPolicySetChange > **HttpPolicySetChange** = \{ `headers?`: [`HttpPolicyValueMap`](#httppolicyvaluemap); `path`: `string`; `query?`: [`HttpPolicyValueMap`](#httppolicyvaluemap); } | \{ `headers`: [`HttpPolicyValueMap`](#httppolicyvaluemap); `path?`: `string`; `query?`: [`HttpPolicyValueMap`](#httppolicyvaluemap); } | \{ `headers?`: [`HttpPolicyValueMap`](#httppolicyvaluemap); `path?`: `string`; `query`: [`HttpPolicyValueMap`](#httppolicyvaluemap); } Values to replace or create on a request. At least one of `path`, `headers`, or `query` is required, and the union encodes that, so an empty `set` is a compile error. `path` is a replacement absolute path. Values under `headers` and `query` are templates: `${secrets.NAME}` inserts a secret, and a literal dollar sign must be written `$$`. Stored secret references are allowed only under `headers` and `query`, never in `path`. *** ### HttpPolicyStringList > **HttpPolicyStringList** = `string` | readonly `string`\[] One string or a nonempty list of strings. *** ### HttpPolicySummary A policy as returned by [HttpPolicy.list](#list-1), with usage counts but without the document. Fetch the full policy with [HttpPolicy.get](#get). #### Properties | Property | Type | Description | | ------------------------ | ----------- | ------------------------------------------------------- | | `attachmentCount` | `number` | How many Sailboxes the policy is currently attached to. | | `createdAt` | `Date` | When the policy was created. | | `hostCount` | `number` | How many hosts the document covers. | | `id` | `string` | The policy's stable identifier. | | `name` | `string` | The policy's name. | | `referencedSecretNames` | `string`\[] | The secret names the document refers to. | | `ruleCount` | `number` | How many rules the document carries across every host. | | `updatedAt` | `Date` | When the policy's name last changed. | *** ### HttpPolicyValueMap > **HttpPolicyValueMap** = `object` Header or query parameter names mapped to one or more values. #### Index Signature \[`name`: `string`]: [`HttpPolicyStringList`](#httppolicystringlist) *** ### ImageArchitecture > **ImageArchitecture** = `"amd64"` | `"arm64"` *** ### ImageBuild The state of a custom image build. #### Extends * `Omit`\<`native.ImageBuild`, `"status"`> #### Properties | Property | Type | Description | Inherited from | | ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | `dockerfilePins?` | [`DockerfileFromResolution`](#dockerfilefromresolution)\[] | What each external image reference resolved to when the spec's source is a Dockerfile; absent otherwise. Carrying these in the spec's `pinnedFrom` keeps creating from the image this build produced, even after a forced build moves what the references mean for your organization. | `Omit.dockerfilePins` | | `errorMessage?` | `string` | Human-readable failure detail; present when `status` is `failed`. | `Omit.errorMessage` | | `imageId` | `string` | The content-addressed image id. | `Omit.imageId` | | `resolvedOciRef?` | `string` | The digest-pinned form of the spec's registry reference when the spec's source is an OCI image; absent otherwise. Creating from this reference instead of the submitted tag keeps naming the same registry content even if the tag has moved since. | `Omit.resolvedOciRef` | | `status` | [`ImageBuildStatus`](#imagebuildstatus-1) | - | - | *** ### ImageBuildOptions Options for [Image.build](#build). #### Properties | Property | Type | Description | | ------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | | `forceBuild?` | `boolean` | Build the image again even if a build already exists, and wait for the fresh build to become ready. 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 with [Image.fromRegistry](#fromregistry) through a 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 specs built earlier keep their pinned version. A forced build of an image built with [Image.fromDockerfile](#fromdockerfile) looks up the tags its `FROM` and `COPY --from` instructions name and moves those pins for your whole organization, while specs 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. | | `timeoutSeconds?` | `number` | Timeout in seconds bounding the whole pipeline, including hashing, uploads, the build, and any automatic retries (default 1800). | *** ### ImageBuildStatus > **ImageBuildStatus** = `"unknown"` | `"queued"` | `"building"` | `"ready"` | `"failed"` The status of a custom image build. *** ### ImageBuildStep > **ImageBuildStep** = \{ `addLocalDir?`: `never`; `addLocalFile?`: `never`; `aptInstall`: [`PackageInstall`](#packageinstall); `pipInstall?`: `never`; `runCommand?`: `never`; } | \{ `addLocalDir?`: `never`; `addLocalFile?`: `never`; `aptInstall?`: `never`; `pipInstall`: [`PackageInstall`](#packageinstall); `runCommand?`: `never`; } | \{ `addLocalDir?`: `never`; `addLocalFile?`: `never`; `aptInstall?`: `never`; `pipInstall?`: `never`; `runCommand`: [`RunCommand`](#runcommand-2); } | \{ `addLocalDir?`: `never`; `addLocalFile`: [`AddLocalFile`](#addlocalfile-1); `aptInstall?`: `never`; `pipInstall?`: `never`; `runCommand?`: `never`; } | \{ `addLocalDir`: [`AddLocalDir`](#addlocaldir-1); `addLocalFile?`: `never`; `aptInstall?`: `never`; `pipInstall?`: `never`; `runCommand?`: `never`; } One build step: exactly one operation. Each union member `never`-types the other operations, so a step that sets two of them is a compile error (a bare union of the operations would accept it). *** ### ImageDefinition A custom image definition: a base, registry, or Dockerfile image plus ordered build steps, where local-file steps still reference paths on this machine. #### Properties | Property | Type | Description | | ---------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `architecture?` | `string` | Target CPU architecture: `amd64` or `arm64`. Unset means `amd64` with `base` and `dockerfile`, and with `ociRef` means whichever architecture the registry image was built for (`amd64` when it was built for both). Setting it with `ociRef` requires the image to provide that architecture. | | `base?` | `string` | Base image to build on: `debian` or `devbox`. The devbox base is a prebuilt development environment that includes Docker, with the daemon started automatically when the Sailbox boots and kept running across sleeps. The daemon can take a few seconds to accept commands right after boot. If it stops, it is not restarted automatically. Mutually exclusive with `ociRef` and `dockerfile`. | | `dockerfile?` | [`DockerfileSourceInput`](#dockerfilesourceinput) | Your own Dockerfile built into the image; mutually exclusive with `base` and `ociRef`. Every image its `FROM` (and `COPY --from`) instructions name 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`). Each named image is pinned to the version its tag pointed at the first time your organization used it, and those pinned versions become part of the built image's identity, so rebuilding the same spec reuses the same image even after a tag moves. A forced build looks the tags up again and builds what they point at now. The built image's `ENV`, `WORKDIR`, and `USER` become the Sailbox defaults for commands you run; its `ENTRYPOINT` and `CMD` are not run, because a Sailbox manages its own processes. | | `env?` | `Record`\<`string`, `string`> | Environment variables baked into the image. | | `ociRef?` | `string` | Your own image as the root filesystem: a reference to a Debian- or Ubuntu-based image whose first segment names a supported public registry (`docker.io`, `ghcr.io`, `public.ecr.aws`, or `quay.io`), with an optional `:tag` or `@sha256:<64 hex>` pin (no tag means the `latest` tag). A tag is pinned for your organization once an image has been built from it: later builds keep getting that version, even if the tag moves upstream. A forced build looks the tag up again and moves the pin for your whole organization. If forced builds of the same tag overlap, the last-requested one that succeeds decides what the tag means, no matter which build finishes first. A digest names exactly one image, so it never moves. The image's `ENV`, `WORKDIR`, and `USER` become the Sailbox defaults for commands you run; its `ENTRYPOINT` and `CMD` are not run, because a Sailbox manages its own processes. Mutually exclusive with `base` and `dockerfile`. | | `steps?` | [`ImageDefinitionStep`](#imagedefinitionstep)\[] | Ordered build steps. | *** ### ImageDefinitionStep One image-definition step. Exactly one of the fields must be set. #### Properties | Property | Type | Description | | ---------------- | ----------------------------------- | ------------------------------------------- | | `addLocalDir?` | [`LocalDirInput`](#localdirinput) | Bake a local directory tree into the image. | | `addLocalFile?` | [`LocalFileInput`](#localfileinput) | Bake one local file into the image. | | `aptInstall?` | `string`\[] | Install system packages with apt. | | `pipInstall?` | `string`\[] | Install Python packages with pip. | | `runCommand?` | `string` | Run a shell command during the build. | *** ### ImageSpec A Sailbox image: a base or registry image plus ordered build steps. #### Extends * `Omit`\<`native.ImageSpec`, `"base"` | `"buildSteps"` | `"architecture"` | `"filesystem"`> #### Properties | Property | Type | Description | Inherited from | | ---------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- | | `architecture?` | [`ImageArchitecture`](#imagearchitecture) | Target CPU architecture. Unset means `amd64` with `base` and `dockerfile`, and with `ociRef` means whichever architecture the registry image was built for (`amd64` when it was built for both). Setting it with `ociRef` requires the image to provide that architecture. | - | | `base?` | [`BaseImage`](#baseimage) | Base image to build on. The `devbox` base is a prebuilt development environment that includes Docker, with the daemon started automatically when the Sailbox boots and kept running across sleeps. The daemon can take a few seconds to accept commands right after boot. If it stops, it is not restarted automatically. Mutually exclusive with `ociRef` and `dockerfile`. | - | | `buildSteps?` | [`ImageBuildStep`](#imagebuildstep)\[] | Ordered build steps applied on top of the image source. | - | | `dockerfile?` | [`DockerfileImage`](#dockerfileimage) | Your own Dockerfile built into the image; mutually exclusive with `base` and `ociRef`. Every image its `FROM` (and `COPY --from`) instructions name 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`). Each named image is pinned to the version its tag pointed at the first time your organization used it, and those pinned versions become part of the built image's identity, so rebuilding the same spec reuses the same image even after a tag moves. A forced build looks the tags up again and builds what they point at now. The built image's `ENV`, `WORKDIR`, and `USER` become the Sailbox defaults for commands you run; its `ENTRYPOINT` and `CMD` are not run, because a Sailbox manages its own processes. | `Omit.dockerfile` | | `env?` | `Record`\<`string`, `string`> | Environment variables baked into the image. | `Omit.env` | | `ociRef?` | `string` | Your own image as the root filesystem: a reference to a Debian- or Ubuntu-based image whose first segment names a supported public registry (`docker.io`, `ghcr.io`, `public.ecr.aws`, or `quay.io`), with an optional `:tag` or `@sha256:<64 hex>` pin (no tag means the `latest` tag). A tag is pinned for your organization once an image has been built from it: later builds keep getting that version, even if the tag moves upstream. A forced build looks the tag up again and moves the pin for your whole organization. If forced builds of the same tag overlap, the last-requested one that succeeds decides what the tag means, no matter which build finishes first. A digest names exactly one image, so it never moves. The image's `ENV`, `WORKDIR`, and `USER` become the Sailbox defaults for commands you run; its `ENTRYPOINT` and `CMD` are not run, because a Sailbox manages its own processes. Mutually exclusive with `base` and `dockerfile`. | `Omit.ociRef` | *** ### IngressPortInput A guest port to reserve for ingress at create time. #### Extends * `Omit`\<`native.IngressPortInput`, `"protocol"` | `"allowlist"`> #### Properties | Property | Type | Description | Inherited from | | ------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | | `allowlist?` | readonly `string`\[] | Sources allowed to reach the port: an address or a range, or a Sail app name on an `http` listener. 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. Empty allows all. | - | | `guestPort` | `number` | The in-guest port to expose (1-65535). | `Omit.guestPort` | | `protocol` | [`IngressProtocol`](#ingressprotocol) | `http` or `tcp`. | - | *** ### IngressProtocol > **IngressProtocol** = `"tcp"` | `"http"` The protocol you request when exposing a port. *** ### IngressScheme > **IngressScheme** = `"path"` | `"subdomain"` How a listener's URL is addressed under `ingressBase`. *** ### ListHttpPoliciesOptions Options for [HttpPolicy.list](#list-1). #### Extends * [`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Inherited from | | ---------- | ------------------- | ------------------------------------------------------- | ------------------------------------------------------- | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `limit?` | `number` | Cap the total number of policies returned. | - | | `search?` | `string` | Filter by id or name, case-insensitively. | - | *** ### ListSailboxesOptions Options for [Sailbox.list](#list-2): the server-side filters, a total-cap `limit`, and an optional `client`. #### Extends * `Omit`\<[`ListSailboxesQuery`](#listsailboxesquery), `"limit"` | `"offset"`>.[`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Inherited from | | ---------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | `appId?` | `string` | Filter to one app by its id. | `Omit.appId` | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `limit?` | `number` | Cap on the total number of Sailboxes returned, bounding the fetch for large orgs; omit to fetch every match. | - | | `order?` | [`SailboxListOrder`](#sailboxlistorder) | Result ordering; `"newest_active"` (most recently active first) when omitted; `"newest_created"` lists the newest-created first. | [`ListSailboxesPageOptions`](#listsailboxespageoptions).[`order`](#order-1) | | `search?` | `string` | Substring filter on the Sailbox name. | `Omit.search` | | `status?` | [`SailboxStatusFilter`](#sailboxstatusfilter) | Filter by lifecycle status. | [`ListSailboxesPageOptions`](#listsailboxespageoptions).[`status`](#status-9) | *** ### ListSailboxesPageOptions Options for [Sailbox.listPage](#listpage): the same filters as [ListSailboxesOptions](#listsailboxesoptions), plus `limit`/`offset` page selection and an optional `client`. #### Extends * [`ListSailboxesQuery`](#listsailboxesquery).[`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Inherited from | | ---------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `appId?` | `string` | Filter to one app by its id. | [`ListSailboxesQuery`](#listsailboxesquery).[`appId`](#appid-5) | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `limit?` | `number` | Page size. | [`ListSailboxesQuery`](#listsailboxesquery).[`limit`](#limit-4) | | `offset?` | `number` | Page offset. | [`ListSailboxesQuery`](#listsailboxesquery).[`offset`](#offset-2) | | `order?` | [`SailboxListOrder`](#sailboxlistorder) | Result ordering; `"newest_active"` (most recently active first) when omitted; `"newest_created"` lists the newest-created first. | [`ListSailboxesQuery`](#listsailboxesquery).[`order`](#order-2) | | `search?` | `string` | Substring filter on the Sailbox name. | [`ListSailboxesQuery`](#listsailboxesquery).[`search`](#search-3) | | `status?` | [`SailboxStatusFilter`](#sailboxstatusfilter) | Filter by lifecycle status. | [`ListSailboxesQuery`](#listsailboxesquery).[`status`](#status-10) | *** ### ListSailboxesQuery Filters for listing Sailboxes. #### Extends * `Omit`\<`native.ListSailboxesQuery`, `"status"` | `"order"`> #### Extended by * [`ListSailboxesPageOptions`](#listsailboxespageoptions) #### Properties | Property | Type | Description | Inherited from | | ---------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------- | | `appId?` | `string` | Filter to one app by its id. | `Omit.appId` | | `limit?` | `number` | Page size. | `Omit.limit` | | `offset?` | `number` | Page offset. | `Omit.offset` | | `order?` | [`SailboxListOrder`](#sailboxlistorder) | Result ordering; `"newest_active"` (most recently active first) when omitted; `"newest_created"` lists the newest-created first. | - | | `search?` | `string` | Substring filter on the Sailbox name. | `Omit.search` | | `status?` | [`SailboxStatusFilter`](#sailboxstatusfilter) | Filter by lifecycle status. | - | *** ### ListVolumesOptions Options for [Volume.list](#list-4). #### Extends * [`ClientOptions`](#clientoptions) #### Properties | Property | Type | Description | Inherited from | | -------------- | ------------------- | ------------------------------------------------------- | ------------------------------------------------------- | | `client?` | [`Client`](#client) | Use a specific client instead of the default (env) one. | [`ClientOptions`](#clientoptions).[`client`](#client-2) | | `maxObjects?` | `number` | Maximum number of volumes to return. | - | *** ### Listener An exposed guest port and how to reach it. #### Properties | Property | Type | Description | | -------------- | ----------------------------------------------- | ------------------------------------------------------------------ | | `endpoint?` | [`ListenerEndpoint`](#listenerendpoint-1) | How to reach the port; `undefined` until the listener is routable. | | `guestPort` | `number` | The in-guest port traffic is forwarded to. | | `protocol` | [`Protocol`](#protocol-3) | Wire protocol exposed. | | `routeStatus` | [`ListenerRouteStatus`](#listenerroutestatus-1) | Status of the listener's ingress route. | *** ### ListenerEndpoint > **ListenerEndpoint** = [`HttpEndpoint`](#httpendpoint) | [`TcpEndpoint`](#tcpendpoint) How to reach an exposed listener; discriminate on `kind`. *** ### ListenerRouteStatus > **ListenerRouteStatus** = `"unknown"` | `"pending"` | `"active"` | `"restoring"` | `"unavailable"` | `string` & `object` Status of a listener's ingress route (open: tolerates unknown values). *** ### LocalDirInput A local directory tree to bake into the image (walked, hashed, and uploaded at resolve; symlinks skipped, file modes preserved). #### Properties | Property | Type | Description | | -------------- | ----------- | ------------------------------------------------------------------ | | `ignore?` | `string`\[] | Gitignore-style patterns to skip. | | `ignoreFile?` | `string` | A gitignore-style file whose patterns to skip (e.g. `.gitignore`). | | `localPath` | `string` | Path on this machine. | | `remotePath` | `string` | Absolute POSIX path of the directory root inside the image. | *** ### LocalFileInput One local file to bake into the image (hashed and uploaded at resolve). #### Properties | Property | Type | Description | | ------------- | -------- | --------------------------------------------------------------------------------- | | `localPath` | `string` | Path on this machine. | | `mode?` | `number` | Permission bits (low 9); omitted uses the builder default (0644). | | `remotePath` | `string` | Absolute POSIX path inside the image; a trailing `/` appends the source basename. | *** ### 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 `ingressPorts` and SSH still work. The [network policy guide](https://docs.sailresearch.com/sailboxes-network-policy) has the entry rules and what each entry allows. #### Properties | Property | Modifier | Type | Description | | --------------- | ---------- | -------------------- | ----------------------------------------------------- | | `allowedHosts` | `readonly` | readonly `string`\[] | The destinations the Sailbox may reach; at least one. | | `mode` | `readonly` | `"allowlist"` | Always `"allowlist"`. | *** ### NetworkPolicy > **NetworkPolicy** = `"public"` | `"no_network"` | [`NetworkAllowlist`](#networkallowlist) How a Sailbox may reach the network, chosen at creation and fixed for its life. `"public"` leaves network access open (the default); `"no_network"` cuts the Sailbox off from other hosts and the internet, so 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. A [NetworkAllowlist](#networkallowlist) restricts outbound access to a list of destinations instead. *** ### NetworkPolicyInfo A Sailbox's network policy as reported by [Sailbox.get](#get-1)/[Sailbox.list](#list-2). `mode` is the raw wire mode (`"no_network"` or `"allowlist"`); it is not narrowed to the [NetworkPolicy](#networkpolicy-4) create union, so a mode a newer backend adds is reported faithfully. `allowedHosts` carries the destinations in allowlist mode. Absent on a snapshot means public. The fields are `readonly`: the object a Sailbox returns is frozen, so this type matches the runtime and a caller cannot rewrite an audited policy. #### Properties | Property | Modifier | Type | Description | | --------------- | ---------- | -------------------- | ---------------------------------------------------------------- | | `allowedHosts` | `readonly` | readonly `string`\[] | Allowlist destinations when the mode uses them; empty otherwise. | | `mode` | `readonly` | `string` | The policy mode, for example `"no_network"`. | *** ### NeverSleep Stop Sail sleeping a Sailbox on its own. `minSecondsBeforeSleep` belongs to [AutomaticSleep](#automaticsleep), so it cannot be combined with this. #### Properties | Property | Modifier | Type | Description | | ------------------------- | ---------- | ----------- | ------------------------------------------- | | `automatic` | `readonly` | `false` | Stop Sail sleeping this Sailbox on its own. | | `minSecondsBeforeSleep?` | `readonly` | `undefined` | - | *** ### OutputMode > **OutputMode** = `"auto"` | `"pipe"` | `"tail"` What happens when a stream's output buffer fills. Each stream has its own buffer, 1 MiB by default (`outputBufferBytes` in [ExecOptions](#execoptions)). Sending `cancel()`, and the exec timeout, end every pause: from then on each stream keeps only its most recent bytes, so a consumer 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 `idempotencyKey`, 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. * `"auto"`, the default: a stream you are consuming pauses the command when its buffer fills and resumes as you read, like a pipe; a stream you are not consuming never pauses the command and keeps only its most recent bytes. * `"pipe"`: both streams pause the command when their buffer fills, until you consume them, so nothing is lost while you are late to start. Consume both streams, or the command stays paused on the one you ignore. Once a stream is released, it goes back to keeping only its most recent bytes. Not available with `pty`. * `"tail"`: the command never pauses for you. Each stream keeps only its most recent bytes, even while you are consuming it, so a slow consumer skips output without notice; `stdoutTruncated` and `stderrTruncated` say only that the result holds less than the command wrote. A pty command always behaves this way. *** ### PackageInstall A set of packages to install (apt or pip). #### Properties | Property | Type | Description | | ------------ | ----------- | -------------- | | `packages?` | `string`\[] | Package names. | *** ### Protocol > **Protocol** = `"tcp"` | `"http"` | `string` & `object` The protocol reported on a listener (open: tolerates unknown values). *** ### PtyConfig The pseudo-terminal a `pty` exec runs under. Every field has a default, so `{}` (or `pty: true`) is a usable terminal. #### Properties | Property | Type | Description | | -------- | -------- | ----------------------------------------------- | | `cols?` | `number` | Initial width in columns (default 80). | | `rows?` | `number` | Initial height in rows (default 24). | | `term?` | `string` | `$TERM` for the pty (default `xterm-256color`). | *** ### ResolvedConfig The config resolved from the environment and `~/.sail`. #### Extends * `Omit`\<`native.ResolvedConfig`, `"ingressScheme"` | `"mode"`> #### Properties | Property | Type | Description | Inherited from | | ---------------- | ----------------------------------- | ----------------------------------------------------- | -------------------- | | `apiKey?` | `string` | The resolved API key; absent when none is configured. | `Omit.apiKey` | | `apiUrl` | `string` | Sail API URL. | `Omit.apiUrl` | | `ingressBase` | `string` | Base host/URL public listeners are addressed under. | `Omit.ingressBase` | | `ingressScheme` | [`IngressScheme`](#ingressscheme-1) | - | - | | `sailboxApiUrl` | `string` | Sailbox-API URL. | `Omit.sailboxApiUrl` | *** ### RunCommand A shell command to run during the build. #### Properties | Property | Type | Description | | ----------- | -------- | ----------------------------------------- | | `command?` | `string` | The command, run via the builder's shell. | *** ### RunOptions Options for [Sailbox.run](#run): the subset of [ExecOptions](#execoptions) that fits a buffered, run-to-completion command. #### Extends * `Pick`\<[`ExecOptions`](#execoptions), `"timeoutSeconds"` | `"cwd"` | `"env"` | `"user"` | `"idempotencyKey"` | `"outputBufferBytes"`> #### Properties | Property | Type | Description | Inherited from | | --------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | | `check?` | `boolean` | Reject with `CommandFailedError` (carrying the completed result) when the command exits nonzero or times out, instead of resolving. | - | | `cwd?` | `string` | Working directory to run the command in (shell commands only). Unset starts the command in the image's working directory, or `/` when the image does not set one. | `Pick.cwd` | | `env?` | `Readonly`\<`Record`\<`string`, `string`>> | Extra environment for the command. Entries override the guest's defaults (including `LANG` and the `IS_SANDBOX=1` sandbox marker) and the image env, but a few reserved variables that identify the Sailbox (such as `SAILBOX_ID`) cannot be overridden. | [`ExecOptions`](#execoptions).[`env`](#env-1) | | `idempotencyKey?` | `string` | Stable key so a reconnect reattaches to the same command. An exec has one live handle at a time: a second handle started with the same key takes over the stream, and the first stops receiving live output and resolves from a bounded recorded result. A first handle reconnecting after a dropped connection can race a handle that attached meanwhile, and either handle's result may come back incomplete; avoid overlapping same-key handles. The UTF-8 value can be up to 256 KiB. | `Pick.idempotencyKey` | | `outputBufferBytes?` | `number` | Size of each stream's output buffer in bytes, 1 MiB by default. Must be between 64 KiB and 64 MiB. | `Pick.outputBufferBytes` | | `signal?` | `AbortSignal` | Aborting rejects with the signal's reason and force-cancels the remote command (SIGKILL, like [ExecProcess.cancel](#cancel) with `force`), briefly retrying transient failures. Best effort: the kill is sent once the submission settles (an abort mid-submission cancels the command as soon as its launch is confirmed), and a kill that still fails leaves the command running. | - | | `timeoutSeconds?` | `number` | Wall-clock limit in seconds before the server kills the command. | `Pick.timeoutSeconds` | | `user?` | `string` | Run the command as this guest user: a user name or numeric uid, optionally with a group appended after a colon (`"alice"`, `"1000"`, `"alice:staff"`, 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 unset, commands run as the image's `USER` if the image sets one, root otherwise; pass `"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 `"0:0"` needs no upgrade. | `Pick.user` | *** ### SailboxCheckpoint A durable checkpoint handle. #### Extends * `Omit`\<`native.SailboxCheckpoint`, `"status"` | `"expiresAt"`> #### Properties | Property | Type | Description | Inherited from | | ----------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | `checkpointGeneration` | `number` | Checkpoint generation captured by this checkpoint. | `Omit.checkpointGeneration` | | `checkpointId` | `string` | The checkpoint id. | `Omit.checkpointId` | | `expiresAt?` | `Date` | When the checkpoint expires: seven days out unless a TTL asked for a different window. Starting a Sailbox from it after that fails. | - | | `sailboxId` | `string` | The Sailbox the checkpoint was taken from. | `Omit.sailboxId` | | `status` | [`SailboxStatus`](#sailboxstatus-1) | - | - | *** ### SailboxDeprecation > **SailboxDeprecation** = `native.SailboxDeprecation` Actionable notice that a Sailbox's runtime should be upgraded: a `deadline` date and a `message` with upgrade instructions. *** ### SailboxHandle Returned by create / resume / fromCheckpoint: the Sailbox's identity and lifecycle status. #### Properties | Property | Type | Description | | ------------ | ----------------------------------- | ----------------------------------------------------- | | `name` | `string` | The caller-supplied Sailbox name. | | `sailboxId` | `string` | The Sailbox's stable identifier. | | `status` | [`SailboxStatus`](#sailboxstatus-1) | Lifecycle status at the time the operation completed. | *** ### SailboxInfo A read snapshot of a Sailbox (get / list). Timestamps are RFC 3339 strings. #### Extends * `Omit`\<`native.SailboxInfo`, `"status"` | `"autoSleep"` | `"networkPolicy"`> #### Properties | Property | Modifier | Type | Description | Inherited from | | ----------------------- | ---------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------- | | `appId` | `public` | `string` | Identifier of the owning app. | `Omit.appId` | | `appName` | `public` | `string` | Name of the owning app. | `Omit.appName` | | `architecture` | `public` | `string` | CPU architecture (for example `arm64`). | `Omit.architecture` | | `autoSleep?` | `public` | [`AutoSleep`](#autosleep-4) | When Sail may sleep this Sailbox on its own. | - | | `checkpointGeneration` | `public` | `number` | Monotonic checkpoint generation counter. | `Omit.checkpointGeneration` | | `cpuRequestedVcpu` | `public` | `number` | Requested CPU, in vCPUs. | `Omit.cpuRequestedVcpu` | | `cpuUsedVcpu` | `public` | `number` | Current CPU usage, in vCPUs. | `Omit.cpuUsedVcpu` | | `createdAt` | `public` | `string` | When the Sailbox was created (RFC 3339). | `Omit.createdAt` | | `createdByUserId?` | `public` | `string` | The user whose credential created this Sailbox (for a restore, the user who ran it). Absent for service-key creates. | `Omit.createdByUserId` | | `deprecation?` | `public` | `SailboxDeprecation` | Actionable runtime deprecation notice, when an upgrade is needed. | `Omit.deprecation` | | `diskRequestedBytes` | `public` | `number` | Requested disk, in bytes. | `Omit.diskRequestedBytes` | | `diskUsedBytes` | `public` | `number` | Current disk usage, in bytes. | `Omit.diskUsedBytes` | | `errorMessage?` | `public` | `string` | Human-readable error detail when the Sailbox is in an error state. | `Omit.errorMessage` | | `guestSchemaVersion?` | `public` | `number` | The Sailbox runtime schema version the Sailbox last booted with. | `Omit.guestSchemaVersion` | | `imageId` | `public` | `string` | Identifier of the image the Sailbox was created from. | `Omit.imageId` | | `lastCheckpointedAt?` | `public` | `string` | When the most recent checkpoint was taken, if any (RFC 3339). | `Omit.lastCheckpointedAt` | | `memoryMib` | `public` | `number` | Configured memory, in MiB. | `Omit.memoryMib` | | `memoryRequestedBytes` | `public` | `number` | Requested memory, in bytes. | `Omit.memoryRequestedBytes` | | `memoryUsedBytes` | `public` | `number` | Current memory usage, in bytes. | `Omit.memoryUsedBytes` | | `name` | `public` | `string` | The Sailbox name. | `Omit.name` | | `networkPolicy?` | `readonly` | [`NetworkPolicyInfo`](#networkpolicyinfo) | The Sailbox's network policy; absent means public. Frozen when read. | - | | `sailboxId` | `public` | `string` | The Sailbox id. | `Omit.sailboxId` | | `startedAt?` | `public` | `string` | When the Sailbox first started running, if it ever has (RFC 3339). A resume does not rewrite it. | `Omit.startedAt` | | `stateDiskSizeGib` | `public` | `number` | Configured state-disk size, in GiB. | `Omit.stateDiskSizeGib` | | `status` | `public` | [`SailboxStatus`](#sailboxstatus-1) | - | - | | `updatedAt` | `public` | `string` | When the Sailbox was last updated (RFC 3339). | `Omit.updatedAt` | | `vcpuCount` | `public` | `number` | Configured number of vCPUs. | `Omit.vcpuCount` | | `visibility?` | `public` | `string` | `"private"` when access is restricted to the creator; absent/`"org"` is the default org-wide access. | `Omit.visibility` | | `volumeMounts` | `public` | `SailboxVolumeMount`\[] | Volumes attached to this Sailbox and the paths they are mounted at. Empty when the Sailbox has none. | `Omit.volumeMounts` | *** ### SailboxInfoPage One page of list results plus the pagination envelope. #### Extends * `Omit`\<`native.SailboxInfoPage`, `"items"`> #### Properties | Property | Type | Description | Inherited from | | ---------- | -------------------------------- | ------------------------------------------ | -------------- | | `hasMore` | `boolean` | Whether more results exist past this page. | `Omit.hasMore` | | `items` | [`SailboxInfo`](#sailboxinfo)\[] | - | - | | `limit` | `number` | The page size that was applied. | `Omit.limit` | | `offset` | `number` | The offset that was applied. | `Omit.offset` | | `total` | `number` | Total matching Sailboxes across all pages. | `Omit.total` | *** ### SailboxListOrder > **SailboxListOrder** = `"newest_active"` | `"newest_created"` Result ordering for a Sailbox list: most recently active first, or newest created first. *** ### SailboxPage One page of [Sailbox](#sailbox) instances plus the pagination envelope. #### Extends * `Omit`\<[`SailboxInfoPage`](#sailboxinfopage), `"items"`> #### Properties | Property | Type | Description | Inherited from | | ---------- | ------------------------ | ------------------------------------------ | -------------- | | `hasMore` | `boolean` | Whether more results exist past this page. | `Omit.hasMore` | | `items` | [`Sailbox`](#sailbox)\[] | - | - | | `limit` | `number` | The page size that was applied. | `Omit.limit` | | `offset` | `number` | The offset that was applied. | `Omit.offset` | | `total` | `number` | Total matching Sailboxes across all pages. | `Omit.total` | *** ### SailboxSize > **SailboxSize** = `"s"` | `"m"` | `"l"` Named resource size; each sets the vCPU count plus default memory/disk. *** ### SailboxStatus > **SailboxStatus** = `"running"` | `"paused"` | `"sleeping"` | `"failed"` | `"terminated"` | `string` & `object` Lifecycle status of a Sailbox. Open: tolerates values added server-side. *** ### SailboxStatusFilter > **SailboxStatusFilter** = `"running"` | `"paused"` | `"sleeping"` | `"failed"` | `"terminated"` The closed set of statuses accepted as a list filter. *** ### SecretInfo A secret's name and timestamps. Sail never returns the stored value, so no value field exists. Timestamps are RFC 3339 strings. #### Properties | Property | Type | Description | | ------------ | -------- | --------------------------------------------------- | | `createdAt` | `string` | When the secret was first set (RFC 3339). | | `name` | `string` | The secret's name, unique within your organization. | | `updatedAt` | `string` | When the secret's value last changed (RFC 3339). | *** ### ShellOptions Options for [Sailbox.shell](#shell-1). #### Properties | Property | Type | Description | | | | ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cwd?` | `string` | Working directory for the session. Unset starts it in the image's working directory, or `/` when the image does not set one. | | | | `env?` | `Readonly`\<`Record`\<`string`, `string`>> | Extra environment for the session, with the precedence and reserved names of [ExecOptions.env](#env-1). | | | | `noForward?` | `boolean` | While attached, the Sailbox's browser opens and localhost servers reach your machine, files dragged onto the terminal upload and paste as guest paths, and Ctrl+V forwards your clipboard. On devbox images the clipboard is two-way (pastes land on the Sailbox's clipboard, and what you copy inside the Sailbox comes back); other images upload a pasted image as a file and paste its path. Set `true` to turn all of it off, for example for an untrusted or automated session. | | | | `shell?` | `string` | Login shell to run when no command is given (default: the guest's `$SHELL`, else `/bin/bash`). Ignored when a command is given. | | | | `term?` | `string` | `$TERM` for the remote pty (default: the local `$TERM`). | | | | `timeoutSeconds?` | `number` | Wall-clock limit for the session in seconds; omit for no limit. | | | | `user?` | `string` | Run the session as this user (Docker's `USER` syntax: \`name | uid\[:group | gid]`). Unset runs as the image's `USER`when the image sets one, root otherwise: the same identity [Sailbox.exec](#exec-1) uses.`"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 [Sailbox.upgrade](#upgrade) is called. | *** ### SshEndpoint The public TCP endpoint a Sailbox's SSH listener is reachable at. #### Properties | Property | Type | Description | | -------- | -------- | ----------------- | | `host` | `string` | Hostname to dial. | | `port` | `number` | Port to dial. | *** ### TcpEndpoint The address to dial for a `tcp` listener. #### Properties | Property | Type | Description | | -------- | -------- | ----------------- | | `host` | `string` | Hostname to dial. | | `kind` | `"tcp"` | - | | `port` | `number` | Port to dial. | *** ### UpgradeResult The outcome of a Sailbox runtime upgrade. #### Extends * `Omit`\<`native.UpgradeResult`, `"status"`> #### Properties | Property | Type | Description | Inherited from | | ---------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | | `applied` | `boolean` | 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. | `Omit.applied` | | `status` | [`SailboxStatus`](#sailboxstatus-1) | - | - | *** ### VolumeInfo A managed NFS volume. Timestamps are RFC 3339 strings. #### Properties | Property | Type | Description | | ------------- | -------- | ----------------------------------------- | | `backend` | `string` | Storage backend serving the volume. | | `createdAt?` | `string` | Creation time (RFC 3339), if reported. | | `name` | `string` | The volume name. | | `status` | `string` | Lifecycle status. | | `updatedAt?` | `string` | Last-update time (RFC 3339), if reported. | | `volumeId` | `string` | The volume id. | *** ### VolumeMountInput An NFS volume to mount at create time. #### Properties | Property | Type | Description | | ------------ | -------- | ----------------------------------------------- | | `mountPath` | `string` | Absolute guest path to mount at. | | `volumeId` | `string` | The volume id (from `getVolume`/`listVolumes`). | *** ### WaitForListenerOptions Options for [Sailbox.waitForListener](#waitforlistener-1). #### Properties | Property | Type | Description | | ------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `signal?` | `AbortSignal` | Aborting stops the wait and rejects with the signal's reason. An abandoned in-flight probe winds down on its own (by `timeoutSeconds` at the latest); it does not touch the listener. Because the wind-down relies on the timeout, `signal` requires a finite `timeoutSeconds`. | | `timeoutSeconds?` | `number` | Give up waiting after this many seconds (default 60; `Infinity` waits indefinitely). | *** ### WriteOptions Options for uploading a file. #### Properties | Property | Type | Description | | | | ----------------- | --------- | ---------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `createParents?` | `boolean` | Create missing parent directories. | | | | `mode?` | `number` | Unix mode bits for the written file (default `0o644`). | | | | `user?` | `string` | Owner for the written file and any parent directories the write creates, in Docker's `USER` syntax: \`name | uid\[:group | gid]`. The write itself always runs as root, like `COPY --chown`, so it succeeds even where that owner could not write. Unset follows the image's `USER`when the image sets one, root otherwise: the same identity commands run as, so an uploaded file is usable by the code in the Sailbox. Pass`"0:0"`to force root ownership. Requires a Sailbox whose guest honors requested users; on older Sailboxes the write fails until [Sailbox.upgrade](#upgrade) is called, except with the exact spelling`"0:0"\`, which needs none. | ## Errors Errors thrown by this SDK surface. All of them extend [SailError](#sailerror), so an `instanceof SailError` check matches everything below. ### SailError Base class for every error surfaced by the SDK. #### Extends * `Error` #### Extended by * [`InvalidArgumentError`](#invalidargumenterror) * [`InternalError`](#internalerror) * [`NotFoundError`](#notfounderror) * [`PermissionDeniedError`](#permissiondeniederror) * [`FileNotFoundError`](#filenotfounderror) * [`BrokenPipeError`](#brokenpipeerror) * [`TimeoutError`](#timeouterror) * [`TransportError`](#transporterror) * [`ApiError`](#apierror) * [`SailboxCreationError`](#sailboxcreationerror) * [`ImageBuildError`](#imagebuilderror) * [`SailboxExecutionError`](#sailboxexecutionerror) #### Constructors ##### Constructor > **new SailError**(`message`, `code?`, `details?`): [`SailError`](#sailerror) ###### Parameters | Parameter | Type | Default value | | --------- | ------------------ | ------------- | | `message` | `string` | `undefined` | | `code` | `string` | `"SailError"` | | `details` | `SailErrorDetails` | `{}` | ###### Returns [`SailError`](#sailerror) ###### Overrides `Error.constructor` #### Properties | Property | Modifier | Type | Description | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | *** ### ApiError A non-2xx API response. #### Extends * [`SailError`](#sailerror) #### Extended by * [`SecretInUseError`](#secretinuseerror) * [`HttpPolicyInUseError`](#httppolicyinuseerror) #### Constructors ##### Constructor > **new ApiError**(`message`, `details?`): [`ApiError`](#apierror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`ApiError`](#apierror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `body?` | `readonly` | `unknown` | Parsed response body from the failed request, when available. | - | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | | `status?` | `readonly` | `number` | HTTP status code returned by the API, when the failure carries one. | - | *** ### BrokenPipeError A stream (e.g. exec stdin) was closed and can no longer be written. #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new BrokenPipeError**(`message`, `details?`): [`BrokenPipeError`](#brokenpipeerror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`BrokenPipeError`](#brokenpipeerror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### CommandFailedError Thrown by [Sailbox.run](#run) with `check` when the command exits nonzero or times out. Carries the completed result as [result](#result). #### Extends * [`SailboxExecutionError`](#sailboxexecutionerror) #### Constructors ##### Constructor > **new CommandFailedError**(`message`, `result`): [`CommandFailedError`](#commandfailederror) ###### Parameters | Parameter | Type | | --------- | --------------------------- | | `message` | `string` | | `result` | [`ExecResult`](#execresult) | ###### Returns [`CommandFailedError`](#commandfailederror) ###### Overrides [`SailboxExecutionError`](#sailboxexecutionerror).[`constructor`](#constructor-12) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailboxExecutionError`](#sailboxexecutionerror).[`code`](#code-12) | | `result` | `readonly` | [`ExecResult`](#execresult) | - | - | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailboxExecutionError`](#sailboxexecutionerror).[`retryable`](#retryable-12) | | `rpcStatus` | `readonly` | `string` | Transport status classifying the failure (for example `"unavailable"`), empty when the failure carries no status. Distinct from [code](#code-15), which stays the taxonomy discriminator on every SailError. | [`SailboxExecutionError`](#sailboxexecutionerror).[`rpcStatus`](#rpcstatus-2) | *** ### FileNotFoundError A remote file path does not exist. #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new FileNotFoundError**(`message`, `details?`): [`FileNotFoundError`](#filenotfounderror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`FileNotFoundError`](#filenotfounderror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### HttpPolicyInUseError Deleting an HTTP policy that is still attached to a Sailbox. Clear or replace it on every Sailbox first, then delete it. #### Extends * [`ApiError`](#apierror) #### Constructors ##### Constructor > **new HttpPolicyInUseError**(`message`, `details?`): [`HttpPolicyInUseError`](#httppolicyinuseerror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`HttpPolicyInUseError`](#httppolicyinuseerror) ###### Inherited from [`ApiError`](#apierror).[`constructor`](#constructor) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | `body?` | `readonly` | `unknown` | Parsed response body from the failed request, when available. | [`ApiError`](#apierror).[`body`](#body) | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`ApiError`](#apierror).[`code`](#code) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`ApiError`](#apierror).[`retryable`](#retryable) | | `status?` | `readonly` | `number` | HTTP status code returned by the API, when the failure carries one. | [`ApiError`](#apierror).[`status`](#status) | *** ### ImageBuildError A custom image could not be built or its local content could not be uploaded. #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new ImageBuildError**(`message`, `details?`): [`ImageBuildError`](#imagebuilderror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`ImageBuildError`](#imagebuilderror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### InternalError An unexpected internal SDK failure. #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new InternalError**(`message`, `details?`): [`InternalError`](#internalerror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`InternalError`](#internalerror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### InvalidArgumentError Invalid arguments or configuration (bad request, missing/invalid API key). #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new InvalidArgumentError**(`message`, `details?`): [`InvalidArgumentError`](#invalidargumenterror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`InvalidArgumentError`](#invalidargumenterror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### NotFoundError The Sailbox, volume, or other resource does not exist (or is another org's). #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new NotFoundError**(`message`, `details?`): [`NotFoundError`](#notfounderror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`NotFoundError`](#notfounderror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### PermissionDeniedError The credential is not permitted to perform the operation. #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new PermissionDeniedError**(`message`, `details?`): [`PermissionDeniedError`](#permissiondeniederror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`PermissionDeniedError`](#permissiondeniederror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### SailboxCreationError A Sailbox could not be created (provisioning failed). #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new SailboxCreationError**(`message`, `details?`): [`SailboxCreationError`](#sailboxcreationerror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`SailboxCreationError`](#sailboxcreationerror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `body?` | `readonly` | `unknown` | Parsed response body from the failed create request, when available. | - | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | | `status?` | `readonly` | `number` | HTTP status code returned by the create request, when the failure carries one. | - | *** ### SailboxExecRequestNotFoundError The exec request could not be found (for example after the Sailbox moved machines). #### Extends * [`SailboxExecutionError`](#sailboxexecutionerror) #### Constructors ##### Constructor > **new SailboxExecRequestNotFoundError**(`message`, `details?`): [`SailboxExecRequestNotFoundError`](#sailboxexecrequestnotfounderror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`SailboxExecRequestNotFoundError`](#sailboxexecrequestnotfounderror) ###### Overrides [`SailboxExecutionError`](#sailboxexecutionerror).[`constructor`](#constructor-12) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailboxExecutionError`](#sailboxexecutionerror).[`code`](#code-12) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailboxExecutionError`](#sailboxexecutionerror).[`retryable`](#retryable-12) | | `rpcStatus` | `readonly` | `string` | Transport status classifying the failure (for example `"unavailable"`), empty when the failure carries no status. Distinct from [code](#code-15), which stays the taxonomy discriminator on every SailError. | [`SailboxExecutionError`](#sailboxexecutionerror).[`rpcStatus`](#rpcstatus-2) | *** ### SailboxExecutionError Base class for failures during an exec. #### Extends * [`SailError`](#sailerror) #### Extended by * [`CommandFailedError`](#commandfailederror) * [`SailboxTerminatedError`](#sailboxterminatederror) * [`SailboxExecRequestNotFoundError`](#sailboxexecrequestnotfounderror) * [`SailboxHostLostError`](#sailboxhostlosterror) #### Constructors ##### Constructor > **new SailboxExecutionError**(`message`, `code?`, `details?`): [`SailboxExecutionError`](#sailboxexecutionerror) ###### Parameters | Parameter | Type | Default value | | --------- | ------------------ | ------------------------- | | `message` | `string` | `undefined` | | `code` | `string` | `"SailboxExecutionError"` | | `details` | `SailErrorDetails` | `{}` | ###### Returns [`SailboxExecutionError`](#sailboxexecutionerror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | | `rpcStatus` | `readonly` | `string` | Transport status classifying the failure (for example `"unavailable"`), empty when the failure carries no status. Distinct from [code](#code-15), which stays the taxonomy discriminator on every SailError. | - | *** ### SailboxHostLostError The machine hosting the Sailbox was lost while an exec was in flight. #### Extends * [`SailboxExecutionError`](#sailboxexecutionerror) #### Constructors ##### Constructor > **new SailboxHostLostError**(`message`, `details?`): [`SailboxHostLostError`](#sailboxhostlosterror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`SailboxHostLostError`](#sailboxhostlosterror) ###### Overrides [`SailboxExecutionError`](#sailboxexecutionerror).[`constructor`](#constructor-12) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailboxExecutionError`](#sailboxexecutionerror).[`code`](#code-12) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailboxExecutionError`](#sailboxexecutionerror).[`retryable`](#retryable-12) | | `rpcStatus` | `readonly` | `string` | Transport status classifying the failure (for example `"unavailable"`), empty when the failure carries no status. Distinct from [code](#code-15), which stays the taxonomy discriminator on every SailError. | [`SailboxExecutionError`](#sailboxexecutionerror).[`rpcStatus`](#rpcstatus-2) | *** ### SailboxTerminatedError The Sailbox was terminated while an exec was in flight. #### Extends * [`SailboxExecutionError`](#sailboxexecutionerror) #### Constructors ##### Constructor > **new SailboxTerminatedError**(`message`, `details?`): [`SailboxTerminatedError`](#sailboxterminatederror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`SailboxTerminatedError`](#sailboxterminatederror) ###### Overrides [`SailboxExecutionError`](#sailboxexecutionerror).[`constructor`](#constructor-12) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailboxExecutionError`](#sailboxexecutionerror).[`code`](#code-12) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailboxExecutionError`](#sailboxexecutionerror).[`retryable`](#retryable-12) | | `rpcStatus` | `readonly` | `string` | Transport status classifying the failure (for example `"unavailable"`), empty when the failure carries no status. Distinct from [code](#code-15), which stays the taxonomy discriminator on every SailError. | [`SailboxExecutionError`](#sailboxexecutionerror).[`rpcStatus`](#rpcstatus-2) | *** ### SecretInUseError Deleting a secret that HTTP policies still refer to. Delete those policies first, then delete the secret. #### Extends * [`ApiError`](#apierror) #### Constructors ##### Constructor > **new SecretInUseError**(`message`, `details?`): [`SecretInUseError`](#secretinuseerror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`SecretInUseError`](#secretinuseerror) ###### Inherited from [`ApiError`](#apierror).[`constructor`](#constructor) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | `body?` | `readonly` | `unknown` | Parsed response body from the failed request, when available. | [`ApiError`](#apierror).[`body`](#body) | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`ApiError`](#apierror).[`code`](#code) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`ApiError`](#apierror).[`retryable`](#retryable) | | `status?` | `readonly` | `number` | HTTP status code returned by the API, when the failure carries one. | [`ApiError`](#apierror).[`status`](#status) | *** ### TimeoutError A request exceeded its timeout. #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new TimeoutError**(`message`, `details?`): [`TimeoutError`](#timeouterror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`TimeoutError`](#timeouterror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) | *** ### TransportError A network/connection transport failure. #### Extends * [`SailError`](#sailerror) #### Constructors ##### Constructor > **new TransportError**(`message`, `details?`): [`TransportError`](#transporterror) ###### Parameters | Parameter | Type | | --------- | ------------------ | | `message` | `string` | | `details` | `SailErrorDetails` | ###### Returns [`TransportError`](#transporterror) ###### Overrides [`SailError`](#sailerror).[`constructor`](#constructor-15) #### Properties | Property | Modifier | Type | Description | Inherited from | | ------------ | ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `code` | `readonly` | `string` | Stable, language-neutral error code (e.g. `"NotFound"`). | [`SailError`](#sailerror).[`code`](#code-15) | | `retryable` | `readonly` | `boolean` | Advisory: `true` when retrying the same call may succeed (a transport failure, HTTP 429 or a gateway 5xx, a transient exec failure). `false` when the failure is deterministic (bad argument, missing resource, permission). The SDK's built-in retry already covers the transient window for most calls; this classifies what leaks past it. | [`SailError`](#sailerror).[`retryable`](#retryable-15) |
# Sending requests at scale Source: https://docs.sailresearch.com/requests_at_scale Best practices for submitting thousands of concurrent requests to the Sail API When you need to submit thousands (or tens of thousands) of requests to Sail, serial requests become a major bottleneck. Sail offers two ways to handle high-volume workloads: the **Batch API** and the **Responses API**. ## Batch API The Batch API lets you submit up to 100,000 requests in a single call (max 256 MB per batch). 1. **Submit a batch.** Send all your requests in one `POST /batches` call. 2. **Poll for status.** Check `GET /batches/{batch_id}` until all requests are completed. 3. **Retrieve results.** Fetch individual results via `GET /batches/{batch_id}/{custom_id}`. To view all your previously submitted batches, use `GET /batches`. Attach an `Idempotency-Key` header on submission so a client retry after a network blip replays the original batch reservation instead of creating a duplicate. See [Idempotent Requests](/idempotency). Batch items default to `metadata.completion_window: "balanced"` when the field is omitted. If you set it explicitly, it must be either `"balanced"` or `"flex"`. Other values are rejected for batch items. For low-latency workloads, use the [Responses API](#responses-api-with-background-mode) instead. See [Completion Windows](/completion-windows) for the full tier definitions and per-model availability. ### Python example First, install the `requests` library: ```bash theme={null} pip install requests ``` ```python theme={null} import time import requests BASE_URL = "https://api.sailresearch.com/v1" API_KEY = "YOUR_KEY_HERE" HEADERS = {"Authorization": f"Bearer {API_KEY}"} # 1. Submit a batch batch_requests = [ { "custom_id": f"request-{i}", "params": { "model": "zai-org/GLM-5.3", "max_output_tokens": 2048, "input": [{"role": "user", "content": f"What is a fun fact about the number {i}?"}], "metadata": {"completion_window": "balanced"}, }, } for i in range(100) ] resp = requests.post( f"{BASE_URL}/batches", headers=HEADERS, json={ "endpoint": "/v1/responses", "label": "my-batch-job", "requests": batch_requests, }, ) batch = resp.json() batch_id = batch["id"] print(f"Created batch {batch_id} with {batch['request_counts']} requests") # 2. Poll until all requests are completed while True: status_resp = requests.get(f"{BASE_URL}/batches/{batch_id}", headers=HEADERS) status = status_resp.json() request_status = status["request_status"] done = sum(1 for r in request_status.values() if r["status"] in ("COMPLETED", "FAILED", "CANCELLED")) print(f"Progress: {done}/{len(request_status)}") if done == len(request_status): break time.sleep(5) # 3. Retrieve results for custom_id, info in request_status.items(): if info["status"] == "COMPLETED": result = requests.get( f"{BASE_URL}/batches/{batch_id}/{custom_id}", headers=HEADERS ).json() output_text = "".join( c["text"] for item in result.get("output", []) for c in item.get("content", []) if c.get("type") == "output_text" ) print(f"{custom_id}: {output_text[:100]}...") ``` ## Responses API with background mode You can also submit requests individually using the Responses API with `background=True`. For large-volume workloads (1,000+ requests), we recommend: 1. **Use `AsyncOpenAI` with `DefaultAioHttpClient()`.** The OpenAI SDK's built-in aiohttp client is more efficient than the default `httpx` backend for high-concurrency workloads. 2. **Gate concurrency with an `asyncio.Semaphore`.** This gives you fine-grained control over how many simultaneous connections are opened (e.g. 200), preventing connection exhaustion. 3. **Submit all requests concurrently with `background=True`, then poll.** Fire off all submissions in parallel (gated by the semaphore), collect the response IDs, and poll for completions in a separate loop. 4. **Send a per-request `Idempotency-Key`** so a retry after a transient failure replays the reservation instead of duplicating inference work. See [Idempotent Requests](/idempotency). ### Python example First, install tqdm and the OpenAI SDK with the aiohttp extra: ```bash theme={null} pip install tqdm pip install 'openai[aiohttp]' ``` ```python theme={null} import argparse import asyncio from tqdm import tqdm from openai import DefaultAioHttpClient from openai import AsyncOpenAI async def main( num_requests: int, input_tokens: int, max_output_tokens: int, model: str, ): base_url = "https://api.sailresearch.com/v1" api_key = "YOUR_KEY_HERE" async with AsyncOpenAI( base_url=base_url, api_key=api_key, http_client=DefaultAioHttpClient(), ) as client: # List supported models models = await client.models.list() supported_models = [m.id for m in models.data] print("Supported Models:") print(supported_models) # Submit requests concurrently sem = asyncio.Semaphore(200) pbar_submit = tqdm(total=num_requests, desc="Submitting requests") async def submit(i): filler = "" if input_tokens > 0: filler = " " + ("word " * input_tokens) content = f"TASK {i}: What is a fun fact about the number {i}? Then, find the word at index {i} in the following sequence of words: {filler}" async with sem: response = await client.responses.create( model=model, input=[{"role": "user", "content": content}], max_output_tokens=max_output_tokens, background=True, ) pbar_submit.update(1) return response.id response_ids = await asyncio.gather(*[submit(i) for i in range(num_requests)]) pbar_submit.close() response_ids = list(response_ids) print(f"Created {len(response_ids)} response IDs") # Poll for completions usable = {} poll_sem = asyncio.Semaphore(200) pbar = tqdm(total=len(response_ids), desc="Polling responses") async def poll(response_id): for _ in range(3600): # 1 hour timeout async with poll_sem: response = await client.responses.retrieve(response_id) if response.status == "completed": usable[response_id] = response pbar.update(1) return if response.status == "incomplete": reason = getattr(response.incomplete_details, "reason", None) if reason == "max_output_tokens": usable[response_id] = response else: print(f"{response_id} is incomplete because of {reason or 'an unknown reason'}") pbar.update(1) return if response.status in {"failed", "cancelled"}: print(f"{response_id} ended with status {response.status}") pbar.update(1) return await asyncio.sleep(1) print(f"Timed out waiting for {response_id}") await asyncio.gather(*[poll(rid) for rid in response_ids]) pbar.close() for response_id in response_ids: resp = usable.get(response_id) if resp is None: continue output_text = "".join( c.text for item in resp.output for c in getattr(item, "content", []) or [] if getattr(c, "type", None) == "output_text" ) print(f"\n\n{response_id} {resp.status} with output:\n{output_text}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--num-requests", type=int, default=20000) parser.add_argument("--input-tokens", type=int, default=50) parser.add_argument("--max-output-tokens", type=int, default=4000) parser.add_argument("--model", type=str, default="zai-org/GLM-5.3") args = parser.parse_args() asyncio.run( main( num_requests=args.num_requests, input_tokens=args.input_tokens, max_output_tokens=args.max_output_tokens, model=args.model, ) ) ``` The semaphore limit of 200 is a good starting point. Lower it if you run into connection errors or timeouts; raise it if you have headroom and want faster submission throughput. # Sailboxes Source: https://docs.sailresearch.com/sailbox-sdk The Sailbox class: create, operate, and tear down Sailboxes A `Sailbox` is a Linux VM on the Sail platform: a full cloud environment designed for long-horizon agents. Create one, then run commands, transfer files, expose ports, and checkpoint or pause it. For a guided walkthrough, see the [Sailboxes guide](/sailboxes); this page is the API reference. In a language without a Sail SDK, reach for the [HTTP API](/sailboxes-http-api). ```python Python theme={null} import sail sb = sail.Sailbox.create( app=sail.App.find(name="example-app", mint_if_missing=True), name="sandbox-1", ) print(sb.sailbox_id, sb.status) ``` ```typescript TypeScript theme={null} import { App, Sailbox } from "@sailresearch/sdk"; const app = await App.find("example-app", { mintIfMissing: true }); const sb = await Sailbox.create({ app, name: "sandbox-1" }); console.log(sb.sailboxId, sb.status); ``` ```rust Rust theme={null} use sail::{Client, CreateSailboxRequest}; let client = Client::from_env()?; let app = client.find_app("example-app", /* mint_if_missing */ true).await?; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "sandbox-1".into(), ..Default::default() }, /* timeout */ None, ) .await?; println!("{}", sb.sailbox_id()); ``` ## Sync and async * **Python** methods are synchronous, and every method that does I/O has an async twin under `.aio` (`await sb.exec.aio(...)`). Handles returned by async calls are already async: `await sb.exec.aio(...)` returns a process whose own methods (`wait`, `cancel`, `resize`) are awaited directly, with no `.aio`. The one exception to the rule is [`shell`](#shell), which is sync-only since it drives your local terminal. * **TypeScript** is async-only: every operation returns a `Promise`. * **Rust** is async-only, on a Tokio runtime. Synchronous code can drive any call with `sail::block_on`. ```python Python theme={null} import asyncio import sail async def main(): app = await sail.App.find.aio(name="example-app", mint_if_missing=True) sb = await sail.Sailbox.create.aio(app=app, name="sandbox-1") proc = await sb.exec.aio("echo hi") result = await proc.wait() print(result.stdout) await sb.terminate.aio() asyncio.run(main()) ``` ```typescript TypeScript theme={null} const sb = await Sailbox.create({ app, name: "sandbox-1" }); const proc = await sb.exec("echo hi"); const result = await proc.wait(); console.log(result.stdout); await sb.terminate(); ``` ```rust Rust theme={null} use sail::ExecOptions; let sb = client.sailbox("sb_..."); let proc = sb.exec_shell("echo hi", ExecOptions::default()).await?; let result = proc.wait().await?; print!("{}", result.stdout); sb.terminate().await?; ``` ## Naming The three SDKs expose the same operations with each language's conventions: Python is `snake_case` with keyword arguments, TypeScript is `camelCase` with options objects and unit-suffixed durations (`timeoutSeconds`), and Rust pairs a `Sailbox` object with explicit argument structs. Parameter tables on this page use the Python spelling; each section's TypeScript signature shows the real names. Every TypeScript static also accepts an explicitly constructed `client` in its options object (see [Configuration](/reference/sdk-configuration)); the signatures on this page omit it. ## Attributes Every `Sailbox` carries its identity and lifecycle state: `sailbox_id`, `name`, and `status` (`running`, `paused`, `sleeping`, `failed`, `terminated`). A `Sailbox` returned by [`get`](#sailbox-get) or [`list`](#sailbox-list) also carries the monitoring snapshot from that fetch: owning app, image, configured and observed resource usage, timestamps, and its network policy (see [snapshot fields](#sailboxinfo); absent means public). Treat `sailbox_id` as the durable handle: store the id, and turn it back into a usable `Sailbox` any time with [`Sailbox.get`](#sailbox-get) (which fetches fresh state), or without a network call via `Sailbox.from_id` / `Sailbox.fromId` / `client.sailbox(id)`. The `get` result reflects the Sailbox at the time of the call; call it again for fresh state. *** ## Sailbox.create ```python Python theme={null} @classmethod def create( *, app: App | str, image: ImageDefinition | None = None, name: str, image_build_timeout: int = 1800, timeout: int = 600, size: SailboxSize | None = None, memory_limit_gib: int | None = None, disk_limit_gib: int | None = None, ingress_ports: Sequence[int | IngressPort] | None = (), volumes: Mapping[str, Volume | str] | None = None, visibility: Literal["org", "private"] = "org", network_policy: NetworkPolicy | NetworkAllowlist | Literal["public", "no_network"] = NetworkPolicy.PUBLIC, ) -> Sailbox ``` ```typescript TypeScript theme={null} static create(options: { app: App | string; name: string; image?: ImageSpec | Image; // defaults to a plain Debian base imageBuildTimeoutSeconds?: number; // 1800 timeoutSeconds?: number; // 600 per create attempt; 0 = unbounded size?: "s" | "m" | "l"; memoryLimitGib?: number; diskLimitGib?: number; ingressPorts?: (number | IngressPortInput)[]; volumes?: Record; visibility?: "org" | "private"; networkPolicy?: NetworkPolicy; }): Promise ``` ```rust Rust theme={null} pub async fn create_sailbox( &self, // Client req: &CreateSailboxRequest, timeout: Option, ) -> Result ``` Creates a new Sailbox. The SDK builds any custom image first, then blocks until the VM is running or creation fails. `size` is a resource ceiling, not a billing reservation; ongoing Sailbox billing uses your actual CPU, memory, and disk consumption. Each size also has a one-time creation charge. `memory_limit_gib` and `disk_limit_gib` optionally tune the size's ceilings, in whole GiB within the size's range; raising a ceiling does not increase the ongoing rate, and lowering one caps what the Sailbox can use. See [Sailbox pricing](/sailboxes-pricing). | Parameter | Default | Description | | --------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `image` | `None` | A `sail.Image` value or custom [image definition](/sailbox-sdk-images). Defaults to a prebuilt Debian base (no image build). | | `app` | required | The owning [app](/sailbox-sdk-apps): an `App` from `App.find()`, or its id. | | `name` | required | Human-readable Sailbox name. | | `image_build_timeout` | `1800` | Seconds to wait for a custom image build before creating the VM. Must be `> 0`. | | `timeout` | `600` | Seconds to bound each create attempt while the Sailbox waits for capacity and boots. Pass `0` to wait without a client-side bound. | | `size` | `None` | Resource size: `"s"`, `"m"` (the platform default), or `"l"`. Each size sets the vCPU count plus default memory and disk; `"s"` gives the fastest cold starts and resumes, and caps what a runaway workload can consume. | | `memory_limit_gib` | `None` | Memory ceiling in whole GiB, within the size's range: 2-64 for `"s"`, 8-128 for `"m"`, 16-256 for `"l"`. The size's default when omitted. | | `disk_limit_gib` | `None` | Disk ceiling in whole GiB, within the size's range: 8-128 for `"s"`, 32-512 for `"m"`, 64-1024 for `"l"`. The size's default when omitted. | | `ingress_ports` | empty | Guest ports to expose. Each entry is a bare `int` (HTTP shorthand) or an [`IngressPort`](#ingressport). | | `volumes` | `None` | Shared volumes to mount, mapping an absolute guest path to a [`Volume`](#volumes) (or its id). | | `visibility` | `"org"` | Who may operate the Sailbox: `"org"` (anyone in your org) or `"private"` (only you). See below. | | `network_policy` | `NetworkPolicy.PUBLIC` | How the Sailbox may reach the network, chosen at creation and fixed for its life: `NetworkPolicy.PUBLIC` (the default), `NetworkPolicy.NO_NETWORK` (TypeScript `"no_network"`) for no network access at all, or a `NetworkAllowlist(...)` to allow only the listed destinations. `NO_NETWORK` cannot be combined with `ingress_ports`; an allowlist can. See [Network policy](#network-policy). | 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). **Returns** a running `Sailbox`. **Raises** a creation error when Sail cannot create the Sailbox, a permission error on 401/403, and an invalid-argument error for an unknown `size` or invalid ingress ports and volume mounts. In Python, pin `image=sail.Image.debian_arm64` or `debian_amd64` when using `@sail.function`; those images match your local Python version so function bytecode can deserialize in the guest. See [Images & Functions](/sailbox-sdk-images). ### SSH For a quick shell, use [`shell()`](#shell) or `sail box shell`: it streams a PTY over the same channel as `exec` and uses no ingress port. SSH is opt-in, for when you want a standard SSH endpoint: a devbox, your own client, `scp`, or port forwarding. Enable SSH after create with [`enable_ssh`](#sailbox-enable-ssh), which trusts your org's SSH certificate authority, starts `sshd`, and exposes guest port `22` as raw TCP. See the [SSH access guide](/sailboxes-networking#ssh-access) for the access model, how to connect, and source restrictions. `visibility` chooses who may operate the Sailbox, fixed for its life. `"org"`, the default, lets any credential in your org exec, copy files, SSH into, or run lifecycle operations on it. `"private"` restricts all of that to you: only your credential can operate the Sailbox (your org can still see it in listings, but not act on it). It requires an API key minted by your user, not a service key. Org admins can override the restriction for exec, file, setting a wake time, and the pause, sleep, resume, terminate, and upgrade operations by setting `SAIL_OWNER_OVERRIDE_REASON` (or the `X-Sail-Owner-Override-Reason` header on raw HTTP calls); exposing or removing listeners, checkpoint, and restore stay creator-only. Every override requires that reason, and your org's audit log records it. SSH has no override: a private Sailbox's SSH server accepts only its creator's certificates. From the CLI, pass `--visibility private` to `sail box create`. *** ## Sailbox.get ```python Python theme={null} @classmethod def get(sailbox_id: str) -> Sailbox ``` ```typescript TypeScript theme={null} static get(sailboxId: string): Promise ``` ```rust Rust theme={null} pub fn sailbox(&self, sailbox_id: impl Into) -> Sailbox // Client ``` Fetches a Sailbox by id and returns a fully usable `Sailbox`: run commands, read and write files, and manage listeners on it directly. `get` never wakes a paused or sleeping Sailbox. Operations that run inside it (commands, file reads and writes, network traffic) wake a sleeping Sailbox on demand; a paused Sailbox rejects them until you call `resume`. The returned object reflects the Sailbox at the time of the call, so call `get` again for fresh state. Unknown and wrong-org ids both raise a not-found error, so you cannot tell an unknown id from one owned by another org. In Rust, `client.sailbox(id)` binds the id without a network call; fetch current state with `sb.info()`. ```python Python theme={null} sb = sail.Sailbox.get("sb_...") result = sb.exec("echo hello").wait() ``` ```typescript TypeScript theme={null} const sb = await Sailbox.get("sb_..."); const result = await (await sb.exec("echo hello")).wait(); ``` ```rust Rust theme={null} use sail::ExecOptions; let sb = client.sailbox("sb_..."); let result = sb .exec_shell("echo hello", ExecOptions::default()) .await? .wait() .await?; ``` *** ## Sailbox.list ```python Python theme={null} @classmethod def list( *, app_id: str | None = None, status: str | None = None, search: str | None = None, order: Literal["newest_active", "newest_created"] | None = None, limit: int | None = None, ) -> list[Sailbox] ``` ```typescript TypeScript theme={null} static list(params?: { appId?: string; status?: SailboxStatusFilter; search?: string; order?: SailboxListOrder; limit?: number; }): Promise ``` ```rust Rust theme={null} pub async fn list_sailboxes( &self, // Client query: &ListSailboxesQuery, ) -> Result ``` Lists Sailboxes for the current org. Python and TypeScript fetch pages internally until every match (or `limit` of them) is collected; `limit` caps the total returned, bounding the fetch for large orgs. (Rust's `list_sailboxes` returns one page with its envelope.) `app_id` filters by the owning app id (resolve a name through [`App.find`](/sailbox-sdk-apps) first). `search` filters by name substring. `order` is `"newest_active"` (most recently active first, the default) or `"newest_created"` (newest-created first). Use [`Sailbox.list_page`](#sailbox-list-page) to control paging yourself or to read the pagination envelope. *** ## Sailbox.list\_page ```python Python theme={null} @classmethod def list_page( *, app_id: str | None = None, status: str | None = None, search: str | None = None, order: Literal["newest_active", "newest_created"] | None = None, limit: int = 50, offset: int = 0, ) -> SailboxPage ``` ```typescript TypeScript theme={null} static listPage(query?: ListSailboxesQuery): Promise ``` ```rust Rust theme={null} pub async fn list_sailboxes(&self, query: &ListSailboxesQuery) -> Result ``` Same call as `Sailbox.list`, but returns a [`SailboxPage`](#sailboxpage) with the Sailboxes plus the `limit`/`offset`/`total`/`has_more` pagination envelope. (In Rust, `list_sailboxes` always returns the page.) *** ## exec ```python Python theme={null} def exec( command: str | Sequence[str] | SailFunction, *function_args, timeout: int | None = None, background: bool = False, cwd: str | None = None, open_stdin: bool = False, pty: bool | PtyConfig = False, env: Mapping[str, str] | None = None, user: str | int | None = None, idempotency_key: str | None = None, output_mode: OutputMode | Literal["auto", "pipe", "tail"] = "auto", output_buffer_bytes: int = 1048576, kwargs: Mapping | None = None, ) -> ExecProcess | Any ``` ```typescript TypeScript theme={null} exec(command: string | readonly string[], options?: { timeoutSeconds?: number; background?: boolean; cwd?: string; openStdin?: boolean; pty?: boolean | PtyConfig; env?: Record; user?: string; idempotencyKey?: string; outputMode?: "auto" | "pipe" | "tail"; outputBufferBytes?: number; }): Promise ``` ```rust Rust theme={null} pub async fn exec_shell(&self, command: &str, options: ExecOptions) -> Result; pub async fn exec(&self, argv: impl IntoIterator>, options: ExecOptions) -> Result; ``` Runs a command in the Sailbox and returns a process handle. By default a stream you are reading pauses the command when you fall behind, so nothing is lost until a cancel or the exec `timeout` ends the pauses, and a stream you are not reading keeps only its most recent 1 MiB. To get every byte, start reading right after `exec` returns. `output_mode` and `output_buffer_bytes` (`outputMode` and `outputBufferBytes` in TypeScript) change that; what counts as reading a stream differs by language. See [Exec process](#sailboxexecprocess). A string command runs via `/bin/sh -lc`, so shell syntax (pipes, redirects, `&&`) works. Python and TypeScript also accept an argv list, which execs the program directly with no shell interpretation, and Rust separates the two as `exec_shell` (string) and `exec` (argv). Python's `exec` additionally accepts a `@sail.function`-decorated Python callable; it then blocks and returns the function's return value directly. For a function, `output_mode` must stay `auto`, and the function's complete encoded response (its serialized return value, captured stdout and stderr, and any error details, as encoded on the wire) must fit `output_buffer_bytes`; a larger response raises `SailboxFunctionSerializationError`. A second call with the same `idempotency_key` while the function runs takes over its output, and the earlier call may then fail to decode its result. See [Images & Functions](/sailbox-sdk-images). | Parameter | Default | Description | | --------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `command` | required | The command to run. | | `timeout` | `None` | Command runtime budget in seconds. Omit for no SDK-imposed limit. Must be `> 0` when set. | | `background` | `False` | Launch through a detached shell that returns immediately (string commands only). The command's output is discarded, so live output stays empty and `wait()` only confirms the launcher started it. | | `cwd` | `None` | Working directory to run the command from (string commands only). | | `open_stdin` | `False` | Open the command's stdin for writing. When `False`, stdin reads as instant EOF. | | `pty` | `False` | Run the command under a pseudo-terminal: `True` for the default terminal, or a `PtyConfig(term=..., cols=..., rows=...)` (TS `{ term, cols, rows }`) to set `$TERM` and the initial window, which default to `xterm-256color` and 80x24. `isatty()` is true, control bytes written to stdin become signals, and `resize(cols, rows)` adjusts the window. stdout and stderr merge onto one output stream. Implies `open_stdin`. | | `env` | `None` | Extra environment variables for the command. Entries override the guest defaults (including `LANG` and the `IS_SANDBOX=1` sandbox marker) and the image environment. A few reserved variables that identify the Sailbox (such as `SAILBOX_ID`) cannot be overridden. For `pty` commands the local `COLORTERM`, `LANG`, `LC_*`, and `TERM_PROGRAM` are forwarded automatically for keys not set here. | | `user` | `None` | Run the command as this user: a user name or numeric uid, optionally with a group after a colon (`"alice"`, `1000`, `"alice:staff"`, the Docker `USER` syntax). A named user or group must exist in the Sailbox's `/etc/passwd` / `/etc/group`; a numeric uid or gid need not. `HOME` (plus `USER` and `LOGNAME` when a name resolves) default to the account, with `env` entries still winning. The command starts in the image's working directory when this user can enter it, and in the filesystem root otherwise. When `user` is not given, commands run as the image's `USER` if the image sets one, root otherwise; pass `user="0:0"` to force root (`"root"` is a user name like any other, resolved through the Sailbox's `/etc/passwd`). Sailboxes created before user support shipped must call [`upgrade`](#upgrade) once first; the exact spelling `user="0:0"` needs no upgrade. | | `idempotency_key` | `None` | Defaults to a generated key, so retrying the initial submission won't double-launch the command. An exec has one live handle at a time: a second handle started with the same key takes over the stream, and the first stops receiving live output and resolves from a bounded recorded result. A first handle reconnecting after a dropped connection can race a handle that attached meanwhile, and either handle's result may come back incomplete; avoid overlapping same-key handles. | | `output_mode` | `"auto"` | What happens when a stream's buffer fills: `auto`, `pipe`, or `tail`. See **Output limits** under [Exec process](#sailboxexecprocess). `pipe` is not available with `pty`. | | `output_buffer_bytes` | `1048576` | Size of each stream's buffer in bytes, from 64 KiB to 64 MiB. It is what `wait()` returns per stream and how far a reader can fall behind before the command pauses (`auto`, `pipe`) or output is dropped (`tail`). | ```python Python theme={null} result = sb.exec("echo hi", timeout=5).wait() print(result.stdout, result.exit_code) # Stream output as it arrives (a chunk can be a partial line); the loop # ends when the command finishes: proc = sb.exec("for i in 1 2 3; do echo $i; sleep 1; done") for chunk in proc.stdout: print(chunk, end="") # Piping stdin: proc = sb.exec("wc -l", open_stdin=True) proc.stdin.write("one\ntwo\n") proc.stdin.close() print(proc.wait().stdout) ``` ```typescript TypeScript theme={null} const result = await (await sb.exec("echo hi", { timeoutSeconds: 5 })).wait(); console.log(result.stdout, result.exitCode); // Stream output as it arrives (a chunk can be a partial line); the loop // ends when the command finishes: const proc = await sb.exec("for i in 1 2 3; do echo $i; sleep 1; done"); for await (const chunk of proc.stdout) { process.stdout.write(chunk); } // Piping stdin: const wc = await sb.exec(["wc", "-l"], { openStdin: true }); await wc.writeStdin("one\ntwo\n"); await wc.closeStdin(); console.log((await wc.wait()).stdout); ``` ```rust Rust theme={null} use sail::ExecOptions; use std::time::Duration; let result = sb .exec_shell( "echo hi", ExecOptions { timeout: Some(Duration::from_secs(5)), ..Default::default() }, ) .await? .wait() .await?; print!("{} {}", result.stdout, result.exit_code); // Piping stdin: let wc = sb .exec( ["wc", "-l"], ExecOptions { open_stdin: true, ..Default::default() }, ) .await?; wc.write_stdin(b"one\ntwo\n").await?; wc.close_stdin().await?; print!("{}", wc.wait().await?.stdout); ``` Multiple execs can run on the same Sailbox concurrently; coordinate access to shared files and ports in your own commands. Running a command on a sleeping Sailbox wakes it. A paused Sailbox rejects commands until you call `resume`. *** ## run ```python Python theme={null} def run( command: str | Sequence[str], *, timeout: int | None = None, cwd: str | None = None, env: Mapping[str, str] | None = None, user: str | int | None = None, check: bool = False, idempotency_key: str | None = None, output_buffer_bytes: int = 1048576, ) -> ExecResult ``` ```typescript TypeScript theme={null} run(command: string | readonly string[], options?: { timeoutSeconds?: number; cwd?: string; env?: Record; user?: string; check?: boolean; idempotencyKey?: string; outputBufferBytes?: number; signal?: AbortSignal; }): Promise ``` ```rust Rust theme={null} pub async fn run(&self, argv: impl IntoIterator>, options: RunOptions) -> Result pub async fn run_shell(&self, command: &str, options: RunOptions) -> Result ``` Runs a command to completion and returns its buffered result: a one-shot convenience over [`exec`](#exec) followed by `wait()`. A string command runs via `/bin/sh -lc`; a list is exec'd directly. `cwd` sets the working directory for string commands only (like `exec`, an argv command with `cwd` raises). `user` picks the user the command runs as (see [`exec`](#exec)). By default a nonzero exit code returns normally on the result; check `exit_code`. With `check` set, a nonzero exit or a timeout raises `CommandFailedError` carrying the completed result instead. (Rust reports everything through the returned `ExecResult`.) A command that exceeds `timeout` is killed and reports `timed_out` on the result; without `check`, a timeout alone does not raise. `idempotency_key` makes a retried `run` wait on the original command instead of launching it again, so a control-loop retry cannot double-execute. While the first call is still running, a second call with the same key takes over its output stream, and the earlier call's result may come back truncated. In TypeScript, aborting `signal` force-cancels the remote command and rejects. The result's `stdout` and `stderr` hold only the most recent `output_buffer_bytes` (`outputBufferBytes` in TypeScript) of each stream (1 MiB by default, up to 64 MiB), with `stdout_truncated` and `stderr_truncated` set when older output was dropped; the command never pauses for unread output. To get every byte, use [`exec`](#exec) and read the stream (see [Exec process](#sailboxexecprocess)). The interactive and detached `exec` options (`open_stdin`, `pty`, `background`) and the `pipe` output mode are not available on `run`. ```python Python theme={null} result = sb.run("echo hello") print(result.exit_code, result.stdout) ``` ```typescript TypeScript theme={null} const result = await sb.run("echo hello"); console.log(result.exitCode, result.stdout); ``` ```rust Rust theme={null} use sail::RunOptions; let result = sb.run_shell("echo hello", RunOptions::default()).await?; println!("{} {}", result.exit_code, result.stdout); ``` *** ## shell ```python Python theme={null} def shell( command: str | None = None, *, shell: str | None = None, term: str | None = None, cwd: str | None = None, user: str | int | None = None, timeout: int | None = None, env: Mapping[str, str] | None = None, no_forward: bool = False, ) -> int ``` ```typescript TypeScript theme={null} shell(command?: string, options?: { shell?: string; term?: string; cwd?: string; user?: string; timeoutSeconds?: number; env?: Record; noForward?: boolean; }): Promise ``` ```rust Rust theme={null} pub async fn shell( &self, command: Option<&str>, options: ShellOptions, ) -> Result ``` Opens an interactive pty session on the Sailbox and bridges it to your local terminal. With no `command`, runs a login shell; pass `command` to run that under a pty instead (e.g. a REPL or `vim`). Keystrokes (including Ctrl-C, Ctrl-Z, and Ctrl-D) reach the remote process, its output renders locally, and terminal resizes propagate. Blocks until the remote process exits and returns its exit code. Requires an interactive local terminal (stdin and stdout must be TTYs) on a Unix machine, so it suits CLIs and dev tools rather than server-side harnesses. This is the equivalent of `ssh`-ing into the Sailbox, without running an SSH server. `shell` overrides the login shell (default `$SHELL`, else `/bin/bash`); it is ignored when `command` is given. `env` adds environment variables to the session, with the same precedence and reserved names as for [`exec`](#exec). The session runs as the image's `USER` when the image sets one, root otherwise: the same identity [`exec`](#exec) uses. `user` runs it as someone else instead (a user name or numeric uid, optionally with a group after a colon, like `"alice"`, `1000`, `"alice:staff"`); `user="0:0"` is always root. A `user` other than `"0:0"` requires a Sailbox whose guest honors requested users; on older Sailboxes the session fails until [`upgrade`](#upgrade) is called. While the session is open, browser opens and localhost servers in the Sailbox are forwarded to your machine. When a program in the Sailbox opens a browser (a login like `claude login` or `gh auth login`), the page opens in your local browser, and a login that redirects to a `localhost` callback completes end to end. A server the Sailbox starts on `localhost` keeps serving inside the Sailbox the whole time (code and agents there reach it as usual); while the shell is open it is also mirrored to the same port on your machine, unless that port is already in use locally. The mirror lasts only for the session. Files dragged onto the terminal upload into the Sailbox and paste as their guest paths, and Ctrl+V forwards your clipboard. On devbox images the clipboard is two-way: a pasted image or text lands on the Sailbox's clipboard, and text copied inside the Sailbox comes back to yours. Other images upload a pasted image as a file and paste its path. Pass `no_forward=True` (TS `noForward`) to turn all of it off, for example for an untrusted or automated session. Plain `exec` forwards nothing; for the same forwarding on `sail box exec --tty`, see the [CLI reference](/reference/cli#sail-box-shell). ```python Python theme={null} sb.shell() ``` ```typescript TypeScript theme={null} await sb.shell(); ``` ```rust Rust theme={null} use sail::shell::ShellOptions; sb.shell(/* command */ None, ShellOptions::default()).await?; ``` From the CLI: ```bash theme={null} sail box shell ``` *** ## The fs namespace File and directory operations live under the `fs` namespace: `sb.fs` in Python and TypeScript, `sb.fs()` in Rust. Reads and writes stream bytes to and from the guest, and Python paths accept `str` or `PurePosixPath`. The directory helpers `mkdir` (creates missing parents), `remove` (deletes recursively), and `exists` behave like `mkdir -p`, `rm -rf`, and `test -e`, and `upload_dir` and `download_dir` transfer whole directories. Writes give what they create to the image's `USER` by default, or to root when the image sets none. That is the same identity [`exec`](#exec) runs commands as, so an uploaded file is usable by the code in the Sailbox. Reads and the directory helpers act as root by default, so they work on any path. Each operation except the reads and the directory download takes an optional `user` in the Docker `USER` syntax: a user name or numeric uid, optionally with a group after a colon (`"alice"`, `1000`, `"alice:staff"`). The other directory helpers then run as that user, with its permissions enforced. The writes give what they create that owner (like `COPY --chown`) while the write itself always runs as root, so it works even where the owner cannot write. `"0:0"` is always root, and the directory upload gives what it creates the same way. Reads and the download take no `user`: a `user` only decides which paths an operation may touch and who owns what it creates, a read creates nothing in the Sailbox, and a download reads any path as root the way the reads do. A `user` other than `"0:0"` requires a Sailbox whose guest honors requested users; on older Sailboxes the operation fails until [`upgrade`](#upgrade) is called. *** ## fs.read ```python Python theme={null} def read(path: str | PurePosixPath) -> bytes ``` ```typescript TypeScript theme={null} read(path: string): Promise ``` ```rust Rust theme={null} pub async fn read(&self, path: &str) -> Result, SailError> ``` Reads a regular file from the Sailbox as bytes. Loads the whole file into memory; for very large files (checkpoints, datasets) prefer [`read_stream`](#read-stream). Raises a file-not-found error if the path does not exist. ```python Python theme={null} data = sb.fs.read("/workspace/output.txt") print(data.decode()) ``` ```typescript TypeScript theme={null} const data = await sb.fs.read("/workspace/output.txt"); console.log(data.toString()); ``` ```rust Rust theme={null} let data = sb.fs().read("/workspace/output.txt").await?; println!("{}", String::from_utf8_lossy(&data)); ``` *** ## fs.read\_stream ```python Python theme={null} def read_stream(path: str | PurePosixPath) -> FileStream # iterable, sync and async ``` ```typescript TypeScript theme={null} readStream(path: string): Promise // async-iterable ``` ```rust Rust theme={null} pub async fn read_stream(&self, path: &str) -> Result ``` Yields a regular file's contents in chunks without buffering the whole file in memory. Iterate to completion (or close the stream) so it is released. The Python iterator supports both `for` and `async for`. ```python Python theme={null} with open("local.bin", "wb") as f: for chunk in sb.fs.read_stream("/workspace/large.bin"): f.write(chunk) ``` ```typescript TypeScript theme={null} import { createWriteStream } from "node:fs"; const out = createWriteStream("local.bin"); for await (const chunk of await sb.fs.readStream("/workspace/large.bin")) { out.write(chunk); } out.end(); ``` ```rust Rust theme={null} use std::io::Write; let mut out = std::fs::File::create("local.bin")?; let reader = sb.fs().read_stream("/workspace/large.bin").await?; while let Some(chunk) = reader.next().await { out.write_all(&chunk?)?; } ``` *** ## fs.write ```python Python theme={null} def write( path: str | PurePosixPath, data: str | bytes | bytearray | memoryview | IOBase, *, create_parents: bool = True, mode: int = 0o644, user: str | int | None = None, ) -> None ``` ```typescript TypeScript theme={null} write(path: string, data: Buffer | Uint8Array | string, options?: { createParents?: boolean; mode?: number; user?: string; }): Promise ``` ```rust Rust theme={null} pub async fn write( &self, path: &str, data: &[u8], options: WriteOptions, ) -> Result<(), SailError> ``` Writes data to a regular file in the Sailbox. Missing parent directories are created by default in every language. `path` must be absolute. To write several files in one call, see [`fs.write_files`](#write-files). | Parameter | Default | Description | | ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `path` | required | Absolute destination path. | | `data` | required | Bytes or a string. Python also accepts a file-like object, streamed from the source. | | `create_parents` | `True` | Create missing parent directories. | | `mode` | `0o644` | POSIX permission bits (0–`0o777`). | | `user` | `None` | Owner for the written file and any directories the write creates (`"alice"`, `1000`, `"alice:staff"`). The write itself always runs as root, like `COPY --chown`. Omitted follows the image's `USER`, else root; `"0:0"` forces root. Requires a guest that honors requested users; see [the fs namespace](#fs). | ```python Python theme={null} sb.fs.write("/workspace/input.txt", "hello\n") ``` ```typescript TypeScript theme={null} await sb.fs.write("/workspace/input.txt", "hello\n"); ``` ```rust Rust theme={null} use sail::WriteOptions; sb.fs() .write("/workspace/input.txt", b"hello\n", WriteOptions::default()) .await?; ``` *** ## fs.write\_files ```python Python theme={null} def write_files( files: Mapping[str | PurePosixPath, str | bytes | bytearray | memoryview | IOBase], *, create_parents: bool = True, mode: int = 0o644, user: str | int | None = None, ) -> None ``` ```typescript TypeScript theme={null} writeFiles(files: Record, options?: { createParents?: boolean; mode?: number; user?: string; }): Promise ``` ```rust Rust theme={null} pub async fn write_files, D: AsRef<[u8]>>( &self, files: impl IntoIterator, options: WriteOptions, ) -> Result<(), SailError> ``` Writes several complete files in one call. `files` maps each absolute guest path to its contents; every file gets the same `create_parents`, `mode`, and `user` as [`fs.write`](#write). In Rust, `files` is any iterator of path and contents pairs, borrowed or owned: each entry is streamed from the buffer you pass, not copied first. How a batch runs, what happens on a failure, and when to stream instead are covered in [Filesystem](/sailboxes-filesystem#write-files). ```python Python theme={null} sb.fs.write_files({"/workspace/a.txt": "first\n", "/workspace/b.txt": b"second\n"}) ``` ```typescript TypeScript theme={null} await sb.fs.writeFiles({ "/workspace/a.txt": "first\n", "/workspace/b.txt": "second\n", }); ``` ```rust Rust theme={null} use sail::WriteOptions; sb.fs() .write_files( [("/workspace/a.txt", &b"first\n"[..]), ("/workspace/b.txt", &b"second\n"[..])], WriteOptions::default(), ) .await?; ``` *** ## fs.write\_stream ```python Python theme={null} def write_stream( path: str | PurePosixPath, *, create_parents: bool = True, mode: int = 0o644, user: str | int | None = None, ) -> FileWriter ``` ```typescript TypeScript theme={null} writeStream(path: string, options?: { createParents?: boolean; mode?: number; user?: string; }): Promise ``` ```rust Rust theme={null} pub async fn write_stream( &self, path: &str, options: WriteOptions, ) -> Result ``` Opens a streaming write and returns a writer: push chunks with `write` and confirm with `finish`. Only `finish` commits the write; a writer that goes away without finishing (an explicit `abort`, an exception, or dropping it) cancels the transfer instead, and the guest file state is then unspecified. Use it when your data arrives incrementally (streaming logs, assembling an archive on the fly) rather than from a source [`write`](#write) can consume whole. The options match [`write`](#write), including `user` for the written file's owner. Python makes the file mode explicit and defaults it to `0o644`. ```python Python theme={null} with sb.fs.write_stream("/logs/run.log") as writer: for line in ["step 1 ok\n", "step 2 ok\n"]: writer.write(line) # A clean exit finishes (commits); an exception aborts and propagates. ``` ```typescript TypeScript theme={null} const writer = await sb.fs.writeStream("/logs/run.log"); try { for (const line of ["step 1 ok\n", "step 2 ok\n"]) { await writer.write(line); } await writer.finish(); } catch (err) { await writer.abort(); throw err; } ``` ```rust Rust theme={null} use sail::WriteOptions; let mut writer = sb .fs() .write_stream("/logs/run.log", WriteOptions::default()) .await?; for line in ["step 1 ok\n", "step 2 ok\n"] { writer.write(line.as_bytes()).await?; } writer.finish().await?; // Dropping an unfinished writer aborts the transfer. ``` *** ## fs.mkdir / fs.remove / fs.exists ```python Python theme={null} def mkdir(path: str | PurePosixPath, *, user: str | int | None = None) -> None def remove(path: str | PurePosixPath, *, user: str | int | None = None) -> None def exists(path: str | PurePosixPath, *, user: str | int | None = None) -> bool ``` ```typescript TypeScript theme={null} mkdir(path: string, options?: { user?: string }): Promise remove(path: string, options?: { user?: string }): Promise exists(path: string, options?: { user?: string }): Promise ``` ```rust Rust theme={null} pub async fn mkdir(&self, path: &str, user: Option<&str>) -> Result<(), SailError> pub async fn remove(&self, path: &str, user: Option<&str>) -> Result<(), SailError> pub async fn exists(&self, path: &str, user: Option<&str>) -> Result ``` `mkdir` creates a directory and any missing parents (like `mkdir -p`) and is a no-op if it already exists. `remove` deletes a file or a whole directory tree (like `rm -rf`) and is a no-op if the path is already absent. `exists` reports whether a path exists; it follows symlinks (like `test -e`), so a dangling symlink reports false even though [`fs.ls`](#ls) lists it. A failure (for example a permission error) raises with the guest's stderr in the message. With `user`, the helper runs as that user with its permissions enforced: `mkdir` leaves the created directories owned by it, `remove` can only delete what that user may delete, and `exists` reports what that user can observe (a path it lacks permission to reach reports false). ```python Python theme={null} sb.fs.mkdir("/workspace/results") if not sb.fs.exists("/workspace/results/run.lock"): sb.fs.remove("/workspace/results/stale") ``` ```typescript TypeScript theme={null} await sb.fs.mkdir("/workspace/results"); if (!(await sb.fs.exists("/workspace/results/run.lock"))) { await sb.fs.remove("/workspace/results/stale"); } ``` ```rust Rust theme={null} sb.fs().mkdir("/workspace/results").await?; if !sb.fs().exists("/workspace/results/run.lock").await? { sb.fs().remove("/workspace/results/stale").await?; } ``` *** ## fs.ls ```python Python theme={null} def ls(path: str | PurePosixPath, *, user: str | int | None = None) -> list[DirEntry] ``` ```typescript TypeScript theme={null} ls(path: string, options?: { user?: string }): Promise ``` ```rust Rust theme={null} pub async fn ls(&self, path: &str, user: Option<&str>) -> Result, SailError> ``` Lists a directory's immediate entries (no recursion) as [`DirEntry`](#direntry) records. A missing path raises, as does a path that is not a directory and a listing too large for the exec output cap. An entry whose name is not valid UTF-8 fails the listing, since the path API cannot address it. With `user`, the listing runs as that user, so a directory it cannot read raises a permission error. ```python Python theme={null} for entry in sb.fs.ls("/workspace"): print(f"{entry.type:9} {entry.size:>8} {entry.name}") ``` ```typescript TypeScript theme={null} for (const entry of await sb.fs.ls("/workspace")) { console.log(entry.type, entry.size, entry.name); } ``` ```rust Rust theme={null} for entry in sb.fs().ls("/workspace").await? { println!("{:?} {} {}", entry.entry_type, entry.size, entry.name); } ``` *** ## fs.upload\_dir ```python Python theme={null} def upload_dir(local_dir: str | Path, guest_dir: str | PurePosixPath, *, user: str | int | None = None) -> None ``` ```typescript TypeScript theme={null} uploadDir(dirs: { localDir: string; guestDir: string; user?: string }): Promise ``` ```rust Rust theme={null} pub async fn upload_dir(&self, local_dir: &Path, guest_dir: &str, user: Option<&str>) -> Result<(), SailError> ``` Uploads a local directory's contents into a Sailbox directory. The local directory's entries land inside the Sailbox directory, which is created if needed. Entries the upload does not name are left in place; a same-named file is replaced. Uploaded files keep their permission bits (the setuid, setgid, and sticky bits are cleared) and belong to the image's `USER` (root when the image sets none or its identity cannot be read). A `user` (the same syntax the other operations take) gives the entries to that user instead, like `COPY --chown`; it must exist in the Sailbox, and like the other operations' `user` it requires a Sailbox whose guest honors requested users. The Sailbox's image must provide `tar` and `gzip`, which the transfer uses to ship the directory as one compressed archive; the default images do. ```python Python theme={null} sb.fs.upload_dir("./local_data", "/workspace/data") ``` ```typescript TypeScript theme={null} await sb.fs.uploadDir({ localDir: "./local_data", guestDir: "/workspace/data", }); ``` ```rust Rust theme={null} sb.fs().upload_dir(Path::new("./local_data"), "/workspace/data").await?; ``` *** ## fs.download\_dir ```python Python theme={null} def download_dir(guest_dir: str | PurePosixPath, local_dir: str | Path) -> None ``` ```typescript TypeScript theme={null} downloadDir(dirs: { guestDir: string; localDir: string }): Promise ``` ```rust Rust theme={null} pub async fn download_dir(&self, guest_dir: &str, local_dir: &Path) -> Result<(), SailError> ``` Downloads a Sailbox directory's contents to a local directory. The Sailbox directory's entries land inside the local directory, which is created if needed. Entries the download does not name are left in place; a same-named file is replaced. The transfer reads every file in the tree, so download directories of ordinary files: system trees like `/proc` or `/sys` hold files that cannot be read as plain data, and downloading them fails. A file that is being written while the download runs is captured as it is at that moment, the way copying a live file would; download after writers finish for a consistent copy. On Windows, a directory that contains symbolic links cannot be downloaded, since Windows restricts creating them. The Sailbox's image must provide `tar` and `gzip`, which the transfer uses to ship the directory as one compressed archive; the default images do. ```python Python theme={null} sb.fs.download_dir("/workspace/results", "./local_results") ``` ```typescript TypeScript theme={null} await sb.fs.downloadDir({ guestDir: "/workspace/results", localDir: "./local_results", }); ``` ```rust Rust theme={null} sb.fs().download_dir("/workspace/results", Path::new("./local_results")).await?; ``` *** ## listener / listeners ```python Python theme={null} def listener(guest_port: int) -> Listener def listeners() -> list[Listener] def wait_for_listener( guest_port: int, *, timeout: float = 60.0, ) -> Listener ``` ```typescript TypeScript theme={null} listener(guestPort: number): Promise listeners(): Promise waitForListener(guestPort: number, options?: { timeoutSeconds?: number; // 60 signal?: AbortSignal; }): Promise ``` ```rust Rust theme={null} pub async fn listener(&self, guest_port: u32) -> Result; pub async fn listeners(&self) -> Result, SailError>; pub async fn wait_for_listener( &self, guest_port: u32, options: WaitForListenerOptions, // timeout (60s) ) -> Result; ``` Look up the exposed guest ports and how to reach them. Waiting blocks until the route is active and the endpoint is reachable (an HTTP probe to the URL, or a TCP connectivity check to the host/port), then returns the ready listener; it raises a timeout error otherwise. ```python Python theme={null} listener = sb.wait_for_listener(3000, timeout=60) print(listener.endpoint.url) ``` ```typescript TypeScript theme={null} const listener = await sb.waitForListener(3000, { timeoutSeconds: 60 }); if (listener.endpoint?.kind === "http") { console.log(listener.endpoint.url); } ``` ```rust Rust theme={null} use sail::{ListenerEndpoint, WaitForListenerOptions}; let listener = sb .wait_for_listener(3000, WaitForListenerOptions::default()) .await?; if let Some(ListenerEndpoint::Http { url }) = listener.endpoint() { println!("{url}"); } ``` Ports are exposed at create time via `ingress_ports`, or at runtime with `expose`/`unexpose` (see [Networking](/sailboxes-networking)). *** ## enable\_ssh ```python Python theme={null} def enable_ssh( *, allowlist: list[str] | None = None, wait: bool = True, timeout: float = 60.0, ) -> TcpEndpoint | None ``` ```typescript TypeScript theme={null} enableSsh(options?: { allowlist?: string[]; wait?: boolean; // true timeoutSeconds?: number; // 60 }): Promise ``` ```rust Rust theme={null} pub async fn enable_ssh( &self, options: EnableSshOptions, // { allowlist, wait, timeout } ) -> Result, SailError> ``` Prepares this Sailbox for SSH (idempotent): installs your org's SSH certificate authority as trusted, starts `sshd`, and exposes guest port `22` as raw TCP once the CA-only server owns it. See the [SSH access guide](/sailboxes-networking#ssh-access) for the access model and how to connect. By default it blocks until SSH is reachable, up to `timeout` seconds, and returns the endpoint to dial; pass `wait=False` to skip the probe and return nothing. `allowlist` restricts which source addresses or ranges may connect to port `22`, replacing any existing restriction. Left empty, a first enable opens the port to any source, and a re-enable keeps the existing restriction. Disabling SSH removes the port-22 listener along with its restriction, so enabling again starts fresh. *** ## checkpoint ```python Python theme={null} def checkpoint( *, name: str | None = None, ttl_seconds: int | None = None, ) -> SailboxCheckpoint ``` ```typescript TypeScript theme={null} checkpoint(options?: { name?: string; ttlSeconds?: number; }): Promise ``` ```rust Rust theme={null} pub async fn checkpoint( &self, options: CheckpointOptions, // { name, ttl } ) -> Result ``` Creates a durable checkpoint handle for this Sailbox. Running Sailboxes are snapshotted first. Paused and sleeping Sailboxes return a handle to their existing checkpoint without waking. Upgrade a Sailbox that uses an older guest payload before you create a checkpoint handle. `name` sets a display name for the handle. `ttl_seconds` (`ttl` in Rust), when set, must be `> 0` and overrides the server's default retention window. Set it when you keep a checkpoint to reuse as a template, so the handle does not expire while you still need it. The returned handle carries `expires_at`, a timestamp (`datetime` in Python, `Date` in TypeScript, `OffsetDateTime` in Rust) for when the checkpoint expires: seven days out unless you asked for a different window. Starting a Sailbox from it after that fails. *** ## from\_checkpoint ```python Python theme={null} @classmethod def from_checkpoint( checkpoint_id: str, *, name: str, timeout: int | None = None, ) -> Sailbox ``` ```typescript TypeScript theme={null} static fromCheckpoint(options: { checkpointId: string; name: string; timeoutSeconds?: number; }): Promise ``` ```rust Rust theme={null} pub async fn create_from_checkpoint( &self, // Client checkpoint_id: &str, name: &str, timeout: Option, ) -> Result ``` Creates a new running Sailbox, called `name`, from a durable checkpoint handle returned by [`checkpoint`](#checkpoint). The new Sailbox gets a fresh network identity; existing TCP connections do not carry over, and ingress ports are not inherited. `timeout` (seconds, `> 0` when set) bounds the call, since a restore can block for many minutes while the new Sailbox queues for capacity. A call that times out fails, and the restore may still finish in the background; the new Sailbox then shows up in [`Sailbox.list`](#sailbox-list). Omit `timeout` to wait without a client-side bound. The new Sailbox restores the memory saved in the checkpoint as well as the writable disk, so processes the original was running carry on there. Commands you started with `exec` stop in the new Sailbox, though their writes up to the checkpoint are kept, and one you started with the background option keeps running there. Start the other commands you need again. Sometimes the new Sailbox comes up cold instead, with the disk intact and nothing running. Write code that expects a cold start. `checkpoint()` does not support a Sailbox that has volume mounts. Create a separate Sailbox without volume mounts before you create a checkpoint handle. *** ## upgrade ```python Python theme={null} def upgrade() -> UpgradeResult ``` ```typescript TypeScript theme={null} upgrade(): Promise ``` ```rust Rust theme={null} pub async fn upgrade(&self) -> Result ``` Upgrades this Sailbox's runtime to the latest version, picking up new Sailbox features, fixes, and performance improvements without recreating the Sailbox. A running Sailbox reboots in place on its current disk: all filesystem state is preserved, but processes restart as they would after a machine reboot (any application state not yet written to disk is lost, as after a sudden power loss). A paused or sleeping Sailbox is upgraded without waking; the upgrade is recorded and applied at the next wake. Returns an `UpgradeResult`: `applied` is true when the upgrade happened immediately (the Sailbox was running) and false when it will apply at the next wake, and `status` is the Sailbox's lifecycle status after the call. Already-up-to-date Sailboxes report immediate success without rebooting. *** ## pause / sleep / resume / terminate ```python Python theme={null} def pause() -> None def sleep(wake_at: Optional[datetime] = None) -> Optional[datetime] def resume() -> Sailbox def terminate() -> None ``` ```typescript TypeScript theme={null} pause(): Promise sleep(wakeAt?: Date): Promise resume(): Promise terminate(): Promise ``` ```rust Rust theme={null} pub async fn pause(&self) -> Result<(), SailError>; pub async fn sleep(&self, wake_at: Option) -> Result, SailError>; pub async fn resume(&self) -> Result<(), SailError>; pub async fn terminate(&self) -> Result<(), SailError>; ``` * **`pause`** checkpoints and pauses the Sailbox in memory until you explicitly resume it. Commands and network traffic do not wake a paused Sailbox. * **`sleep`** checkpoints the Sailbox to disk; inbound traffic, an operation, or an explicit `resume` wakes it. An optional wake time schedules a wall-clock wake (see [Lifecycle](/sailboxes-lifecycle)). * **`resume`** wakes a paused or sleeping Sailbox. Raises a not-found error if the Sailbox is terminated. * **`terminate`** permanently ends the Sailbox. Idempotent: terminating an already-terminated Sailbox succeeds. Sail may also sleep a Sailbox on its own, but only when nothing would notice: no CPU or network activity, no process waiting on a timer, and no open connections a sleep would break. A slept Sailbox wakes transparently on traffic or the next operation. See [Lifecycle](/sailboxes-lifecycle) for how these interact with checkpoints, and [Pricing](/sailboxes-pricing) for billing. *** ## Volumes A `Volume` is an org-scoped shared filesystem (NFS) that can be mounted into one or more Sailboxes. Resolve one by name, then pass it (or its id) in the `volumes` mapping of [`Sailbox.create`](#sailbox-create), keyed by the absolute guest path to mount it at: ```python Python theme={null} import sail vol = sail.Volume.find("shared-cache", mint_if_missing=True) sb = sail.Sailbox.create( app=app, name="worker-1", volumes={"/mnt/cache": vol}, ) ``` ```typescript TypeScript theme={null} import { Sailbox, Volume } from "@sailresearch/sdk"; const vol = await Volume.find("shared-cache", { mintIfMissing: true }); const sb = await Sailbox.create({ app, name: "worker-1", volumes: { "/mnt/cache": vol }, }); ``` ```rust Rust theme={null} use sail::{CreateSailboxRequest, VolumeMount}; let vol = client.get_volume("shared-cache", /* mint_if_missing */ true).await?; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "worker-1".into(), volume_mounts: vec![VolumeMount { volume_id: vol.volume_id, mount_path: "/mnt/cache".into(), }], ..Default::default() }, /* timeout */ None, ) .await?; ``` The management surface: ```python Python theme={null} @staticmethod def find(name: str, *, mint_if_missing: bool = False) -> Volume @staticmethod def list(*, max_objects: int | None = None) -> list[Volume] def delete(*, allow_missing: bool = False) -> Volume | None @staticmethod def delete_by_name(name: str, *, allow_missing: bool = False) -> Volume | None ``` ```typescript TypeScript theme={null} static find( name: string, options?: ClientOptions & { mintIfMissing?: boolean } ): Promise static list(options?: ClientOptions & { maxObjects?: number }): Promise delete(options?: { allowMissing?: boolean }): Promise ``` ```rust Rust theme={null} pub async fn get_volume(&self, name: &str, mint_if_missing: bool) -> Result pub async fn list_volumes(&self, max_objects: Option) -> Result, SailError> pub async fn delete_volume(&self, volume_id: &str, allow_missing: bool) -> Result, SailError> ``` * **`find`** looks up a volume by name; `mint_if_missing` creates it when no volume with that name exists. * **`list`** returns the org's active volumes, newest first; `max_objects` caps the count. * **`delete`** deletes the volume. With `allow_missing`, deleting an already-deleted volume succeeds instead of raising a not-found error. Each handle carries `volume_id`, `name`, `backend`, `status`, and `created_at` / `updated_at` timestamps. *** ## HTTP policies An [HTTP policy](/sailboxes-credentials#policies) shapes the HTTPS requests a Sailbox sends; [credential injection](/sailboxes-credentials) is its main use. Policies belong to your organization, and each Sailbox has at most one attached at a time: ```python Python theme={null} def set_http_policy(policy: HttpPolicy | HttpPolicySummary | str) -> None def http_policy() -> HttpPolicy | None def clear_http_policy() -> None ``` ```typescript TypeScript theme={null} setHttpPolicy(policy: HttpPolicy | HttpPolicySummary | string): Promise httpPolicy(): Promise clearHttpPolicy(): Promise ``` ```rust Rust theme={null} pub async fn set_http_policy(&self, policy: &HttpPolicy) -> Result<(), SailError>; pub async fn http_policy(&self) -> Result, SailError>; pub async fn clear_http_policy(&self) -> Result<(), SailError>; ``` * **`set_http_policy`** attaches a policy, replacing any policy already attached. In Python and TypeScript, pass a policy handle, a summary row from a listing, or a policy id string. * **`http_policy`** returns the attached policy, or `None` / `null` when no policy is attached. * **`clear_http_policy`** detaches the policy. It also succeeds when no policy is attached. A policy change applies to HTTPS connections the Sailbox opens after the call; connections already open keep the previous policy until they close. Create policies and store the secrets they reference with the org-scoped `HttpPolicy` and `Secret` surfaces (`Client` methods in Rust). The [credential injection guide](/sailboxes-credentials) walks through the full flow in every language. *** ## Supporting types Field names below use the Python spelling; TypeScript exposes the same fields in camelCase. ### DirEntry One entry in a directory listing from [`fs.ls`](#ls). Reported for the entry itself, so a symlink's `type` is `"symlink"` regardless of what it points at. | Field | Description | | --------------- | ------------------------------------------------------------------------------- | | `name` | The entry's base name, with no directory prefix. | | `type` | `"file"`, `"directory"`, `"symlink"`, or `"other"` (device, FIFO, socket, ...). | | `size` | Size in bytes as reported by the guest. | | `modified_time` | Last-modified time as a Unix timestamp in seconds, with a fractional part. | | `mode` | Unix permission bits, e.g. `0o644`. The file-type bits are not included. | ### Network policy The `network_policy` create argument (TypeScript `networkPolicy`) is one of: * omit it (or `None`) for public networking, the default. * `NetworkPolicy.NO_NETWORK` (TypeScript `"no_network"`, Rust `NetworkPolicy::NoNetwork`) for no network access at all. It cannot be combined with `ingress_ports`. * a `NetworkAllowlist(hosts)` (TypeScript `{ mode: "allowlist", allowedHosts }`, Rust `NetworkPolicy::Allowlist(hosts)`) to allow only the listed destinations: hostnames, `*.` wildcard hostnames, IPv4 addresses, and IPv4 ranges in CIDR form, at least one and at most 128. Only connections the Sailbox opens are limited, so it combines with `ingress_ports` and SSH. A list that breaks the [entry rules](/sailboxes-network-policy#entries) fails the call with `InvalidArgumentError` before a Sailbox is created. The policy is fixed for the Sailbox's life, and `from_checkpoint` keeps the policy of the source. `get` and `list` read it back as a `NetworkPolicyInfo`: `mode` and, for an allowlist, the stored `allowed_hosts`. It is absent when the Sailbox is public. The [network policy guide](/sailboxes-network-policy) covers what each entry allows. ### IngressPort A guest port to expose for ingress. In Python, `Sailbox.create` also accepts a bare `int` as shorthand for `IngressPort(port)` (an HTTP port). | Field | Default | Description | | ------------ | -------- | ------------------------------------------------------------------------------------- | | `guest_port` | required | Guest port to expose (1–65535). | | `protocol` | `"http"` | `"http"` for a stable HTTPS URL, or `"tcp"` for a byte-transparent raw-TCP host/port. | | `allowlist` | `None` | Addresses, ranges, or Sail app names allowed to connect. Empty means public. | Address and range entries work for HTTP and TCP listeners. App-name entries match authenticated traffic from another Sailbox and work on HTTP listeners only, because raw-TCP connections carry no source app identity (so a `"tcp"` allowlist must contain only addresses and ranges). An entry that reads as an address or a range is taken as one, so an app name cannot read as either, and cannot contain a `/`. An address must not carry an IPv6 zone, such as `fe80::1%eth0`, which names an interface on one machine rather than a source. To send that authenticated traffic, pass `headers=sail.ingress_auth_headers()` on the request when calling from inside a Sailbox. From the host, fetch a specific Sailbox's headers with `sb.ingress_auth_headers()` (this needs an organization-scoped API key). Reserved ports: guest port `22` cannot be an HTTP port (expose it as `tcp` for SSH) and `10000`/`10001`/`15001`/`15002` are reserved for Sail's in-guest services. Leaving `allowlist` empty normally makes the port publicly reachable, with one exception: for well-known database, cache, and search ports (e.g. `5432`, `6379`), a raw-TCP expose with no allowlist is rejected, so you can't accidentally publish an unprotected Postgres or Redis to the whole internet. To make one of these ports public on purpose, say so explicitly with `allowlist=["0.0.0.0/0", "::/0"]`. ```python Python theme={null} sb = sail.Sailbox.create( app=app, name="db-box", ingress_ports=[80, 443, sail.IngressPort(5432, "tcp", allowlist=["203.0.113.0/24"])], ) ``` ```typescript TypeScript theme={null} const sb = await Sailbox.create({ app, name: "db-box", ingressPorts: [ { guestPort: 80, protocol: "http" }, { guestPort: 443, protocol: "http" }, { guestPort: 5432, protocol: "tcp", allowlist: ["203.0.113.0/24"] }, ], }); ``` ```rust Rust theme={null} use sail::{IngressPort, IngressProtocol}; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "db-box".into(), ingress_ports: vec![ IngressPort { guest_port: 80, protocol: IngressProtocol::Http, allowlist: Vec::new(), }, IngressPort { guest_port: 5432, protocol: IngressProtocol::Tcp, allowlist: vec!["203.0.113.0/24".to_string()], }, ], ..Default::default() }, /* timeout */ None, ) .await?; ``` ### Exec process A handle to a command running in the Sailbox (`ExecProcess` in Python, `ExecProcess` in TypeScript and Rust). The command runs inside the Sailbox, and a temporary network interruption does not kill it. Sail may reattach after an interruption, but reattachment does not guarantee exact output replay. Closing the handle abandons the live attachment without killing the command. **Live output.** `stdout` and `stderr` are iterators (`for` in Python, `for await` in TypeScript, `next().await` in Rust). The Python and TypeScript iterators yield text, incrementally decoded from the raw stream (a multibyte character split across chunks arrives whole); the raw byte stream is available as `stdout_bytes` / `stderr_bytes` in Python, `.raw()` on the stream in TypeScript, and is what the Rust reader yields directly. Bytes travel exactly as the command wrote them (escape sequences and binary payloads included). **Output limits.** Each stream has a buffer, 1 MiB by default. `output_buffer_bytes` (`outputBufferBytes` in TypeScript) sets its size, from 64 KiB to 64 MiB. The `output_mode` option (`outputMode` in TypeScript) sets what happens when a buffer fills. This applies to commands started without a `pty`; a `pty` command always behaves like `tail`. * `auto`, the default. If you are reading a stream and fall behind, the command pauses when the buffer fills and resumes as you read, like a pipe. If you are not reading a stream, the command never pauses and the stream keeps only its most recent bytes. Reading a stream is how you get every byte, and it slows the command when you cannot keep up. * `pipe`. The command pauses when either buffer fills and stays paused until you read that stream, so nothing is lost while you are late to start reading. Read both streams, or the command stays paused on the one you ignore; `2>&1` or `2>/dev/null` in a shell command is the easy way out. Once you release a reader, its stream goes back to keeping only its most recent bytes. `wait()` without a reader waits for as long as the command stays paused. Not available with a `pty`. * `tail`. The command never pauses for you. Each stream keeps only its most recent bytes, even while you are reading it, so a slow reader skips output without notice (a Rust `StreamReader` can check `took_drop`). `stdout_truncated` and `stderr_truncated` say only that the result holds less than the command wrote, which is also the case after a reader consumed everything. Stdout and stderr are handled separately. Sending `cancel()`, and the exec `timeout`, end every pause: from then on each stream keeps only its most recent bytes, so a reader more than a buffer behind skips ahead. A command that ignores the cancel signal keeps running that way; cancel with `force` to stop it. The command keeps its original timeout. If a handle attaches to a command launched earlier under the same `idempotency_key`, that handle's pause deadline starts when the attachment succeeds, so it can release the pauses one full timeout after that; `cancel()` and `close()` release them at once. A command that never pauses for you still runs no faster than your connection carries its output; when output outruns the connection, the command pauses on the Sailbox until the backlog drains. With `auto`, start reading right after `exec()` returns to get every byte (what starts a read differs by language; see below). You can read stdout without holding stderr, or the reverse. The stream you are not holding keeps its most recent bytes and never pauses the command when it fills, and a reader that starts on it late begins with whatever is still held; with `pipe`, that is everything. If you hold both readers, read them at the same time, each from its own thread or task (`asyncio.gather` in Python, `Promise.all` in TypeScript, two tasks in Rust). The exit code is available from `poll()` (Python also has `exit_code`) as soon as the streams end. **Claiming and releasing a stream.** Each stream can be read once; a second attempt fails (`InvalidArgumentError` in Python and TypeScript, `SailError::InvalidArgument` in Rust), before or after the first reader is released. After release, Sail again keeps only the stream's most recent bytes, in every mode. * **Python.** Accessing `proc.stdout` or `proc.stdout_bytes` (and the `stderr` twins) claims the stream and returns a generator; from that access on the command pauses rather than lose output (see **Output limits** for the exceptions). The stream is released when the generator ends, when you call `close()` on it (`await` its `aclose()` for an async generator), or when nothing references it any more (a `for` loop over `proc.stdout` drops it when the loop ends, including by `break`). To stop early on purpose, keep the generator in a variable and call `close()`. * **TypeScript.** Accessing `proc.stdout` or `proc.stderr` claims nothing. The stream is claimed when iteration starts (`for await`, `.raw()`, `.text()`, `.bytes()`) or when you call `.toReadable()`. It is released when the iteration finishes or you leave it (`break`, `return`, or a thrown error inside `for await`), when `.text()` or `.bytes()` reaches the end, or when the `Readable` is destroyed (released at once, even while a read is waiting for output). * **Rust.** `reader(OutputStream::Stdout)` or `reader_async(...)` claims the stream. It is held while the reader value exists and released when the reader is dropped. In every language, `close()` on the handle, or your process exiting, releases both streams. The command keeps running and never pauses. `wait()` fails after `close()` unless it already resolved a result. If the connection to the Sailbox is interrupted, the command keeps running and does not pause while Sail reconnects; output produced in the meantime can be missing, and `stdout_truncated` / `stderr_truncated` report when that happened. Reattachment is best-effort, not an exact replay. A `pty` command never pauses: Sail keeps only its most recent output, up to the buffer size, so a reader further behind than that misses older output. Use `resync()` to ask the command to repaint its current screen. **stdin.** With `open_stdin=True`, write to the command's stdin and deliver EOF by closing it (Python `proc.stdin.write`/`close`, TypeScript `writeStdin`/`closeStdin`, Rust `write_stdin`/`close_stdin`). Writes block (like a pipe write) while the command is not reading. Writing to a completed command, or after the command closed its stdin, raises a broken-pipe error. **`wait()`** waits for the command to finish and returns its [result](#sailboxexecresult), holding each stream's buffer, its most recent output (see **Output limits** above). It never pauses the command itself and can be called while a reader is still open; with `pipe`, it waits for as long as an unread stream keeps the command paused. After `close()` it fails, unless a result was already resolved; a repeat call returns that result. For foreground execs it waits for the command to finish; for background execs it waits only for the detached launcher. An exec that ended without a real exit code (the machine hosting the Sailbox was lost before the command finished) raises a host-lost error instead of returning a result. In Python and the `sail` CLI, `Ctrl-C` during a wait additionally sends `SIGINT` to the remote command and resumes waiting, and a second `Ctrl-C` escalates to `SIGKILL`; terminal-facing surfaces forward the interrupt like a local foreground job. The TypeScript and Rust libraries leave process signal handling to your application; wire the same behavior with `cancel` if you want it. **`poll()`** (Rust: `try_wait`) returns the exit code once the output stream has ended, else nothing. It never blocks and never drops output, so it is the way to get the exit code after reading the streams yourself. If the connection was lost for good mid-command, the stream ends early with the outcome still unknown: `poll()` stays empty and `wait()` fetches the result Sail recorded. **`cancel()`** signals the guest command: `SIGINT` by default, `SIGKILL` with `force`. Idempotent on the server. If the Sailbox is sleeping, cancel wakes it to deliver the signal; if you paused the Sailbox, resume it first (cancel raises rather than waiting, since the guest cannot receive the signal while paused). **`close()`** abandons the handle without killing the command. It releases both streams. The command keeps running and never pauses, and Sail keeps only the most recent output of each stream. Call `cancel()` instead if the command should stop. `wait()` fails after `close()` unless it already resolved a result. **`resize(cols, rows)`** adjusts a `pty` command's window. **`resync()`** asks a `pty` command to repaint its current screen on the output stream. A command runs at full speed and never waits for a slow reader, so if you render its output yourself and fall far behind, the oldest output is dropped and the screen can end up garbled. Call `resync()` to receive the current screen instead of a broken, partial one. It does nothing for a command with no `pty`. The interactive `shell` helper calls it for you. ### Exec result The output of a completed exec (`ExecResult` in Python, `ExecResult` in TypeScript and Rust). | Field | Type | Description | | ------------------ | ------ | --------------------------------------------------------------------------------- | | `stdout` | `str` | The most recent standard output, up to the exec's buffer size (1 MiB by default). | | `stderr` | `str` | The most recent standard error, up to the exec's buffer size (1 MiB by default). | | `exit_code` | `int` | Process exit code. | | `timed_out` | `bool` | The command hit its `timeout` budget and was killed. | | `stdout_truncated` | `bool` | The command wrote more stdout than `stdout` holds, so older bytes are missing. | | `stderr_truncated` | `bool` | The command wrote more stderr than `stderr` holds, so older bytes are missing. | ### Listener An exposed guest port and how to reach it. Every listener carries its guest port, `protocol`, route status, and a typed `endpoint`: an [`HttpEndpoint`](#httpendpoint) (with `url`) or a [`TcpEndpoint`](#tcpendpoint) (with `host`/`port`), absent until routable. In TypeScript the endpoint is a union discriminated on `kind`; in Rust it is the `ListenerEndpoint` enum returned by `listener.endpoint()`. Listeners are snapshots; re-fetch with `sb.listener(guest_port)`. ### HttpEndpoint The routable HTTPS address of an `"http"` listener. | Field | Type | Description | | ----- | ----- | ----------------------------------------- | | `url` | `str` | The HTTPS URL to reach the guest service. | ### TcpEndpoint The host and port to connect to for a `"tcp"` listener. | Field | Type | Description | | ------ | ----- | ----------------- | | `host` | `str` | Hostname to dial. | | `port` | `int` | Port to dial. | ### Sailbox snapshot fields The monitoring snapshot carried by every `Sailbox` returned from [`Sailbox.get`](#sailbox-get) and [`Sailbox.list`](#sailbox-list). | Field | Type | Description | | --------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sailbox_id` | `str` | Sailbox id. | | `app_id` / `app_name` | `str` | Owning app. | | `image_id` | `str` | Image the Sailbox runs. | | `name` | `str` | Sailbox name. | | `status` | `str` | Lifecycle status. | | `memory_mib` / `vcpu_count` / `state_disk_size_gib` | `int` | Configured maximums. | | `cpu_requested_vcpu` | `int` | Configured vCPU maximum. | | `cpu_used_vcpu` | `float` | Latest observed vCPU usage. | | `memory_requested_bytes` / `memory_used_bytes` | `int` | Configured max vs observed memory. | | `disk_requested_bytes` / `disk_used_bytes` | `int` | Configured max vs observed disk. | | `architecture` | `str` | CPU architecture. | | `guest_schema_version` | `int \| None` | Version of the managed runtime the Sailbox last booted with. | | `deprecation` | `SailboxDeprecation \| None` | Set when this Sailbox's managed runtime should be upgraded and the caller can act on it: a `deadline` date and a `message` with upgrade instructions. `None` otherwise. | | `error_message` | `str \| None` | Failure detail, when applicable. | | `checkpoint_generation` | `int` | Monotonic checkpoint counter. | | `started_at` / `last_checkpointed_at` | `datetime \| None` | Timestamps (`Date` in TypeScript, `OffsetDateTime` in Rust). | | `created_at` / `updated_at` | `datetime` | Timestamps (`Date` in TypeScript, `OffsetDateTime` in Rust). | | `created_by_user_id` | `str \| None` | The user whose credential created the Sailbox; `None` for service-key creates. | | `visibility` | `str \| None` | `"private"` for creator-restricted Sailboxes; `None`/`"org"` otherwise. | | `network_policy` | `NetworkPolicyInfo \| None` | The Sailbox's network policy: `None` means public; otherwise `mode` (`"no_network"` or `"allowlist"`) and, for an allowlist, the `allowed_hosts` in effect. Read-only. | Observed-usage fields reflect the latest live sample within roughly the last two minutes, falling back to zero when no recent sample is available. ### SailboxPage One page of [`Sailbox.list_page`](#sailbox-list-page) results. | Field | Type | Description | | ---------- | --------------- | ---------------------------------------------- | | `items` | `list[Sailbox]` | The Sailboxes on this page. | | `limit` | `int` | Page size used. | | `offset` | `int` | Page offset used. | | `total` | `int` | Total matching Sailboxes. | | `has_more` | `bool` | Whether more Sailboxes exist beyond this page. | # Apps Source: https://docs.sailresearch.com/sailbox-sdk-apps The org-owned application a Sailbox belongs to An `App` is the application that owns your Sailboxes. Every [`Sailbox.create`](/sailbox-sdk#sailbox-create) call needs an app, usually resolved by name with `App.find()`. ```python Python theme={null} import sail app = sail.App.find(name="example-app", mint_if_missing=True) print(app.id, app.name) ``` ```typescript TypeScript theme={null} import { App } from "@sailresearch/sdk"; const app = await App.find("example-app", { mintIfMissing: true }); console.log(app.id, app.name); ``` ```rust Rust theme={null} use sail::Client; let client = Client::from_env()?; let app = client.find_app("example-app", /* mint_if_missing */ true).await?; println!("{} {}", app.id, app.name); ``` ## App.find ```python Python theme={null} @classmethod def find(name: str, *, mint_if_missing: bool = False) -> App ``` ```typescript TypeScript theme={null} static find( name: string, options?: ClientOptions & { mintIfMissing?: boolean } ): Promise ``` ```rust Rust theme={null} pub async fn find_app(&self, name: &str, mint_if_missing: bool) -> Result ``` Finds an app by name, optionally creating it if it does not exist. | Parameter | Default | Description | | ----------------- | -------- | ----------------------------------------------- | | `name` | required | The app name to look up. | | `mint_if_missing` | `False` | Create the app if no app with that name exists. | **Returns** the `App`. **Raises** a not-found error if the app does not exist and `mint_if_missing` is `False`, and a permission error on auth failures. ## App.list ```python Python theme={null} @classmethod def list() -> list[App] ``` ```typescript TypeScript theme={null} static list(options?: ClientOptions): Promise ``` ```rust Rust theme={null} pub async fn list_apps(&self) -> Result, SailError> ``` Returns every app the current org owns, newest first. Apps with no Sailboxes yet are included. The response is not paginated. ## Attributes | Attribute | Type | Description | | ------------ | ---------- | -------------------------------------------------------------------- | | `id` | `str` | Stable app identifier. | | `name` | `str` | App name. | | `created_at` | `datetime` | Creation timestamp (`Date` in TypeScript, `OffsetDateTime` in Rust). | # Errors Source: https://docs.sailresearch.com/sailbox-sdk-errors Sailbox and image error taxonomy Every SDK failure carries the same taxonomy: catch the base `SailError` for everything, or match a specific failure. ```python Python theme={null} import sail try: sb = sail.Sailbox.create(app=app, name="box") result = sb.exec("false", timeout=5).wait() except sail.SailboxCreationError: ... # creation failed except sail.SailboxError: ... # any other Sailbox failure ``` ```typescript TypeScript theme={null} import { Sailbox, SailboxCreationError, SailError } from "@sailresearch/sdk"; try { const sb = await Sailbox.create({ app, name: "box" }); await (await sb.exec("false", { timeoutSeconds: 5 })).wait(); } catch (err) { if (err instanceof SailboxCreationError) { // creation failed } else if (err instanceof SailError) { // any other SDK failure } } ``` ```rust Rust theme={null} use sail::{CreateSailboxRequest, SailError}; match client .create_sailbox( &CreateSailboxRequest { app_id: app.id.clone(), name: "box".into(), ..Default::default() }, /* timeout */ None, ) .await { Ok(sb) => { let _ = sb.exec_shell("false", Default::default()).await?.wait().await?; } Err(SailError::Creation { message, .. }) => eprintln!("creation failed: {message}"), Err(err) => eprintln!("{err}"), } ``` ## Command failure is not an exception A command that runs to completion and exits nonzero is not an SDK failure. `run` and `exec(...).wait()` return normally with the exit code on the result; check it yourself: ```python Python theme={null} result = sb.run("make test") if result.exit_code != 0: print(result.stderr) ``` ```typescript TypeScript theme={null} const result = await sb.run("make test"); if (result.exitCode !== 0) { console.error(result.stderr); } ``` ```rust Rust theme={null} let result = sb.run_shell("make test", RunOptions::default()).await?; if result.exit_code != 0 { eprint!("{}", result.stderr); } ``` To treat command failure as an error, pass `check` to `run`: a nonzero exit or a timeout then raises `CommandFailedError`, which carries the completed result. Rust has no `check`; it reports a nonzero exit through the returned `ExecResult`, so use the exit-code check above. ```python Python theme={null} try: sb.run("make test", check=True) except sail.CommandFailedError as err: print(err.result.stderr) ``` ```typescript TypeScript theme={null} try { await sb.run("make test", { check: true }); } catch (err) { if (err instanceof CommandFailedError) console.error(err.result.stderr); } ``` Exec errors in the taxonomy below cover the SDK failing to run the command at all, not the command's own exit status. ## The taxonomy | Failure | Python | TypeScript | Rust `SailError::` | | --------------------------- | --------------------------------- | --------------------------------- | --------------------- | | Creation failed | `SailboxCreationError` | `SailboxCreationError` | `Creation` | | Custom image build failed | `ImageBuildError` | `ImageBuildError` | `ImageBuild` | | Exec failed | `SailboxExecutionError` | `SailboxExecutionError` | `Execution` | | Sailbox gone (terminated) | `SailboxTerminatedError` | `SailboxTerminatedError` | `Terminated` | | Unknown exec request | `SailboxExecRequestNotFoundError` | `SailboxExecRequestNotFoundError` | `ExecRequestNotFound` | | Host machine lost mid-run | `SailboxHostLostError` | `SailboxHostLostError` | `HostLost` | | Unknown id / unexposed port | `NotFoundError` | `NotFoundError` | `NotFound` | | Auth failure | `PermissionDeniedError` | `PermissionDeniedError` | `PermissionDenied` | | Invalid argument | `InvalidArgumentError` | `InvalidArgumentError` | `InvalidArgument` | | Missing guest file | `FileNotFoundError` | `FileNotFoundError` | `FileNotFound` | | Readiness or build timeout | `TimeoutError` | `TimeoutError` | `Transport` (timeout) | | Writing to a closed stdin | `BrokenPipeError` | `BrokenPipeError` | `BrokenPipe` | | Network/transport failure | `TransportError` | `TransportError` | `Transport` | | Deleting an in-use secret | `SecretInUseError` | `SecretInUseError` | `Api` | | Deleting an attached policy | `HttpPolicyInUseError` | `HttpPolicyInUseError` | `Api` | | Unexpected API response | `ApiError` | `ApiError` | `Api` | Every class derives from `sail.SailError`. The Python classes that match a Python builtin also inherit it (`NotFoundError` is a `LookupError`, `TimeoutError` the builtin `TimeoutError`, `ApiError` a `RuntimeError`, and so on), so handlers written against the builtins keep working. `SecretInUseError` and `HttpPolicyInUseError` subclass `ApiError`, so handlers that catch `ApiError` also catch them. API and creation failures carry `status_code` (`status` in TypeScript) and the parsed response `body`. Every error carries a `retryable` flag: `True` means retrying the same call may succeed (the failure was transient, like a network drop or a busy service), `False` means it is deterministic and a retry would just fail the same way. ## Notable errors ### Creation failed Raised when [`Sailbox.create`](/sailbox-sdk#sailbox-create) fails. When creation succeeded but SSH setup failed (with `ssh=True`), the message carries the new Sailbox's id so you can fetch it to retry `enable_ssh` or terminate it. ### Host machine lost mid-run 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 [`exec`](/sailbox-sdk#exec) again starts it over from the beginning, so any side effects the partial run applied will happen again. The Sailbox itself recovers automatically; you do not need to resume it. ### Function errors (Python only) `SailboxFunctionError` is raised when a [`@sail.function`](/sailbox-sdk-images#sail-function) call fails while running in the Sailbox. It carries the remote failure context: | Attribute | Type | Description | | ------------ | ----- | ---------------------------- | | `error_type` | `str` | Remote exception class name. | | `traceback` | `str` | Remote traceback text. | | `stdout` | `str` | Captured remote stdout. | | `stderr` | `str` | Captured remote stderr. | `SailboxFunctionSerializationError` is raised when a function payload or result cannot be serialized, or the remote function runtime cannot be prepared (including a Python major.minor version mismatch between your local interpreter and the Sailbox's `python3`). Both are subclasses of `SailboxExecutionError`. # Images & Functions Source: https://docs.sailresearch.com/sailbox-sdk-images Base images, the custom-image builder, and running Python functions in a Sailbox Images define the root filesystem a Sailbox boots from: start from a base image, optionally chain build steps, and pass the result to [`Sailbox.create`](/sailbox-sdk#sailbox-create). `@sail.function` additionally lets Python ship a local function into a Sailbox and run it as if it were local. See the [Images guide](/sailboxes-images) for a task-oriented walkthrough. ## Base images ```python Python theme={null} arm = sail.Image.debian_arm64 amd = sail.Image.debian_amd64 # Devtools preinstalled, for use as a remote developer workstation: dev = sail.Image.devbox("arm64") ``` ```typescript TypeScript theme={null} import { Image } from "@sailresearch/sdk"; const arm = Image.debian("arm64"); const amd = Image.debian("amd64"); // Devtools preinstalled, for use as a remote developer workstation: const dev = Image.devbox("arm64"); ``` ```rust Rust theme={null} use sail::{BaseImage, ImageArchitecture}; use sail::imagebuild::ImageDefinition; let arm = ImageDefinition { base: Some(BaseImage::Debian), architecture: ImageArchitecture::Arm64, ..Default::default() }; ``` The `devbox` images are the Debian base plus a baked development layer: Node LTS with `npm`, `build-essential` compilers, the OS libraries editor remote servers need, the `claude` and `codex` CLIs, Docker with the `docker compose` and `docker buildx` plugins, and common developer tools (`jq`, `gh`, `fd`, `fzf`, `uv`, `mise`, `tmux`, `git-lfs`, and more). Devbox images boot fast because the whole layer ships prebuilt, and `uv`/`mise` lazy-install further language toolchains on demand. The trade-off is that they are prebuilt only: builder methods such as `apt_install` and `pip_install` are rejected on a devbox base. Use a `debian` base when you need custom build steps. The Docker daemon starts automatically when a devbox Sailbox boots and keeps running across sleeps. Right after a fresh boot, the daemon can take a few seconds to accept commands. If it ever stops, start `dockerd` again as root. If it complains about a stale pid file, delete `/var/run/docker.pid` and retry. Devbox images also have a working clipboard. During `sail box shell`, Ctrl+V puts your local clipboard on the guest's clipboard. Pasting a screenshot into `claude` or `codex` works exactly as it does on your own machine, and text copied inside the guest is copied back to your local clipboard. In Python, `sail.Image.debian_arm64` and `debian_amd64` are shorthand for `sail.Image.debian("arm64")` and `debian("amd64")`, and pin the image to your local Python version so `@sail.function` can deserialize local bytecode. Pass `install_python=False`, as in `sail.Image.debian("arm64", install_python=False)`, to keep the base's stock `python3` instead. ### from\_registry Use your own image as the root filesystem. Sail pulls it and layers the Sailbox runtime on top, so every builder method works the same as on a Debian base. ```python Python theme={null} image = sail.Image.from_registry("python:3.13") ``` ```typescript TypeScript theme={null} const image = Image.fromRegistry("python:3.13"); ``` ```rust Rust theme={null} let image = ImageDefinition { oci_ref: Some("python:3.13".to_string()), ..Default::default() }; ``` Reference a Debian- or Ubuntu-based 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`, `acme/tool` means `docker.io/acme/tool`, and the other registries are named in full, as in `ghcr.io/acme/tool`. You can pass a tag, a digest (`name@sha256:...`), or just the name, which means the `latest` tag. A tag is pinned for your organization once an image has been built from it: later builds keep getting that version, even if the tag moves upstream. Use `force_build` (`forceBuild` in TypeScript, `BuildMode::ForceBuild` in Rust) to look the tag up again and move the pin for your whole organization. If forced builds of the same tag overlap, the last-requested one that succeeds decides what the tag means, no matter which build finishes first. A digest names exactly one image, so it never moves. 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, and the build fails if the image was not built for it. Its environment variables, working directory, and `USER` become the defaults for commands you run; its `ENTRYPOINT` and `CMD` are not run. See [Bring your own base image](/sailboxes-images#bring-your-own-base-image) for the full requirements. ### from\_dockerfile Build your own Dockerfile into the image. Sail builds it and layers the Sailbox runtime on top, so every builder method composes on the result. ```python Python theme={null} from pathlib import Path env_dir = Path("./envs/task1") image = sail.Image.from_dockerfile(env_dir / "Dockerfile", context_dir=env_dir) ``` ```typescript TypeScript theme={null} const image = Image.fromDockerfile("./envs/task1/Dockerfile", { contextDir: "./envs/task1", }); ``` ```rust Rust theme={null} use std::collections::HashMap; use sail::imagebuild::{DockerfileInput, DockerfileSource, ImageDefinition}; let image = ImageDefinition { dockerfile: Some(DockerfileSource { dockerfile: DockerfileInput::Path("./envs/task1/Dockerfile".into()), context_dir: Some("./envs/task1".into()), build_args: HashMap::new(), ignore: Vec::new(), }), ..Default::default() }; ``` Pass the path to a Dockerfile, or its literal text with `contents=` (`{ contents }` in TypeScript, `DockerfileInput::Contents` in Rust). `context_dir` is the directory `COPY` and `ADD` read from, with `.dockerignore` honored (a `.dockerignore` file named after your Dockerfile, for example `Dockerfile.dockerignore`, is used instead when present, as with Docker) and `ignore` patterns applied on top. Every image a `FROM` or `COPY --from` names must live on a supported public registry, and a short name works as you would expect: `FROM python:3.12` means `docker.io/library/python:3.12`. The image the Dockerfile produces must be Debian- or Ubuntu-based. The build runs for amd64 unless you pass `architecture`, and `build_args` values fill the Dockerfile's `ARG` instructions. Tags a `FROM` or `COPY --from` names are pinned on your organization's first use and reused after that; `force_build` (`forceBuild` in TypeScript, `BuildMode::ForceBuild` in Rust) looks them up again. See [Build from a Dockerfile](/sailboxes-images#build-from-a-dockerfile) for the full behavior. ## The image builder An image definition is an immutable value. Builder methods return a new definition, so you chain them and either pass the result straight to `Sailbox.create` (which builds it for you) or call `build()` to build eagerly. ```python Python theme={null} image = ( sail.Image.debian_arm64 .apt_install("git", "curl") .pip_install("httpx") .env({"LOG_LEVEL": "info"}) .build() ) ``` ```typescript TypeScript theme={null} const spec = await Image.debian("arm64") .aptInstall("git", "curl") .pipInstall("httpx") .env({ LOG_LEVEL: "info" }) .build(); ``` ```rust Rust theme={null} use std::collections::HashMap; use sail::{BaseImage, ImageArchitecture}; use sail::imagebuild::{BuildMode, ImageDefinition, ImageDefinitionStep}; use std::time::Duration; let image = ImageDefinition { base: Some(BaseImage::Debian), architecture: ImageArchitecture::Arm64, env: HashMap::from([("LOG_LEVEL".to_string(), "info".to_string())]), steps: vec![ ImageDefinitionStep::AptInstall(vec!["git".into(), "curl".into()]), ImageDefinitionStep::PipInstall(vec!["httpx".into()]), ], ..Default::default() }; let spec = client .build_image_definition(&image, Duration::from_secs(1800), BuildMode::ReuseExisting) .await?; ``` ### apt\_install ```python Python theme={null} def apt_install(*packages: str) -> ImageDefinition ``` ```typescript TypeScript theme={null} aptInstall(...packages: string[]): Image ``` ```rust Rust theme={null} ImageDefinitionStep::AptInstall(packages: Vec) ``` Adds a step that installs Debian packages with `apt`. Requires at least one non-empty package name. ### pip\_install ```python Python theme={null} def pip_install(*packages: str) -> ImageDefinition ``` ```typescript TypeScript theme={null} pipInstall(...packages: string[]): Image ``` ```rust Rust theme={null} ImageDefinitionStep::PipInstall(packages: Vec) ``` Adds a step that installs Python packages with `pip`. Requires at least one non-empty package name. ### run\_commands ```python Python theme={null} def run_commands(*cmd: str) -> ImageDefinition ``` ```typescript TypeScript theme={null} runCommand(command: string): Image ``` ```rust Rust theme={null} ImageDefinitionStep::RunCommand(command: String) ``` Adds one build step per shell command, in order. Each command must be non-empty. ### add\_local\_file ```python Python theme={null} def add_local_file( local_path: str | Path, remote_path: str, *, mode: int | None = None, ) -> ImageDefinition ``` ```typescript TypeScript theme={null} addLocalFile(localPath: string, remotePath: string, options?: { mode?: number; }): Image ``` ```rust Rust theme={null} ImageDefinitionStep::AddLocalFile { local_path: PathBuf, remote_path: String, mode: Option, } ``` Bakes the contents of one local file into the image at `remote_path`. Only the file's content hash, target path, and mode identify the image, so a one-byte change forces a rebuild. | Parameter | Default | Description | | ------------- | -------- | ------------------------------------------------------------------------ | | `local_path` | required | Path to the local file. | | `remote_path` | required | Absolute POSIX destination. A trailing slash appends the local basename. | | `mode` | `None` | POSIX permission bits (low 9 bits, max `0o777`). Defaults to `0o644`. | Raises an invalid-argument error if the source is missing, the path is invalid, or the file exceeds the 5 GiB single-file limit. ### add\_local\_dir ```python Python theme={null} def add_local_dir( local_path: str | Path, remote_path: str, *, ignore: Sequence[str] | Path | str | None = None, ) -> ImageDefinition ``` ```typescript TypeScript theme={null} addLocalDir(localPath: string, remotePath: string, options?: { ignore?: string[]; ignoreFile?: string; }): Image ``` ```rust Rust theme={null} ImageDefinitionStep::AddLocalDir { local_path: PathBuf, remote_path: String, ignore: Vec, ignore_file: Option, } ``` Bakes a local directory into the image at `remote_path`. Each regular file is hashed and uploaded; per-file modes come from the local stat. Symlinks are skipped. `ignore` takes gitignore-style patterns, or point at an existing ignore file (such as `.gitignore`) instead. `remote_path` must be absolute. ### env ```python Python theme={null} def env(env: dict[str, str]) -> ImageDefinition ``` ```typescript TypeScript theme={null} env(env: Record): Image ``` ```rust Rust theme={null} // ImageDefinition field: env: HashMap ``` Sets environment variables baked into the image. Requires at least one non-empty key. ### build ```python Python theme={null} def build(*, timeout: int = 1800, force_build: bool = False) -> ImageDefinition ``` ```typescript TypeScript theme={null} build(options?: { timeoutSeconds?: number; forceBuild?: boolean; }): Promise ``` ```rust Rust theme={null} pub async fn build_image_definition( &self, // Client def: &ImageDefinition, timeout: Duration, mode: BuildMode, ) -> Result ``` Builds the image and blocks until it is ready, returning a built definition you can create Sailboxes from. `timeout` bounds the whole pipeline (local file uploads and the build) and must be `> 0`. Everything you create from the result needs no further build. By default, Sail may reuse an existing ready build for the definition (`BuildMode::ReuseExisting` in Rust). Use `force_build=True`, `forceBuild: true`, or `BuildMode::ForceBuild` 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 with [`from_registry`](#from_registry) through a 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 the result of an earlier build keeps its pinned version. For an image built with [`from_dockerfile`](#from_dockerfile), a forced build looks up the tags its `FROM` and `COPY --from` instructions name and moves those pins for your whole organization, while the result of an earlier build keeps the versions its 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. Raises an image-build error if the build fails and a timeout error if it does not finish within `timeout`. You rarely need to call `build()` yourself: passing an unbuilt definition to [`Sailbox.create`](/sailbox-sdk#sailbox-create) builds it first (bounded by `image_build_timeout`). *** ## @sail.function Python only. ```python theme={null} @sail.function def fn(...): ... # or @sail.function() def fn(...): ... ``` Decorates a Python function so it can run inside a Sailbox via [`Sailbox.exec`](/sailbox-sdk#exec). The decorator returns a [`SailFunction`](#sailfunction); calling it locally still invokes the original function unchanged. ```python theme={null} @sail.function def add(x: int, y: int) -> int: return x + y value = sb.exec(add, 2, 3, timeout=30) print(value) # 5 ``` When you pass a `SailFunction` to `exec`, the call blocks and returns the function's return value directly (not a `ExecProcess`). The SDK serializes the function plus its arguments, runs it with the image's `python3`, and returns the deserialized result. **Constraints:** * Function execution is synchronous; `background=True` is not supported. * The Sailbox's `python3` must match your local Python major.minor, because the serialized bytecode is version-sensitive. This is why the `debian` bases pin the local version. * Imported third-party packages are referenced by name, so they must exist in the Sailbox environment. * Keep arguments and return values small; write large artifacts from inside the Sailbox and return a small reference instead. 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 the exec's `output_buffer_bytes` (1 MiB by default, up to 64 MiB); 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. `output_mode` must stay `auto` for a function. **Raises** `sail.SailboxFunctionError` (with the remote `error_type`, `traceback`, `stdout`, `stderr` attached) when the function raises remotely, and `sail.SailboxFunctionSerializationError` if the payload or result cannot be serialized or the runtime cannot be prepared. See [Errors](/sailbox-sdk-errors). ### SailFunction The wrapper returned by `@sail.function`. You normally don't construct it directly. Calling a `SailFunction` locally is identical to calling the wrapped function. Async functions, async generators, and generator functions are rejected at decoration time with `TypeError`. | Member | Description | | --------------------------- | ------------------------------------- | | `func` | The wrapped callable. | | `__call__(*args, **kwargs)` | Invokes the wrapped function locally. | # What are Sailboxes? Source: https://docs.sailresearch.com/sailboxes Efficient cloud environments for long-horizon agents This page is a high-level guide to Sailboxes, efficient cloud environments designed for rollouts and long-horizon agents. See the [Sailbox reference](/sailbox-sdk) for reference documentation on Sailboxes. ## What are Sailboxes and why should I use them? Sailboxes are persistent Linux VMs designed for long-horizon agents. They can be dynamically provisioned in seconds and are the perfect cloud environment for any agent. They provide a number of advantages over other sandboxing providers: * Cost efficiency: we are both >70% cheaper than other providers and only charge you for the exact portion of CPU, memory, and disk you use. Since agents spend most of their time blocked on I/O, we are significantly more cost-efficient than anyone else. * Elastic scaling: use as much (or as little) compute as you need, up to your Sailbox size's CPU, memory, and disk ceilings. Your agent will never OOM, and because billing follows actual usage, you will never be charged for unused capacity. * Pause and resume all sandbox state. In-flight work survives the pause with no save/restore code on your side, and open network connections survive for up to 10 minutes. ## Provider comparison
| Provider | vCPU | Memory | Sleep during inference | Max runtime | Memory snapshots | Local NVMe disks | Docker-in-Docker | Start/resume time | | ------------- | ----------------------------- | ---------------------------- | ---------------------- | -------------- | ---------------- | ---------------- | ---------------- | ----------------- | | Sailboxes | \$0.015
active vCPU·h | \$0.008
active GB·h | Yes | No fixed limit | Yes | Yes | Yes | \<2s | | Modal | \$0.071
active vCPU·h | \$0.008
reserved\* GB·h | No | 24 hours | Alpha | No | Beta | \<500ms | | E2B | \$0.0504
reserved vCPU·h | \$0.0162
reserved GB·h | No | 24 hours | Yes | No | Yes | \<1s | | Vercel | \$0.128
active vCPU·h | \$0.0212
reserved GB·h | No | 5 hours | No | Yes | Yes | \<1s | | Daytona | \$0.0504
reserved vCPU·h | \$0.0162
reserved GB·h | No | No fixed limit | Experimental | Yes | Yes | \<500ms | | Cloudflare | \$0.072
active vCPU·h | \$0.009
reserved GB·h | No | No fixed limit | No | Yes | Yes | \<3s | | AWS AgentCore | \$0.0895
active vCPU·h | \$0.00945
active GB·h | No | 8 hours | No | No | No | 1s |

\*Modal allows bursting beyond reserved capacity, but this is best-effort (e.g. sandboxes OOM if no memory is available).

## What is the tradeoff? Sailboxes achieve their efficiency through live migrations: we are able to achieve much higher utilization on our underlying hardware than other providers by migrating VMs based on their live resource usage. These migrations only occur a few times a day, take several seconds, and happen completely without the knowledge of the agent. While these migrations mean that the occasional command incurs a few seconds of additional latency, the tradeoff is massive gains in cost-efficiency. Unlike human-in-the-loop workflows like chatbots where patience is limited, long-horizon agents can tolerate the occasional latency blip without issue. Sailboxes directly trade off this p99 latency for better economics and we believe this makes them the ideal platform for any long-horizon agent. ## Start here * [Quickstart](/sailboxes-quickstart): create a Sailbox, run commands, expose a service, and clean up. * [Migrate to Sailboxes](/sailboxes-migrating): move an app, an agent, or a dev environment onto Sailboxes, with your coding agent doing the work. * [Pricing](/sailboxes-pricing): understand observed-usage billing dimensions and per-hour rates. * [Network policy](/sailboxes-network-policy): limit what a Sailbox can reach, or cut it off from the network entirely. * [Credential injection](/sailboxes-credentials): let a Sailbox call authenticated HTTPS APIs without storing credentials inside it. * [Harbor](/harbor): run Harbor evaluation tasks on Sailboxes with one flag. # Access Control Source: https://docs.sailresearch.com/sailboxes-access-control Who can operate a Sailbox, and who can reach what it serves There are two things to control: who can operate a Sailbox, and who can reach the services it exposes. ## Choose who can operate the Sailbox Every Sailbox has a visibility, fixed for its life: * **Org** (the default) lets anyone in your organization run commands, copy files, SSH in, and pause, sleep, checkpoint, or terminate it. * **Private** restricts all of that to you. Your organization can still see the Sailbox in listings, but cannot act on it; an org admin can override that for commands and lifecycle operations by giving a reason, which is recorded in the audit log. Creating one requires an API key minted by you, so that Sail knows who the creator is. A service key, which belongs to the organization rather than to a member, cannot create or operate one.
```bash CLI theme={null} sail box create --app my-app --name mybox --visibility private ``` ```python Python theme={null} sb = sail.Sailbox.create(app=app, name="mybox", visibility="private") ``` ```typescript TypeScript theme={null} const sb = await Sailbox.create({ app, name: "mybox", visibility: "private" }); ``` ```rust Rust theme={null} use sail::{CreateSailboxRequest, Visibility}; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "mybox".into(), visibility: Visibility::Private, ..Default::default() }, /* timeout */ None, ) .await?; ```
## Make a web server public A Sailbox accepts no inbound traffic until you expose a port. Exposing an HTTP port gives it a public HTTPS URL that anyone can reach, with TLS handled for you. Nothing inside the Sailbox needs to know about certificates or hostnames.
```bash CLI theme={null} sail box create --app my-app --name web --port 8000 sail box address 8000 ``` ```python Python theme={null} sb = sail.Sailbox.create(app=app, name="web", ingress_ports=[8000]) print(sb.wait_for_listener(8000, timeout=60).endpoint.url) ``` ```typescript TypeScript theme={null} const sb = await Sailbox.create({ app, name: "web", ingressPorts: [{ guestPort: 8000, protocol: "http" }], }); const listener = await sb.waitForListener(8000, { timeoutSeconds: 60 }); if (listener.endpoint?.kind === "http") { console.log(listener.endpoint.url); } ``` ```rust Rust theme={null} use sail::{ CreateSailboxRequest, IngressPort, IngressProtocol, ListenerEndpoint, WaitForListenerOptions, }; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "web".into(), ingress_ports: vec![IngressPort { guest_port: 8000, protocol: IngressProtocol::Http, allowlist: Vec::new(), }], ..Default::default() }, /* timeout */ None, ) .await?; let listener = sb .wait_for_listener(8000, WaitForListenerOptions::default()) .await?; if let Some(ListenerEndpoint::Http { url }) = listener.endpoint() { println!("{url}"); } ```
In the SDKs, `expose` and `unexpose` add and remove ports on a running Sailbox: `sb.expose(8080)` in Python, `await sb.expose(8080)` in TypeScript, `sb.expose(8080, IngressProtocol::Http, &[]).await?` in Rust. Add or remove ports on a running Sailbox with `sail box expose ` and `sail box unexpose `. `sail box listeners ` shows what is exposed. HTTP and WebSocket traffic both work, and a sleeping Sailbox wakes when a request arrives. To serve on your own hostname, see [Custom Domains](/sailboxes-custom-domains). ## Restrict who can reach it Pass an allowlist when you expose a port. An entry is an address or range, or the name of a Sail app, which admits authenticated requests from Sailboxes in that app. Anything else fails before it reaches the Sailbox.
```bash CLI theme={null} sail box expose 8080 --allowlist 203.0.113.0/24 --allowlist admin-tools ``` ```python Python theme={null} sb.expose(8080, allowlist=["203.0.113.0/24", "admin-tools"]) ``` ```typescript TypeScript theme={null} await sb.expose(8080, { allowlist: ["203.0.113.0/24", "admin-tools"] }); ``` ```rust Rust theme={null} use sail::IngressProtocol; sb.expose( 8080, IngressProtocol::Http, &["203.0.113.0/24".to_string(), "admin-tools".to_string()], ) .await?; ```
At create time, pass the same allowlist on the port: `ingress_ports=[sail.IngressPort(8080, allowlist=[...])]` in Python, `ingressPorts: [{ guestPort: 8080, protocol: "http", allowlist: [...] }]` in TypeScript, and the `allowlist` field of `IngressPort` in Rust. Re-exposing a port replaces its whole allowlist, so the same command tightens or relaxes access. An app name does not have to exist yet, and names from other organizations never match. For a Sailbox to pass an app-name allowlist, its request must carry the identity headers the SDK provides. Inside the calling Sailbox: ```python theme={null} import requests import sail requests.get(url, headers=sail.ingress_auth_headers()) ``` From outside, such as a test driving several Sailboxes, fetch the headers for a Sailbox you own with `source_sb.ingress_auth_headers()` (`sourceSb.ingressAuthHeaders()` in TypeScript). ## Raw TCP ports Expose a port as raw TCP for protocols other than HTTP, such as Postgres or a custom server. You get a public host and port.
```bash CLI theme={null} sail box expose 5432 --tcp --allowlist 203.0.113.0/24 ``` ```python Python theme={null} listener = sb.expose(5432, protocol="tcp", allowlist=["203.0.113.0/24"]) ``` ```typescript TypeScript theme={null} const listener = await sb.expose(5432, { protocol: "tcp", allowlist: ["203.0.113.0/24"], }); ``` ```rust Rust theme={null} use sail::IngressProtocol; let listener = sb .expose(5432, IngressProtocol::Tcp, &["203.0.113.0/24".to_string()]) .await?; ```
A raw TCP port has no platform-side authentication. Whatever is listening inside the Sailbox is the only access control, so make sure it requires credentials. Raw TCP connections carry no app identity, so a TCP allowlist holds addresses and ranges only. Exposing a well-known unauthenticated port such as Postgres, MySQL, or Redis without an allowlist is rejected; pass `--allowlist 0.0.0.0/0 --allowlist ::/0` to confirm you want it open to everyone. ## Connect with a shell The quickest way into a Sailbox is `sail box shell`. It opens an interactive terminal over the same channel the CLI uses to run commands.
```bash CLI theme={null} sail box shell ``` ```python Python theme={null} sb.shell() ``` ```typescript TypeScript theme={null} await sb.shell(); ```
While the shell is open, servers the Sailbox runs on localhost and links it opens are forwarded to your machine, so you can develop against it without exposing anything. Pass `--no-forward` to turn that off. ## SSH access Reach for SSH when you need a real SSH endpoint rather than a terminal: `scp` and `rsync`, an editor's remote mode, or port forwarding you control. Enabling it exposes port 22, which counts against your organization's raw TCP limit. SSH is organization-scoped. Enabling it exposes port 22, and the Sailbox trusts your organization's certificate authority, so anyone in the org can connect with a short-lived certificate for their own key. There are no per-Sailbox keys to hand out. A private Sailbox is the exception: its SSH server accepts only its creator's certificates.
```bash CLI theme={null} sail box ssh enable --allowlist 203.0.113.0/24 ssh .sail ``` ```python Python theme={null} sb.enable_ssh(allowlist=["203.0.113.0/24"]) ``` ```typescript TypeScript theme={null} await sb.enableSsh({ allowlist: ["203.0.113.0/24"] }); ``` ```rust Rust theme={null} use sail::EnableSshOptions; sb.enable_ssh(EnableSshOptions { allowlist: vec!["203.0.113.0/24".to_string()], ..Default::default() }) .await?; ```
The SDK call enables SSH on the Sailbox. To connect from a machine, run `sail box ssh alias ` there once; the CLI's `enable` does that for you. # Autosleep Source: https://docs.sailresearch.com/sailboxes-autosleep Automatically sleep idle Sailboxes and wake them on demand Autosleep is optional and on by default. When it is on, Sail sleeps a Sailbox that has sat idle and wakes it the moment anything needs it. This also lets a Sailbox sleep while it is blocked waiting on an inference request, and wake when the reply arrives. You are not charged while a Sailbox sleeps, and waking takes a couple of seconds. Turn it off for a Sailbox that must stay up, or change how long it must be idle first. ## What counts as idle Sail sleeps a Sailbox on its own only when nothing inside would notice. All of these must hold, and Sail checks them again just before the sleep: * **No process has used CPU** since the last check. Even slow background work keeps the Sailbox awake. * **No process is waiting on a timer.** The one exception is an alarm set for a wall-clock time, such as a job scheduled for 9:00. Sail lets the Sailbox sleep and wakes it shortly before the alarm is due, unless the alarm is only a couple of minutes away, in which case it stays up. * **No TCP or UDP connection is open** from inside the Sailbox, in either direction, other than a request that is waiting on its reply. A Sailbox has to stay in that state for the idle window before Sail sleeps it. The default window is about five minutes; you can set your own, from one second to an hour. Sail checks periodically, so the actual sleep can land a little after the window ends. ## What survives Sleeping checkpoints the whole machine. Disk, memory, and running processes come back exactly as they were, and a process that was blocked, waiting for input or a wall-clock alarm, carries on from where it was. Any wake can come back cold instead, with the disk intact and nothing running. Write code that expects a cold start. ## What wakes it * A request to one of its exposed ports, HTTP or raw TCP. The connection is held while the Sailbox wakes, then forwarded. * A command: `sail box exec`, `sail box shell`, a file transfer, or an SSH connection. Binding an existing Sailbox by id and running a command wakes it in every SDK, with no explicit resume. * A wall-clock alarm a process inside set, as above. * An explicit resume, or a scheduled wake. ## Turn it off, or change the window `never` keeps the Sailbox running until you sleep, pause, or terminate it yourself. A number of seconds from 1 through 3600 replaces the default idle window. `auto` restores the default. The setting can be changed at any point in a Sailbox's life, and your own `sleep`, `pause`, `resume`, and scheduled wakes work the same whatever it is.
```bash CLI theme={null} sail box create --app my-app --name session --auto-sleep never sail box auto-sleep 30 ``` ```python Python theme={null} sb = sail.Sailbox.create(app=app, name="session", auto_sleep=sail.AutoSleep.never()) sb.set_auto_sleep(sail.AutoSleep.not_before(30)) ``` ```typescript TypeScript theme={null} const sb = await Sailbox.create({ app, name: "session", autoSleep: { automatic: false }, }); await sb.setAutoSleep({ automatic: true, minSecondsBeforeSleep: 30 }); ``` ```rust Rust theme={null} use sail::{AutoSleep, CreateSailboxRequest}; use std::time::Duration; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "session".into(), auto_sleep: AutoSleep::Never, ..Default::default() }, /* timeout */ None, ) .await?; sb.set_auto_sleep(AutoSleep::NotBefore(Duration::from_secs(30))).await?; ```
The window only says when Sail may consider sleeping the Sailbox. It still has to be idle by the rules above. ## Sleep it yourself `sleep` checkpoints the Sailbox and powers it down right away; it wakes on the same triggers as an automatic sleep. `pause` does the same but ignores traffic and commands: only an explicit `resume` brings it back.
```bash CLI theme={null} sail box sleep sail box pause sail box resume ``` ```python Python theme={null} sb.sleep() sb.pause() sb.resume() ``` ```typescript TypeScript theme={null} await sb.sleep(); await sb.pause(); await sb.resume(); ``` ```rust Rust theme={null} sb.sleep(/* wake_at */ None).await?; sb.pause().await?; sb.resume().await?; ```
## Wake at a time Give `sleep` a wake time and Sail restores the Sailbox when it arrives. Use it for agents and services that sleep between runs and need to be up at a known moment, such as a daily job or a follow-up an agent scheduled for itself.
```bash CLI theme={null} sail box sleep --wake-at 2h ``` ```python Python theme={null} from datetime import datetime, timedelta, timezone sb.sleep(wake_at=datetime.now(timezone.utc) + timedelta(hours=2)) ``` ```typescript TypeScript theme={null} await sb.sleep(new Date(Date.now() + 2 * 60 * 60 * 1000)); ``` ```rust Rust theme={null} use sail::time::{Duration, OffsetDateTime}; sb.sleep(Some(OffsetDateTime::now_utc() + Duration::hours(2))).await?; ```
A Sailbox holds one scheduled wake. An earlier request replaces it; a later one leaves the sooner wake in place, and the call returns whichever won. Calling `sleep` with a time on a Sailbox that is already asleep just updates the wake. The CLI takes a delay like `30m` or `2h`, or an RFC 3339 timestamp. The wake can land a little late, so give it a minute or two of headroom. A paused Sailbox rejects a scheduled wake; only `resume` brings it back. # Billing Overview Source: https://docs.sailresearch.com/sailboxes-billing How Sailbox usage turns into a bill When you create a Sailbox, you specify the size of the VM. You are eligible to use all the resources in your VM and you are only charged for observed usage, not capacity. For example, if a Sailbox with a 4 vCPU ceiling is using 0.01 vCPU, you are charged for 0.01 vCPU. You are not charged at all while a Sailbox is sleeping, paused, checkpointing, or cold-starting. ## What each size includes You pick a size at create time: `s`, `m`, or `l`. A size sets the vCPU count and the default memory and disk ceilings, and it sets the creation charge. Ceilings cap what a workload may spend; they do not reserve anything, so a bigger size costs nothing extra while the Sailbox sits idle. | Size | vCPU | Memory ceiling | Disk ceiling | | ---- | ---- | ---------------------- | ----------------------- | | `s` | 1 | 16 GiB (up to 64 GiB) | 32 GiB (up to 128 GiB) | | `m` | 4 | 32 GiB (up to 128 GiB) | 128 GiB (up to 512 GiB) | | `l` | 8 | 64 GiB (up to 256 GiB) | 256 GiB (up to 1 TiB) | Pass `memory_limit_gib` or `disk_limit_gib` at create time to set a ceiling, within the range shown. The default size is `m`. Choose `s` for the fastest cold starts and resumes; its lower ceilings also cap what a runaway workload can consume. ## Where to see usage * **Dashboard.** The Sailboxes page at [app.sailresearch.com](https://app.sailresearch.com) shows spend for the period broken down by app and by Sailbox, alongside live CPU, memory, and disk utilization. The Billing page holds your credit balance and invoices. * **CLI.** `sail box list` shows each Sailbox's current CPU, memory, and disk usage against its ceilings, and `sail box top` is a live view of the same. ## FAQ Only at creation, and only slightly: the creation charge differs by size. Ongoing charges follow what the Sailbox uses, so an `l` Sailbox that idles at 0.05 vCPU costs the same per hour as an `s` doing the same. Nothing. Sleeping, paused, checkpointing, and cold-starting time is not billed. Charges resume when the Sailbox is running again. Yes. A Sailbox started from a checkpoint is a full Sailbox: it pays the creation charge for its size and bills for its own usage until it sleeps or you terminate it. Clean up a fleet when the work is done. Sail pauses your running and sleeping Sailboxes and blocks anything that would add spend: creating, resuming, and running commands. Nothing is terminated, and you can still pause and terminate from the dashboard. Add credits on the Billing page, then resume the Sailboxes you need. # Credential injection Source: https://docs.sailresearch.com/sailboxes-credentials Let a Sailbox use an API key it can never read Credential injection puts secrets on the network instead of in the Sailbox. You store an API key with Sail, write a policy that says which HTTPS host gets it, and attach the policy to a Sailbox. Code inside sends a normal request with no credential, and Sail adds it on the way out. The Sailbox, and any agent running on it, can use the API but can never read the key. ## Try it This stores a secret, injects it as a bearer token on requests to `httpbin.org`, and asks httpbin to echo the request back: Save the policy as `demo.json`: ```json demo.json theme={null} { "httpbin.org": { "rules": [ { "request": { "set": { "headers": { "authorization": "Bearer ${secrets.DEMO_TOKEN}" } } } } ] } } ``` Then store the secret, create and attach the policy, and make a request from inside the Sailbox:
```bash theme={null} DEMO_TOKEN=me sail secret set DEMO_TOKEN --from-env DEMO_TOKEN sail http-policy create demo --file demo.json sail box http-policy set sail box exec -- curl -s https://httpbin.org/anything -H foo:bar ```
```json Output theme={null} { "headers": { "Accept": "*/*", "Authorization": "Bearer me", "Foo": "bar", "Host": "httpbin.org", "User-Agent": "curl/7.88.1" }, "method": "GET", "url": "https://httpbin.org/anything" } ``` The `curl` inside the Sailbox never saw the token. Only the request that reached httpbin carried it. The same flow from the SDKs, with a GitHub token: ```python Python theme={null} import os import sail sail.Secret.set("GITHUB_TOKEN", os.environ["GITHUB_TOKEN"]) policy = sail.HttpPolicy.create( "github", { "api.github.com": { "rules": [ { "request": { "set": { "headers": { "authorization": "Bearer ${secrets.GITHUB_TOKEN}", }, }, }, }, ], }, }, ) sailbox.set_http_policy(policy) ``` ```typescript TypeScript theme={null} import { HttpPolicy, Secret } from "@sailresearch/sdk"; await Secret.set("GITHUB_TOKEN", process.env.GITHUB_TOKEN!); const policy = await HttpPolicy.create("github", { "api.github.com": { rules: [ { request: { set: { headers: { authorization: "Bearer ${secrets.GITHUB_TOKEN}", }, }, }, }, ], }, }); await sailbox.setHttpPolicy(policy); ``` ```rust Rust theme={null} use sail::Client; use serde_json::json; let client = Client::from_env()?; client .set_secret("GITHUB_TOKEN", &std::env::var("GITHUB_TOKEN")?) .await?; let policy = client .create_http_policy( "github", &json!({ "api.github.com": { "rules": [{ "request": { "set": { "headers": { "authorization": "Bearer ${secrets.GITHUB_TOKEN}", }, }, }, }], }, }), ) .await?; sailbox.set_http_policy(&policy).await?; ``` ## Secrets A secret is a named value that belongs to your organization.
```bash theme={null} sail secret set OPENAI_API_KEY # type the value at a hidden prompt sail secret set OPENAI_API_KEY --from-env OPENAI_API_KEY op read "op://vault/openai/key" | sail secret set OPENAI_API_KEY # or pipe it in sail secret list sail secret delete OPENAI_API_KEY ```
Setting a name that already exists replaces its value. The next matching request from any Sailbox whose policy uses it gets the new value. Names start with a letter or digit and may contain letters, digits, `_`, and `-`, up to 128 characters. A value is one non-empty line of text up to 64 KiB, with no tabs, line breaks, or other control characters. ## Policies A policy is a JSON document that belongs to your organization. Each top-level key is a host, and each host has an ordered list of rules. Sail picks the most specific host entry for a request, then the first rule under it that matches, and applies that rule. If nothing matches, the request goes out unchanged. ```json theme={null} { "api.example.com": { "rules": [ { "match": { "method": "POST", "path": { "prefix": "/v1/" } }, "request": { "set": { "headers": { "authorization": "Bearer ${secrets.EXAMPLE_KEY}" } } } }, { "request": { "set": { "headers": { "x-api-version": "2" } } } } ] } } ``` **Hosts.** Write a bare hostname: no `https://`, port, or path. `api.example.com` matches exactly that host. `*.example.com` matches any direct subdomain, and `*` matches everything not named elsewhere. Use an exact host whenever a rule adds a credential: a wildcard sends the credential to every host it matches. **Match.** Leave `match` out to cover every request to the host; such a rule must be last in its list. Otherwise narrow by `method` (upper case, one or a list), `path`, `headers`, or `query`. Values accept a plain string for an exact match, `{"prefix": "..."}`, or `{"one_of": [...]}`; a header or query condition can also assert `"present": false`. **What a rule does.** | Key | What it does | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `request` | `set`, `add`, or `remove` headers and query parameters, or `set` the path. `${secrets.NAME}` works inside `set.headers` and `set.query`. | | `forward` | Send the request to another HTTPS `host` (and optional `port`). Combine with `request` to add a credential for that host. | | `respond` | Answer with `status`, `headers`, and `body` without contacting the destination. Cannot be combined with the other two. | * A `forward` host must be exact. If Sail cannot reach it, the request fails; it is not sent to the original host instead. * `respond` stops that HTTP request only. It is not a network access control; other traffic to the same host may still be possible. * `request.set.headers` and `request.set.query` are templates: every `$` in them must be part of `${secrets.NAME}` or `$$` (a literal dollar sign). In every other string `$` is plain text, except that an unescaped `${` is rejected because it looks like an unresolved reference. * The secret a policy names must exist before the policy is created. * An invalid document fails at create time with an error naming the part to fix. Sail stores a normalized form, so reading a policy back can return lowercased hosts and filled-in defaults; behavior is the same. * A policy's rules cannot change after creation. Create and attach a new one. Renaming is allowed. ## Attaching A Sailbox holds at most one policy, and one policy can serve many Sailboxes.
```bash theme={null} sail box http-policy set # attach, replacing any current policy sail box http-policy show sail box http-policy clear ```
A set or clear applies to HTTPS connections the Sailbox opens after the call succeeds. A connection that is already open keeps the previous policy until it closes, so close long-lived connections if the change must apply to the next request. ## Where secrets live Secrets are stored by Sail and added at the network edge as the request leaves the Sailbox. Nothing inside the Sailbox ever holds the value, and no Sail API returns it: `sail secret show` and `sail secret list` print names and timestamps only.
```bash theme={null} sail secret show DEMO_TOKEN ```
```text Output theme={null} name: DEMO_TOKEN created_at: 2026-09-03T01:55:06.18426Z updated_at: 2026-09-03T01:55:06.18426Z ``` To remove a secret, clear or replace the policy on every Sailbox that uses it, delete every policy that names it, then delete the secret. Sail refuses the other orders. `sail http-policy list` shows how many Sailboxes use each policy and which secrets it names. ## Limitations * Policies apply to HTTPS only. Plain HTTP and raw TCP are unchanged. * A policy applies only when the client announces its HTTP version during the TLS handshake (ALPN), which almost every client does. For one that does not, add `"missing_alpn": "http/1.1"` next to `rules` on an exact host entry to treat its connections as HTTP/1.1. * Request bodies cannot be matched or changed, and responses cannot be changed. # Custom Domains Source: https://docs.sailresearch.com/sailboxes-custom-domains Serve a Sailbox HTTP listener on your own domain Custom domains make it possible to serve an HTTP listener running in a Sailbox on a hostname you own, in addition to the generated `https://sb-<...>.sail.box` URL every listener gets. Sail obtains and renews the TLS certificate for you. ## Add a custom domain ### 1. Expose an HTTP listener for a Sailbox To expose port 3000 for HTTP traffic from the CLI: ```bash theme={null} sail box expose $SAILBOX_ID 3000 ``` See [Access Control](/sailboxes-access-control) for details on what this command does. ### 2. Point your domain at your target address Every organization has its own target address under `sailboxes.sailresearch.com`. Pointing your domain at it is what proves your organization controls the domain. Find your target with: ```bash Command theme={null} sail box domain target # Output: # CNAME target: 1a2b3c4d5e6f7890.sailboxes.sailresearch.com # Wildcard certificate target: 1a2b3c4d5e6f7890._acme-challenge.sailboxes.sailresearch.com ``` Create a `CNAME` record with your DNS provider: ``` app.example.com CNAME 1a2b3c4d5e6f7890.sailboxes.sailresearch.com ``` Cloudflare (and some other DNS providers) proxy/accelerate traffic. Turn that off for this record. On Cloudflare, set the record to "DNS only". #### Optional: Wildcard records If you anticipate attaching many subdomains to Sailboxes, you should use wildcard DNS records. ``` *.example.com CNAME 1a2b3c4d5e6f7890.sailboxes.sailresearch.com # optional, but recommended _acme-challenge.example.com CNAME 1a2b3c4d5e6f7890._acme-challenge.sailboxes.sailresearch.com ``` * The first record sends every direct subdomain to Sail (`app.example.com` would work, but `hello.app.example.com` would not) * The second record lets Sail create one wildcard certificate for them. This is optional: if it is not set, we issue one certificate per subdomain, which risks hitting certificate issuance rate limits (see [Notes](#notes)) * Attach each hostname to a Sailbox. Do not attach `*.example.com`. #### Root/apex domains like `example.com` Standard CNAME records are not allowed for apex domains. Different DNS providers provide different solutions: look for an `ALIAS`, `ANAME`, or `CNAME` flattening option. In addition to an apex record, Sail needs a TXT record for verification. Overall, the setup looks like: ``` # Apex record example.com .sailboxes.sailresearch.com # Additional TXT verification record _sail-domains.example.com TXT .sailboxes.sailresearch.com ``` | DNS provider | Apex option | | ---------------- | ----------------------------------------------------------------------------------------------------------------- | | Cloudflare | CNAME, flattened at the apex automatically | | Namecheap | ALIAS | | DNSimple | ALIAS | | DNS Made Easy | ANAME | | Porkbun | ALIAS | | Amazon Route 53 | None for external targets (ALIAS records reach AWS resources only). Serve on a subdomain such as `www` instead. | | Azure DNS | None for external targets (alias records reach Azure resources only). Serve on a subdomain such as `www` instead. | | Google Cloud DNS | None. Serve on a subdomain such as `www` instead. | ### 3. Add the domain to your Sailbox Attach the domain to the Sailbox and port. Sail checks that the DNS record is in place, then starts serving the hostname: ```bash theme={null} sail box domain attach $SAILBOX_ID app.example.com --port 3000 # Output: # Attached app.example.com to sb_... on guest port 3000 # https://app.example.com ``` `--port` is required and must identify an exposed HTTP listener. You can also do this in the [Sailbox dashboard](https://app.sailresearch.com/sailboxes) under "Network Listeners." ## List, remove, and replace domains ```bash theme={null} # List the domains attached to a Sailbox. sail box domain list $SAILBOX_ID # Output: # DOMAIN GUEST_PORT URL CREATED_AT # app.example.com 3000 https://app.example.com 2026-08-01T00:00:00Z # Attach a domain to a different Sailbox (it is automatically detached from its current Sailbox) sail box domain attach $OTHER_SAILBOX_ID app.example.com --port 3000 # Detach a domain from a Sailbox. sail box domain detach $OTHER_SAILBOX_ID app.example.com ``` Detaching a domain stops routing it to the Sailbox. Attaching a domain to a different Sailbox detaches it from the old Sailbox automatically. ## Custom domains for TCP listeners All the setup listed on this page is for HTTP listeners. If you have a raw TCP endpoint that you wish to point a custom domain at, you do not need to register the domain with Sail. Just add a DNS record: ```text theme={null} foo.example.com CNAME t1.sail.box ``` Then dial `foo.example.com:`. ## Notes * An organization can attach up to 200 total domains. If your use case needs more than that, [reach out to us](/sailboxes-getting-help). * The certificate provider we use, [Let's Encrypt](https://letsencrypt.org/docs/rate-limits/), allows 50 new certificates per apex domain every 7 days. That limit is global for your domain, not specific to Sail. It is highly recommended that you use the wildcard `_acme-challenge` verification listed in [Optional: Wildcard records](#optional-wildcard-records), and that you contact us if your use case requires large numbers of subdomains. * A domain serves one Sailbox listener at a time, but one listener can have multiple domains (for example, `foo.example.com` and `bar.example.com` can both point at the same listener) * Removing a listener also detaches all of the domains registered to it. Terminating a Sailbox stops serving its domains, but they stay attached and count toward the domain limit until you detach them or attach them to another Sailbox. * DNS records prove your organization owns a domain. If that is no longer the case, you should delete these records. * It takes up to 1 minute for Sail to obtain a valid certificate for your domain. If you make a request in the first minute after you attach a domain, you may see higher latency. * All the features of Sailbox networking still work under a custom domain: * A request to a sleeping Sailbox wakes it. * Plain HTTP requests to the domain redirect to HTTPS. * Listener allowlists keep working. # Forking Source: https://docs.sailresearch.com/sailboxes-forking Copy a Sailbox, or start a fleet of identical ones Copy an existing Sailbox. The copy gets the original's disk and its memory, so whatever was running carries on in the copy. ## Usage Take a checkpoint, then start as many Sailboxes from it as you like:
```bash CLI theme={null} sail box checkpoint --name after-setup sail box from-checkpoint --name worker-1 ``` ```python Python theme={null} checkpoint = sb.checkpoint(name="after-setup") child = sail.Sailbox.from_checkpoint(checkpoint.checkpoint_id, name="worker-1") ``` ```typescript TypeScript theme={null} const checkpoint = await sb.checkpoint({ name: "after-setup" }); const child = await Sailbox.fromCheckpoint({ checkpointId: checkpoint.checkpointId, name: "worker-1", }); ``` ```rust Rust theme={null} use sail::CheckpointOptions; let checkpoint = sb .checkpoint(CheckpointOptions { name: Some("after-setup".to_string()), ..Default::default() }) .await?; let child = client .create_from_checkpoint(&checkpoint.checkpoint_id, "worker-1", /* timeout */ None) .await?; ```
A sleeping or paused Sailbox can be copied without waking it: `checkpoint` returns its existing checkpoint. Starting several copies from one checkpoint reuses the same checkpoint data, so only the first copy pays for it. ## Fan out to many Set up one Sailbox (install dependencies, warm caches, start servers), checkpoint it, and start every worker from that checkpoint instead of repeating the setup in each one. This is the fast path to a fleet for agent rollouts, parallel test shards, or grading many submissions at once. ```python Python theme={null} import asyncio checkpoint = sb.checkpoint() async def fan_out(n: int) -> list[sail.Sailbox]: results = await asyncio.gather( *( sail.Sailbox.from_checkpoint.aio( checkpoint.checkpoint_id, name=f"worker-{i}", timeout=600, ) for i in range(n) ), return_exceptions=True, # keep the copies that came up ) return [r for r in results if isinstance(r, sail.Sailbox)] children = asyncio.run(fan_out(32)) ``` ```typescript TypeScript theme={null} const checkpoint = await sb.checkpoint(); const results = await Promise.allSettled( Array.from({ length: 32 }, (_, i) => Sailbox.fromCheckpoint({ checkpointId: checkpoint.checkpointId, name: `worker-${i}`, timeoutSeconds: 600, }), ), ); // Keep the copies that came up even if some restores fail. const children = results.flatMap((r) => r.status === "fulfilled" ? [r.value] : [], ); ``` ```rust Rust theme={null} use std::time::Duration; use sail::CheckpointOptions; let checkpoint = sb.checkpoint(CheckpointOptions::default()).await?; let names: Vec = (0..32).map(|i| format!("worker-{i}")).collect(); let results = futures::future::join_all(names.iter().map(|name| { client.create_from_checkpoint( &checkpoint.checkpoint_id, name.as_str(), Some(Duration::from_secs(600)), ) })) .await; // Keep the copies that came up even if some restores fail. let children: Vec = results.into_iter().flatten().collect(); ``` Start the copies concurrently, as above, so the restores overlap. Collect results per copy so one failed restore does not cost you the rest, give each a distinct name, and pass a timeout so a stuck restore fails that copy instead of stalling the batch. Each copy is a full Sailbox: it bills like one and runs until it sleeps or you terminate it, so clean up the fleet when the work is done. ## What a copy gets * **The disk and the memory.** Processes the original was running carry on. A command started with `exec` stops in the copy, though its writes up to the checkpoint are kept; one started in the background keeps running. Start anything else the copy needs again. * **A new identity and new networking.** Open TCP connections are reset, and the copy inherits no exposed ports; expose the ones it should serve. ## Checkpoints A checkpoint is a durable snapshot with a name, an id, and an expiry. It lasts seven days unless you set a TTL; set one when a checkpoint is a template you will keep using, so it does not expire underneath you. Starting a copy from an expired checkpoint fails.
```bash CLI theme={null} sail box checkpoint --name template --ttl-seconds 2592000 ``` ```python Python theme={null} checkpoint = sb.checkpoint(name="template", ttl_seconds=30 * 86400) print(checkpoint.expires_at) ``` ```typescript TypeScript theme={null} const checkpoint = await sb.checkpoint({ name: "template", ttlSeconds: 30 * 86400, }); console.log(checkpoint.expiresAt); ``` ```rust Rust theme={null} use std::time::Duration; use sail::CheckpointOptions; let checkpoint = sb .checkpoint(CheckpointOptions { name: Some("template".to_string()), ttl: Some(Duration::from_secs(30 * 86400)), }) .await?; println!("{:?}", checkpoint.expires_at); ```
Checkpoints also protect the original. Take one after important setup, such as installing packages or fetching data: if the machine under a Sailbox fails, Sail restores it from the most recent completed checkpoint and does not replay commands that ran before it. # Getting Help Source: https://docs.sailresearch.com/sailboxes-getting-help Where to ask questions and report problems with Sailboxes The fastest way to get help is the Sail community Slack, where you can contact the Sail team directly. Ask questions, report problems, and share what you are building. ## What to include When something goes wrong, a message with these details gets a faster answer: * The Sailbox id, and the app it belongs to. * What you ran: the SDK call or CLI command, with the language and SDK version. * What happened: the full error message, or what you expected and what you saw instead. * Roughly when it happened, with a timezone. # HTTPS API Source: https://docs.sailresearch.com/sailboxes-http-api Create and operate Sailboxes over plain HTTPS Sailboxes have a public HTTPS API. The SDKs and the CLI are built on it, and you can call it directly from any language, or from `curl`. Every endpoint is listed under [Reference → Sailbox → HTTP API](/api-reference/lifecycle/create-a-sailbox); this page covers what applies to all of them. ```text theme={null} https://sailbox-api.sailresearch.com/v1 ``` The apps endpoints live at `https://api.sailresearch.com/v1`. Your key works on both. ## Authentication Send your API key as a bearer token on every request. Create keys in the [dashboard](https://app.sailresearch.com); a key belongs to one organization and only ever sees that organization's Sailboxes. ```bash theme={null} curl https://sailbox-api.sailresearch.com/v1/whoami \ -H "Authorization: Bearer $SAIL_API_KEY" ``` ```json theme={null} { "org_id": "org_1a2b3c", "user_id": "user_9z8y" } ``` `user_id` is the member the key belongs to, or `null` for a key that belongs to the organization rather than a person. Compare it with a Sailbox's `created_by_user_id` to tell your Sailboxes from a teammate's. A private Sailbox can only be operated by the user whose key created it; an org admin can override some operations by sending an `X-Sail-Owner-Override-Reason` header, which is recorded in the audit log. See [Access Control](/sailboxes-access-control). ## Example Every Sailbox belongs to an app. Get an app id, then create a Sailbox in it: ```bash theme={null} export SAIL_API_KEY="sk_..." curl -X POST https://api.sailresearch.com/v1/apps/find \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "batch-jobs", "mint_if_missing": true}' ``` ```json theme={null} { "id": "app_0f6a2c31-8b4d-4e7a-9c15-2d8e6f4a1b03", "name": "batch-jobs", "created_at": 1753027200 } ``` ```bash theme={null} curl -X POST https://sailbox-api.sailresearch.com/v1/sailboxes \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Idempotency-Key: create-worker-1-attempt-1" \ -H "Content-Type: application/json" \ -d '{ "app_id": "app_0f6a2c31-8b4d-4e7a-9c15-2d8e6f4a1b03", "name": "worker-1", "size": "m", "image": { "base": "BASE_IMAGE_DEBIAN" } }' ``` ```json theme={null} { "sailbox_id": "sb_9c8f1e2a-3b4d-4f5a-8c7e-1d2f3a4b5c6d", "status": "running" } ``` Three things to know about that create: * **It blocks until the Sailbox is up**, which can take a few minutes while it waits for a machine. Set a generous client timeout. * **Read `status`.** A create that is accepted and then cannot bring the machine up still returns 200, with `status` set to `failed` and `error_message` saying why. * **The `Idempotency-Key` makes it safe to retry.** Send the same key and body again and you get the first answer back instead of a second Sailbox. Use a fresh key for every Sailbox you mean to create. See [Retrying safely](#retrying-safely). Terminate it when you are done: ```bash theme={null} curl -X POST https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/terminate \ -H "Authorization: Bearer $SAIL_API_KEY" ``` ## Run commands on a Sailbox Send `command` as a shell string or an argument array. A string supports `cwd` and `background`; an array runs the program directly. ```bash theme={null} curl --no-buffer -X POST \ "https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/exec" \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"command":["sh","-c","printf output; printf error >&2"],"timeout":30}' ``` The response streams newline-delimited JSON. Output bytes are base64 so every byte value is safe in JSON, and the last event carries the exit code: ```jsonl theme={null} {"type":"started","exec_request_id":"exec_123"} {"type":"stdout","data":"b3V0cHV0","seq":1} {"type":"stderr","data":"ZXJyb3I=","seq":1} {"type":"exit","status":"succeeded","return_code":0} ``` A `heartbeat` event arrives every 30 seconds while the command runs. If the connection drops before `exit`, call `POST .../exec/$EXEC_ID/wait` with the id from the `started` event to get the result and a bounded tail of the output. A failure after `started` ends the stream with an `error` event whose `error_code` is a lowercase category such as `unavailable` or `permission_denied`. * To write standard input, set `open_stdin` or `pty` when you start the exec, then `PUT .../exec/$EXEC_ID/stdin?offset=N&eof=true` with the raw bytes. Writes carry their byte offset, so an overlapping retry does not duplicate input; the response reports `accepted_through`. * To reconnect to a live exec, send the same `idempotency_key` with the highest `seq` you received for stdout and stderr. Reconnect is best-effort and does not guarantee exact replay. An exec id that does not fit a URL segment goes in the `exec_request_id` query parameter with `-` in the path. * The idempotency key can be up to 256 KiB of UTF-8, trimmed. Environment variable names match `[A-Za-z_][A-Za-z0-9_]*`, and names and values cannot contain NUL. The encoded request body can be up to 25 MiB; after decoding, 4 MiB. * The reference also lists cancel, PTY resize, and PTY resync. ## Move files Files stream in both directions without being buffered whole. ```bash theme={null} # Upload curl -X PUT --data-binary @local.bin \ "https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/files?path=/workspace/input.bin&mode=420" \ -H "Authorization: Bearer $SAIL_API_KEY" \ -H "Content-Type: application/octet-stream" # Download curl --fail-with-body \ "https://sailbox-api.sailresearch.com/v1/sailboxes/$SAILBOX_ID/files?path=/workspace/input.bin" \ -H "Authorization: Bearer $SAIL_API_KEY" \ --output local.bin ``` `mode` is decimal, 0 through 511, and `create_parents` defaults to true. A complete retry of an upload replaces the file safely; an interrupted one leaves the target unconfirmed. A download's `X-Sail-File-Mode` header carries the mode, and `Content-Length` may be absent, so read until the response ends. For directories, run `mkdir`, `find`, `tar`, and `rm` through the command endpoint, which is what the SDKs do. ## What needs an SDK Two things happen outside this API: * **Building an image** with your own packages or files. Creating a Sailbox over HTTPS needs an image that is already built: a base image, or one an SDK built earlier from the same `image` block. Asking for an unbuilt image returns 409. * **Turning on SSH** inside a Sailbox for the first time. After that the rest is HTTPS. Everything else, from the whole lifecycle to ports, custom domains, secrets, policies, metrics, and spend, is available over HTTPS. Volumes are in alpha, so those endpoints can still change. ## Troubleshooting Failures come back as an HTTP status and one JSON shape: ```json theme={null} { "error": { "message": "app_id is required", "type": "invalid_request_error", "param": null, "code": null } } ``` Match on the status and `type`; `message` is for people and can change. | Status | `type` | What to do | | ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------ | | 400 | `invalid_request_error` | Fix the field it names. Custom-domain registration is the exception: it answers 400 until your DNS resolves. | | 401 | `authentication_error` | The key is missing or invalid. | | 402 | `billing_error` | Add credits, then retry. | | 403 | `permission_error` | The key cannot do this, or the Sailbox is private and someone else's. | | 404 | `not_found_error` | No such Sailbox, volume, or checkpoint in your organization. | | 409 | `conflict_error` | Conflicts with current state. Some clear on their own, such as a volume a terminating Sailbox still mounts. | | 413 | `invalid_request_error` | The body is too large. Most endpoints cap at 64 KiB; `POST /sailboxes` and `POST /apps/find` allow 256 MiB. | | 429 | `rate_limit_error` | With `Retry-After`, the request never ran: wait and retry. Without it, the request ran and hit a limit. | | 500, 503, 504 | `server_error` | Retry with backoff. A proxy can also return a 502 or 504 with no body; treat those the same. | ## Retrying safely Every `POST` that creates or changes a Sailbox, a listener, or a volume takes an `Idempotency-Key` header. Generate one key per logical operation, any unique string up to 255 bytes, and send it on the first attempt and every retry. A retry with the same key, method, path, and byte-identical body gets the first response back, marked `Idempotent-Replayed: true`, instead of running again. * A key is remembered for at least 24 hours and is scoped to the API key that sent it. * Sail remembers 400 and 409 answers too, so after fixing a request send it under a fresh key. Reusing a key for a different request returns 409. * If the original is still running when the retry arrives, the retry waits for it. After 30 seconds it gets a 504; retry again with the same key. * A 500, 503, or 504 usually means nothing happened and the same key runs the request again. Creating a Sailbox is the case to watch: the error can arrive after the Sailbox exists, and a retry can leave you with two. List your Sailboxes, then continue under a fresh key. * Terminating a terminated Sailbox, creating a volume that already exists, and registering a domain the same way twice are safe without a key. Domain registration ignores the header. * `GET /sailboxes` pages with `limit` (up to 100) and `offset`; stop when `has_more` is false. `app`, `status`, and `search` filter, and `manageable_by_caller=true` hides private Sailboxes you cannot operate. * Resume returns 200 either way and reports `resume_state`: `running`, `already_running`, or `terminal_unavailable`, in which case `error_message` says why and you should create a new Sailbox. * `status` and `resume_state` are open sets and responses grow new fields. Match the values you care about and ignore the rest. * For its first ten minutes a new organization is capped on requests in flight; over it you get a 429 with `Retry-After`. # Custom images Source: https://docs.sailresearch.com/sailboxes-images Customize what a Sailbox boots with ## Just use the Sailbox Create a Sailbox and set it up the way you would any Linux machine: copy files in with `sail box cp`, run commands with `sail box exec`, or [enable SSH](/sailboxes-access-control) and use `scp` and `rsync`. When it looks right, checkpoint it. Every Sailbox you start from that checkpoint boots with the same disk, and the same memory, already in place:
```bash theme={null} sail box checkpoint --name after-setup sail box from-checkpoint --name worker-1 ```
This is the fastest way to get many identical environments, and it needs no image at all. See [Forking](/sailboxes-forking). ## Use a container image you already have Point `Sailbox.create` at an image on a public registry. Sail pulls it and layers what a Sailbox needs on top. ```python Python theme={null} import sail image = sail.Image.from_registry("myorg/my-custom-image:latest") sb = sail.Sailbox.create(app=app, name="custom", image=image) ``` ```typescript TypeScript theme={null} import { Image, Sailbox } from "@sailresearch/sdk"; const image = Image.fromRegistry("myorg/my-custom-image:latest"); const sb = await Sailbox.create({ app, name: "custom", image }); ``` ```rust Rust theme={null} use sail::imagebuild::ImageDefinition; let image = ImageDefinition { oci_ref: Some("myorg/my-custom-image:latest".to_string()), ..Default::default() }; ``` Write the reference as you would for `docker pull`. The image must be Debian- or Ubuntu-based and publicly pullable from `docker.io`, `ghcr.io`, `public.ecr.aws`, or `quay.io`. Private registries are not supported. ### How Sail treats your image * **`ENV`, `WORKDIR`, and `USER` become the defaults** for every command you run in the Sailbox. Commands run as the image's `USER` when it sets one and as root otherwise. Pass `user="0:0"` on a call to run as root anyway. * **`ENTRYPOINT` and `CMD` are not run.** A Sailbox manages its own processes; your commands say what to execute. * **The image keeps its own `python3`.** Sail never installs another Python over it, because a pinned interpreter would shadow the one the image was built around. * **A few paths are Sail's.** The build replaces `/init` and some Sail-owned files under `/usr/local/bin`, and writes configuration under `/etc/sailbox` and at `/etc/profile.d/sailbox-env.sh`. Everything else is left alone. * **The Sailbox runs on the architecture the image was built for.** An image published for both amd64 and arm64 runs on amd64; pass `architecture` to require one. ## Build one from a Dockerfile Already have a Dockerfile? Sail builds it for you. ```python Python theme={null} image = sail.Image.from_dockerfile("./Dockerfile", context_dir=".") ``` ```typescript TypeScript theme={null} const image = Image.fromDockerfile("./Dockerfile", { contextDir: "." }); ``` ```rust Rust theme={null} use std::collections::HashMap; use sail::imagebuild::{DockerfileInput, DockerfileSource, ImageDefinition}; let image = ImageDefinition { dockerfile: Some(DockerfileSource { dockerfile: DockerfileInput::Path("./Dockerfile".into()), context_dir: Some(".".into()), build_args: HashMap::new(), ignore: Vec::new(), }), ..Default::default() }; ``` `context_dir` is where `COPY` and `ADD` read from, with `.dockerignore` honored. Every `FROM` and `COPY --from` must name a public image on one of the registries above, and the result must be Debian- or Ubuntu-based. When a step fails, the error includes that step's output. * Pass a path or the Dockerfile text itself (`contents=` in Python, `{ contents }` in TypeScript, `DockerfileInput::Contents` in Rust). * The build runs for amd64 unless you pass `architecture`. `build_args` fill `ARG` instructions like `--build-arg`. Names starting with `BUILDKIT_` and Docker's proxy variables (`HTTP_PROXY` and friends) are rejected; a `RUN` step can set a proxy for itself. * A `Dockerfile.dockerignore` next to the Dockerfile replaces the context's `.dockerignore`, and `ignore` patterns you pass win over both. Python snapshots the context when you call `from_dockerfile`; TypeScript and Rust do it when the image is built. Edits after that point do not reach the build. * The context keeps file modes, empty directories, and symlinks. Hard links arrive as separate files. Setuid, setgid, and sticky bits, named pipes, device nodes, and mode `000` entries are rejected; sockets are skipped. * Up to 25 different images per Dockerfile across `FROM` and `COPY --from`. * Multi-stage builds, `tmpfs` mounts, and `bind` mounts from the context or another stage work. `RUN --mount` of type `cache`, `secret`, or `ssh`, a `bind` mount whose `from` names another image, mount options that are variable references, and `ONBUILD` (in your file or a base image) are rejected. * A `# syntax=` line may declare `docker/dockerfile:1` or a release from 1.4 through 1.22.0. Anything else is rejected. The line does not change how the file is built. ## Build one in code No Dockerfile? Start from Sail's Debian base and chain the steps you need. Each step returns a new definition, so one base can serve several variants. ```python Python theme={null} image = ( sail.Image.debian_amd64 .apt_install("git", "curl") .pip_install("requests") .add_local_dir("./app", "/opt/app", ignore=["*.pyc", "__pycache__/"]) .add_local_file("./config.json", "/etc/app/config.json", mode=0o600) .run_commands("python3 -m pip show requests >/tmp/requests.txt") .env({"APP_ENV": "production"}) ) ``` ```typescript TypeScript theme={null} const image = Image.debian("amd64") .aptInstall("git", "curl") .pipInstall("requests") .addLocalDir("./app", "/opt/app", { ignore: ["*.pyc", "__pycache__/"] }) .addLocalFile("./config.json", "/etc/app/config.json", { mode: 0o600 }) .runCommand("python3 -m pip show requests >/tmp/requests.txt") .env({ APP_ENV: "production" }); ``` ```rust Rust theme={null} use std::collections::HashMap; use sail::{BaseImage, ImageArchitecture}; use sail::imagebuild::{ImageDefinition, ImageDefinitionStep}; let image = ImageDefinition { base: Some(BaseImage::Debian), architecture: ImageArchitecture::Amd64, env: HashMap::from([("APP_ENV".to_string(), "production".to_string())]), steps: vec![ ImageDefinitionStep::AptInstall(vec!["git".into(), "curl".into()]), ImageDefinitionStep::PipInstall(vec!["requests".into()]), ImageDefinitionStep::AddLocalDir { local_path: "./app".into(), remote_path: "/opt/app".into(), ignore: vec!["*.pyc".into(), "__pycache__/".into()], ignore_file: None, }, ImageDefinitionStep::AddLocalFile { local_path: "./config.json".into(), remote_path: "/etc/app/config.json".into(), mode: Some(0o600), }, ImageDefinitionStep::RunCommand( "python3 -m pip show requests >/tmp/requests.txt".to_string(), ), ], ..Default::default() }; ``` | Step | What it does | | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `apt_install(...)` | Installs Debian packages. | | `pip_install(...)` | Installs Python packages into the image's `python3`. | | `add_local_file(src, dst, mode=)` | Copies one file. `mode` sets its permissions. | | `add_local_dir(src, dst, ignore=)` | Copies a directory tree, keeping file modes and skipping symlinks. `ignore` takes gitignore-style patterns or the path of a file like `.gitignore`. | | `run_commands(...)` | Runs a shell command once, during the build. | | `env({...})` | Sets environment variables for every command run from the image. | The same steps chain onto a registry image or a Dockerfile image too. Remote paths must be absolute and cannot contain a space, `$`, `"`, or `\`. Variable names must start with a letter or `_` and contain only letters, digits, and `_`. `sail.Image.debian_amd64` and `debian_arm64` also install a Python matching your local interpreter, so that [`@sail.function`](/sailbox-sdk-images#sail-function) can run your Python functions inside the Sailbox. Use `sail.Image.debian("amd64", install_python=False)` to keep the base's stock `python3`. ## Building and caching Pass an image definition to `Sailbox.create` and Sail uploads any local files, builds the image if it has not been built before, and starts the Sailbox from it. `image_build_timeout` (`imageBuildTimeoutSeconds` in TypeScript) bounds the build, retries included. In Rust the timeout is the duration passed to `build_image_definition`. To build ahead of time instead, call `build` on the definition and pass the result to `Sailbox.create`. In Rust, `build_image_definition` is the ahead-of-time build. * **Builds are cached by content, per organization.** The same base, steps, variables, and file contents reuse the existing image. * **Tags are pinned, per organization.** The first build from a tag such as `python:3.13`, or from a `FROM` line, pins the version the tag pointed at, and later builds keep it even after the tag moves upstream. Pass `force_build` (`forceBuild` in TypeScript, `BuildMode::ForceBuild` in Rust) to look the tag up again. That moves the pin for the whole organization; Sailboxes that already exist keep the version they started on. A digest (`name@sha256:...`) never moves. * **The first build of a large image downloads all of it.** Later builds usually reuse its layers. After a build, and periodically while an image is in use, Sail boots it outside any Sailbox to capture a start snapshot. Sailboxes created from the image resume from that snapshot instead of cold-booting, which keeps starts fast at every size. This makes two things part of the image contract: * **Boot-time initialization runs during every hidden boot.** systemd units and init scripts must be safe to run repeatedly, outside any Sailbox. Your entrypoint and the commands you run in a Sailbox never run during a hidden boot. * **State written at boot is shared.** Whatever boot leaves on disk or in memory is in the snapshot every Sailbox resumes from. Generate per-instance identity (machine IDs, nonces, cached credentials) at runtime, for example in your application's entrypoint, not at boot. Environment variables, networking, and Sail-managed credentials are applied per Sailbox after the resume, so they behave the same either way. # Migrate to Sailboxes Source: https://docs.sailresearch.com/sailboxes-migrating Move an app, an agent, or a dev environment onto Sailboxes Sailboxes are just Linux VMs. Anything that runs on a Linux machine runs in a Sailbox, so most migrations are a matter of getting your code and your data onto one and exposing the ports you need. ## Let your agent do it The fastest way to migrate is to hand the job to your coding agent. Give it three things: 1. **A way in.** Install the [Sail CLI](/reference/cli) on the machine the agent runs on and sign in with `sail auth login`. The agent can then create Sailboxes, run commands with `sail box exec`, and copy files with `sail box cp`. If the agent prefers SSH, run `sail box ssh enable ` and it can use `ssh .sail`, `scp`, and `rsync` as usual. 2. **The docs.** Point it at [https://docs.sailresearch.com/llms.txt](https://docs.sailresearch.com/llms.txt), or connect it to the docs MCP server at `https://docs.sailresearch.com/mcp`. See the [AI Quickstart](/ai-quickstart) for the setup in Claude Code, Codex, Cursor, and other tools. 3. **A goal.** Say what should be true when it is done. A prompt like this is enough to start: ```text theme={null} Migrate this project to run in a Sailbox on Sail (https://sailresearch.com). Read https://docs.sailresearch.com/llms.txt first. Use the sail CLI to create a Sailbox, get the code and dependencies onto it, run the app, and expose the port it listens on. Report the public URL and anything you could not move. ``` Using Claude Code or Codex? Install the [Sail skills](/ai-quickstart) and ask your agent to "Migrate this app to Sail". The `sail-migrate` skill walks through the whole migration, including moving sandboxed execution onto Sailboxes. ## Migrating a web app Expose the port your app listens on when you create the Sailbox, and Sail gives it a public HTTPS URL. Nothing inside the Sailbox needs to know about TLS or hostnames.
```bash CLI theme={null} sail box create --app my-app --name web --port 8000 sail box address 8000 ``` ```python Python theme={null} import sail app = sail.App.find(name="my-app", mint_if_missing=True) sb = sail.Sailbox.create(app=app, name="web", ingress_ports=[8000]) listener = sb.wait_for_listener(8000, timeout=60) print(listener.endpoint.url) ```
If your app is containerized, `docker` and `docker compose` work inside a Sailbox. To bake your container into the Sailbox itself instead of running Docker inside it, build the Dockerfile into a Sailbox image with [`Image.from_dockerfile`](/sailboxes-images#build-one-from-a-dockerfile). The Sailbox then boots straight into your environment. To serve the app on your own hostname, see [Custom Domains](/sailboxes-custom-domains). ## Migrating a dev environment Enable SSH on the Sailbox and use it like any remote machine: your editor's remote mode, `scp`, `rsync`, and port forwarding all work.
```bash theme={null} sail box ssh enable ssh .sail ```
SSH is organization-scoped. Anyone in your org can connect to an org-visible Sailbox with a short-lived certificate for their own key, so there are no per-machine keys to hand out. Create the Sailbox with `--visibility private` when only you should be able to reach it. For a quick shell without SSH, `sail box shell` opens a terminal over the same channel the CLI uses for commands and needs no open port. ## Things that work differently * **You pay for what the Sailbox uses, not what it could use.** Billing follows actual CPU, memory, and disk usage. An idle Sailbox sleeps on its own and wakes the moment something needs it, with its processes and memory intact. See [Autosleep](/sailboxes-autosleep) and [Billing](/sailboxes-billing). There is no need to tear environments down to save money. * **Secrets can stay outside the Sailbox.** Instead of copying API keys into the environment, [Credential injection](/sailboxes-credentials) adds them to outbound HTTPS requests on the way out, so code running in the Sailbox never sees them. * **A working environment can be cloned.** Once the migration is done, take a checkpoint and start as many copies as you need from it. See [Forking](/sailboxes-forking). * **Sailboxes are persistent.** There is no fixed runtime limit. A Sailbox keeps its disk for its whole life, so long-running agents and stateful services do not need to be rebuilt between sessions. Stuck on something? See [Getting Help](/sailboxes-getting-help). # Network policy Source: https://docs.sailresearch.com/sailboxes-network-policy Choose what a Sailbox can reach: the internet, nothing, or only the destinations you list A network policy controls the connections a Sailbox opens. It is one of: * **Public**, the default. The Sailbox can reach the internet. * **No network.** The Sailbox cannot reach anything, and nothing can reach it. * **Allowlist.** The Sailbox can reach only the destinations you list. The policy is chosen when the Sailbox is created and lasts for its whole life. A Sailbox created from a checkpoint keeps the policy of the one it came from, and a Sailbox that already exists keeps the policy it was created with. Whatever its policy, a Sailbox reaches the internet over TCP and IPv4 only. Running commands is unaffected by any policy: `exec` and the shell reach the Sailbox over a Sail-internal path, not its network. Platform features the Sailbox was created with, such as a mounted volume, reach their storage the same way and keep working. For the HTTPS requests a policy allows, [HTTP policies](/sailboxes-credentials#policies) can change or forward them, and [credential injection](/sailboxes-credentials) can add a credential. Connections into a Sailbox are covered in [Networking](/sailboxes-networking). ## No network A no-network Sailbox is cut off from other hosts and the internet: * It cannot open outbound connections, and name resolution does not work. * It cannot expose inbound services. Exposing a port or enabling SSH is rejected. ```python Python theme={null} import sail app = sail.App.find(name="web-demo", mint_if_missing=True) box = sail.Sailbox.create( app=app, name="offline-job", network_policy=sail.NetworkPolicy.NO_NETWORK, ) ``` ```typescript TypeScript theme={null} import { App, Sailbox } from "@sailresearch/sdk"; const app = await App.find("web-demo", { mintIfMissing: true }); const box = await Sailbox.create({ app, name: "offline-job", networkPolicy: "no_network", }); ``` ```rust Rust theme={null} use sail::{CreateSailboxRequest, NetworkPolicy}; let app = client.find_app("web-demo", /* mint_if_missing */ true).await?; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "offline-job".into(), network_policy: NetworkPolicy::NoNetwork, ..Default::default() }, /* timeout */ None, ) .await?; ``` ## Allowlist An allowlist lets a Sailbox reach some destinations but not the rest of the internet: ```python Python theme={null} import sail app = sail.App.find(name="web-demo", mint_if_missing=True) box = sail.Sailbox.create( app=app, name="limited-job", network_policy=sail.NetworkAllowlist( [ "api.example.com", "*.internal.example.com", "203.0.113.0/24", ] ), ) ``` ```typescript TypeScript theme={null} import { App, Sailbox } from "@sailresearch/sdk"; const app = await App.find("web-demo", { mintIfMissing: true }); const box = await Sailbox.create({ app, name: "limited-job", networkPolicy: { mode: "allowlist", allowedHosts: [ "api.example.com", "*.internal.example.com", "203.0.113.0/24", ], }, }); ``` ```rust Rust theme={null} use sail::{CreateSailboxRequest, NetworkPolicy}; let app = client.find_app("web-demo", /* mint_if_missing */ true).await?; let sb = client .create_sailbox( &CreateSailboxRequest { app_id: app.id, name: "limited-job".into(), network_policy: NetworkPolicy::Allowlist(vec![ "api.example.com".into(), "*.internal.example.com".into(), "203.0.113.0/24".into(), ]), ..Default::default() }, /* timeout */ None, ) .await?; ``` ### Entries Each entry is one of: * A hostname, such as `api.example.com`. * A `*.` wildcard hostname. `*.example.com` matches `api.example.com`. It does not match `example.com` or `a.b.example.com`. * An IPv4 address, such as `203.0.113.7`. * An IPv4 range in CIDR form, such as `203.0.113.0/24`. The range must start at its first address (`1.2.3.0/24`, not `1.2.3.5/24`). The list follows these rules: * At least one entry, and at most 128. * An entry names a destination, not a port. An allowed destination is reachable on every port, and an entry with a port is rejected. * IPv6 entries are rejected, because a Sailbox does not reach the internet over IPv6. * An address no Sailbox could ever reach, such as `127.0.0.1` or `169.254.0.0/16`, is rejected. * A private range such as `10.0.0.0/8` is accepted but allows nothing, because a Sailbox cannot reach private addresses. * A list that breaks these rules fails the create call with `InvalidArgumentError` (`InvalidArgument` in Rust) before a Sailbox is created. * Entries are stored lowercased, without a trailing dot, and without duplicates. `get` returns that stored list. ### What an entry allows An allowlist limits only the connections the Sailbox opens. Connections into it are unaffected, so an allowlist Sailbox can still expose ports and enable SSH. | To reach | List | | ------------------------------------------------------------------- | --------------------------------------------------------------- | | A web site or HTTP API over HTTPS or plain HTTP/1, by name | Its hostname, or a wildcard that matches it | | Any other server the connection opens with a TLS handshake, by name | Its hostname, or a wildcard that matches it | | An SSH server, a database, or any other server, by name | Its IP address or range, plus its hostname so the name resolves | | Any server, by IP address | Its IP address or range | A hostname entry allows only a connection whose first bytes announce the name: a plain HTTP/1 request, or a TLS handshake that carries the server name. A connection to a listed host that starts any other way, such as SSH or a database connection, is closed. List the server's address or range for those. Name resolution follows the list. An allowlist Sailbox can resolve only names that a hostname or wildcard entry covers; any other lookup fails immediately, so a list of only addresses and ranges resolves nothing. An [HTTP policy](/sailboxes-credentials#policies) that forwards a request to another host is checked against the allowlist too. The forward is allowed when a hostname or wildcard entry covers that host, or an address or range entry covers the address it resolves to. ### How Sail checks a name When a hostname entry allows a connection, Sail resolves that name itself and connects to the result, not to an address the Sailbox chose, so an override in the Sailbox's `/etc/hosts` does not redirect the connection. Sail checks the name a connection announces, not what the server does with it. If an allowed server also serves other sites, as a shared CDN does, a request can reach those sites through it. ### Blocked connections A connection the list does not allow is not refused when it opens. It opens, and Sail then closes it, so a program sees the failure on its first read or write rather than as a refused connection. ## Limitations An address or range entry allows a connection from its destination address alone, before Sail reads anything from it. These limits apply to a connection that depends on a hostname or wildcard entry instead. * A hostname entry covers only a connection whose first bytes name the server: a plain HTTP/1 request with a `Host` header, or a TLS handshake that carries the server name. A protocol that switches to TLS after it starts (STARTTLS), plaintext HTTP/2, and a TLS client that omits the server name or hides it with Encrypted Client Hello (ECH) are closed. * Sail reads at most the first 8 KiB of a plain HTTP request to find the `Host` header. A request whose headers run past that is closed even when `Host` came first. * Sail waits about five seconds for a connection to announce its destination. A connection that sends nothing in that time is closed, including a client that waits for the server to speak first. # Sailbox Pricing Source: https://docs.sailresearch.com/sailboxes-pricing Observed usage billing dimensions and rates for Sailboxes Sailboxes bill for observed usage: the CPU, memory, and disk a Sailbox actually uses while running, plus a one-time charge when it is created. How that works, what each size includes, and where to see your spend are on the [Billing overview](/sailboxes-billing). These are the rates: | Dimension | Price | | ------------------------------------------------------------------------------------- | -------- | | Used vCPU/hour | \$0.015 | | Used RAM (GiB)/hour | \$0.008 | | Used NVMe disk (GiB)/hour | \$0.0007 | | S Sailbox creation | \$0.005 | | M Sailbox creation | \$0.01 | | L Sailbox creation | \$0.012 | Usage accrues only while a Sailbox is running. Sleeping, paused, checkpointing, and cold-starting time is not sampled and not billed. Usage is sampled about every 15 seconds. # Tinker Source: https://docs.sailresearch.com/sdk-tinker Drive Sail inference from Tinker loops with sail.SailTokenCompleter, and run rollout commands on Sailboxes with sail.TinkerSandbox The `sail.tinker` helpers let you drive Sail inference from a [Tinker](https://tinker-docs.thinkingmachines.ai/) RL or training loop. They bridge Tinker's token-level sampling interface to Sail's raw-token Responses path, so rollouts run against Sail-hosted models (optionally with a LoRA adapter) while logprobs flow back into your training code. These helpers require `tinker-cookbook` installed alongside `sail`. Constructing a `SailTokenCompleter` without `tinker-cookbook` available raises [`sail.InferenceError`](/voyages-sdk-errors). ## sail.SailTokenCompleter A Tinker `TokenCompleter` backed by Sail's raw-token Responses API: each call sends the prompt token ids via the `raw_prompt_tokens` request parameter, which skips server-side chat templating and tokenization and forwards the ids verbatim to the model. Construct one with a model and sampling settings, then `await` it on tokenized prompts to get sampled tokens and their logprobs. ```python theme={null} import asyncio import sail from tinker import types async def main() -> None: completer = sail.SailTokenCompleter( model="moonshotai/Kimi-K2.6", max_tokens=256, temperature=0.7, completion_window="balanced", ) # Prompt token ids from your tokenizer, wrapped in Tinker's ModelInput. model_input = types.ModelInput.from_ints(tokens=[9906, 11, 1917, 0]) result = await completer(model_input) print(result.tokens) # list[int] of sampled token ids print(result.maybe_logprobs) # list[float] | None print(result.stop_reason) # e.g. "stop", "length" asyncio.run(main()) ``` ### Constructor ```python theme={null} SailTokenCompleter( *, model: str, max_tokens: int, temperature: float = 1.0, top_p: float = 1.0, completion_window: str = "balanced", lora: str | None = None, tinker_lora_signed_url: str | None = None, adapter_config: Mapping[str, Any] | str | None = None, tinker_lora_name: str | None = None, metadata: Mapping[str, str] | None = None, timeout: float | None = None, poll_timeout: float | None = 1200.0, voyage: sail.Voyage | None = None, request_logprobs: bool = True, ) ``` | Parameter | Description | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | **Required.** Model id to sample from. Raises `ValueError` if empty. | | `max_tokens` | **Required.** Max tokens to generate; must be `> 0` (`ValueError` otherwise). | | `temperature` | Sampling temperature. Default `1.0`. | | `top_p` | Nucleus sampling cutoff. Default `1.0`. | | `completion_window` | Completion window for each request. Default `"balanced"`. K2.6 LoRA requests use `balanced` by default; `flex` is available for background work and `priority` remains accepted for existing clients. `"asap"` is not available for LoRA requests. | | `lora` | Name of a Sail-registered LoRA adapter to apply. Mutually exclusive with `tinker_lora_signed_url`. | | `tinker_lora_signed_url` | Signed URL to a Tinker checkpoint archive (see [`get_tinker_checkpoint_signed_url_async`](#get-tinker-checkpoint-signed-url-async)). Requires `adapter_config`. Mutually exclusive with `lora`. | | `adapter_config` | LoRA adapter config as a mapping or JSON string. **Required** when `tinker_lora_signed_url` is set. | | `tinker_lora_name` | Optional human-readable name attached to the Tinker LoRA. | | `metadata` | Extra string metadata forwarded on each request. | | `timeout` | Per-request timeout in seconds. | | `poll_timeout` | Seconds to wait for a sampled result before giving up. Default `1200` (20 minutes); `None` waits indefinitely. | | `voyage` | A [`sail.Voyage`](/voyages-sdk) to attribute requests to a voyage. | | `request_logprobs` | Whether to request logprobs from the server. Default `True`. | Passing both `lora` and `tinker_lora_signed_url`, or setting `tinker_lora_signed_url` without `adapter_config`, raises `ValueError`. ### `async __call__(model_input, stop=None)` ```python theme={null} async def __call__(model_input, stop=None) -> TokensWithLogprobs ``` * `model_input`: must expose a callable `.to_ints()` returning the prompt token ids (this is Tinker's `ModelInput`). A non-callable `to_ints`, a non-integer token, or an empty prompt raises `TypeError`/`ValueError`. * `stop`: optional stop condition. An `int` is wrapped as a single-element list; a tuple is converted to a list; other values pass through unchanged. Returns a Tinker `TokensWithLogprobs`: | Field | Description | | ---------------- | ---------------------------------------------------------------------------------------------------------- | | `tokens` | `list[int]`: the sampled token ids. | | `maybe_logprobs` | `list[float]` or `None`: per-token logprobs, or `None` when the response carried none. | | `stop_reason` | The model's stop reason (e.g. `"stop"`, `"length"`), falling back to the response `status` or `"unknown"`. | If the Sail response is malformed (missing or non-integer token data, or mismatched token and logprob lengths), a [`sail.InferenceError`](/voyages-sdk-errors) is raised with the offending response attached as `exc.response`. ## get\_tinker\_checkpoint\_signed\_url\_async ```python theme={null} await get_tinker_checkpoint_signed_url_async( service_client, tinker_path: str, *, ttl_seconds: int | None = None, ) -> str ``` Resolves a Tinker checkpoint path to a signed archive URL, suitable for passing as `tinker_lora_signed_url` to `SailTokenCompleter`. It resolves the path against the Tinker service client and returns the signed URL. This helper is async-only and requires a Tinker service client with async checkpoint-URL support. | Parameter | Description | | ---------------- | ------------------------------------------------------------------------- | | `service_client` | A Tinker `ServiceClient` (exposes `create_rest_client()`). | | `tinker_path` | The Tinker checkpoint path to resolve. | | `ttl_seconds` | Optional checkpoint TTL to set or extend before resolving the signed URL. | Raises [`sail.InferenceError`](/voyages-sdk-errors) if the Tinker client does not provide async checkpoint URL methods, or if the response does not contain a URL. ```python theme={null} import sail signed_url = await sail.get_tinker_checkpoint_signed_url_async( service_client, tinker_path, ttl_seconds=3600, ) completer = sail.SailTokenCompleter( model="moonshotai/Kimi-K2.6", max_tokens=256, completion_window="balanced", tinker_lora_signed_url=signed_url, adapter_config={"r": 16, "alpha": 32}, ) ``` ## sail.TinkerSandbox Runs tinker-cookbook sandbox workloads on a Sailbox. It implements the cookbook's sandbox interface, so recipes that take a sandbox execute their rollout commands in an isolated Sailbox instead of on the training machine. Each instance wraps one Sailbox for its whole life. ```python theme={null} import sail sandbox = sail.TinkerSandbox(sailbox, timeout_seconds=1800) result = await sandbox.run_command("python solve.py", workdir="/task") print(result.exit_code, result.stdout) await sandbox.cleanup() # terminates the Sailbox ``` ### Constructor ```python theme={null} TinkerSandbox( sailbox: Sailbox, *, timeout_seconds: float | None = None, ) ``` | Parameter | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sailbox` | **Required.** The Sailbox the sandbox runs in, normally created by `tinker_sandbox_factory`. The sandbox owns it: `cleanup` and lifetime expiry terminate it. Give each sandbox its own Sailbox; two sandboxes sharing one would share its filesystem and processes, and the first cleanup would terminate it for both. | | `timeout_seconds` | The sandbox's lifetime budget. Once it has elapsed, the next operation terminates the Sailbox and raises the cookbook's `SandboxTerminatedError`. Default no budget. | Requires tinker-cookbook; constructing without it installed raises `ImportError`. ### `async run_command(command, workdir=None, timeout=60, max_output_bytes=None)` Runs a shell command and returns the cookbook's `SandboxResult`. * `command`: the shell command string, run through bash. * `workdir`: directory to run in. * `timeout`: seconds before the command is killed; a killed command is reported with `metrics["timed_out"]` set. * `max_output_bytes`: keeps the first bytes of each output stream, the part the cookbook's parsers read. Without it the full output is kept, up to a large safety ceiling. A terminated or lost Sailbox raises the cookbook's `SandboxTerminatedError`; other errors from the Sailbox are reported as a result with exit code `-1`. ### `async read_file(path, max_bytes=None, timeout=60)` Reads a file into the result's `stdout`, within `timeout` seconds; without `max_bytes` the whole file is kept, up to a large safety ceiling. A missing or unreadable file is reported as a result with exit code `1` rather than raised, and so is a read that runs out of time. ### `async write_file(path, content, executable=False, timeout=60)` Writes a file into the Sailbox, within `timeout` seconds; `executable=True` marks it executable. A write that runs out of time is reported as a result with exit code `1`. ### `async send_heartbeat(timeout=30)` Checks the sandbox's lifetime budget. A Sailbox stays alive without keep-alives, so the heartbeat sends nothing; it only terminates the Sailbox and raises the cookbook's `SandboxTerminatedError` once `timeout_seconds` has elapsed. `timeout` is part of the cookbook's heartbeat signature and is unused here, since there is no request for it to bound. ### `async cleanup()` Terminates the backing Sailbox. Safe to call more than once. ## tinker\_sandbox\_factory ```python theme={null} await tinker_sandbox_factory( env_dir: str | Path, timeout_seconds: float | None = None, *, app: str | None = None, size: str = "s", image_ref: str | None = None, name_prefix: str = "tinker", ) -> TinkerSandbox ``` Creates a `TinkerSandbox` for a tinker-cookbook environment. 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. | Parameter | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `env_dir` | **Required.** The environment directory. Sail uses its parent's `[environment].docker_image` when present; otherwise it builds `env_dir / "Dockerfile"` with `env_dir` as the build context. | | `timeout_seconds` | The sandbox's lifetime budget, measured from when the sandbox exists. | | `app` | The [app](/sailbox-sdk-apps) that groups the created Sailboxes. Default `$SAIL_APP` or `"tinker"`, created on first use. | | `size` | Sailbox size (`"s"`, `"m"`, or `"l"`; see [pricing](/sailboxes-pricing)). Default `"s"`. | | `image_ref` | Registry reference overriding the `task.toml` image. Docker-style short references work (`python:3.11`). | | `name_prefix` | Prefix for the Sailbox's name. Default `"tinker"`. | The registry image or Dockerfile must produce a Debian- or Ubuntu-based filesystem. Raises `ImportError` without tinker-cookbook installed, and a `ValueError` when `image_ref`, the `task.toml` image, and the environment's `Dockerfile` are all absent. # Security & compliance Source: https://docs.sailresearch.com/security Sail's SOC 2, HIPAA, data processing, privacy, and enterprise security commitments. ## SOC 2 Type I Sail is SOC 2 Type I compliant. You can request the independent auditor report through our [Trust Center](https://trust.sailresearch.com/). ## SOC 2 Type II Sail's SOC 2 Type II compliance takes effect August 25, 2026. ## HIPAA and BAAs Enterprise customers can sign a business associate agreement (BAA) with Sail for HIPAA-regulated workloads. ## Master services agreements Enterprise contracts can include a signed master services agreement (MSA). ## Data processing Retention depends on the Sail product and the data involved. The inference retention policy below does not apply to Sailbox state and checkpoints or to security and audit records. ### Inference: Zero Data Retention by default Sail defaults to zero long-term retention of inference request and response data: * We use inference request and response data only to provide the services you request and follow your documented instructions. * We do not use inference request or response data to train, fine-tune, or improve models without your written consent. * We do not sell or share customer personal data from inference requests or responses. * Inference processing is transient and in memory, apart from temporary storage needed to run a job. * We automatically delete temporary inference data after processing and do not retain it for longer than 48 hours, subject to the exceptions in the DPA. * Our public DPA forms part of the self-service terms and written customer agreements. Read our [Data Processing Agreement](/dpa) for the complete terms. ### Sailboxes Sailboxes are stateful by design: * A Sailbox's writable disk persists for the life of the Sailbox, including across pause, sleep, resume, migration, and recovery. * Temporary data used to migrate a Sailbox is retained for no more than 24 hours. * An explicit checkpoint handle expires after seven days by default, or after the TTL set when the checkpoint is created. Expiry controls how long the handle can start a new Sailbox; it does not set a deletion deadline for data still used by a Sailbox. See [Sailbox lifecycle](/sailboxes-lifecycle) for checkpoint, pause, sleep, resume, and termination behavior. ## Data residency and regional processing By default, Sail uses service providers in multiple regions, so customer data may be processed or stored outside the United States. Enterprise customers can pin traffic to a specific geographic region; contact [support@sailresearch.com](mailto:support@sailresearch.com) to confirm the processing and storage locations available for your workload. ## Signed DPAs Enterprise customers can sign a DPA with Sail. Self-service customers are covered by our public [Data Processing Agreement](/dpa). ## Security controls Our published controls include: * Encryption of customer data at rest and in transit * Multi-factor authentication for critical services and regular employee access reviews * Automated infrastructure security scanning and vulnerability management * Audit logging, monitoring, and an incident response process * Tested business continuity and disaster recovery plans ## Trust Center Visit Sail's [Trust Center](https://trust.sailresearch.com/) to review our security controls, request compliance documents, and see our current subprocessors. ## Support Contact [support@sailresearch.com](mailto:support@sailresearch.com) with any questions. # Supercache Source: https://docs.sailresearch.com/supercache Store reusable prompt prefixes for ultra low-cost reads Supercache lets you reuse a prompt prefix for 24 hours. Write it once, then get ultra low-cost reads for later requests with the same prefix. Supercache is available with the Responses API and Chat Completions API. ## Pricing Supercache uses separate read and write prices: | Token group | Price | | ---------------- | ------------------------------------- | | Supercache read | 10% of the regular cached-input price | | Supercache write | 100 times the normal input price | Apply these multipliers to the cached-input and input prices on the [Pricing](/pricing) page. If regular cache and Supercache contain the same token, Sail uses the lower Supercache read price. Supercache writes have a high one-time cost. Use them for large prefixes that you expect to read many times during the next 24 hours. ## Write a prefix Set `metadata.supercache_write` to `"24h"`. This is the only accepted value. Replace the placeholder below with at least 1,025 tokens of reusable content. ```bash Responses API theme={null} curl -X POST https://api.sailresearch.com/v1/responses \ -H "Authorization: Bearer YOUR_SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "\n\nThe first question.", "background": true, "max_output_tokens": 16, "metadata": { "completion_window": "balanced", "supercache_write": "24h" } }' ``` ```bash Chat Completions API theme={null} curl -X POST https://api.sailresearch.com/v1/chat/completions \ -H "Authorization: Bearer YOUR_SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "messages": [ {"role": "system", "content": ""}, {"role": "user", "content": "The first question."} ], "max_completion_tokens": 16, "metadata": { "completion_window": "balanced", "supercache_write": "24h" } }' ``` An explicit write takes priority over a read. If the prefix is already stored, the request writes it again and starts a new 24-hour lifetime. ## Read a prefix Sail automatically uses the longest matching stored prefix. Replace the placeholder with the exact reusable content from the write request. ```bash Responses API theme={null} curl -X POST https://api.sailresearch.com/v1/responses \ -H "Authorization: Bearer YOUR_SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "\n\nA different question.", "background": true, "max_output_tokens": 16, "metadata": { "completion_window": "balanced" } }' ``` ```bash Chat Completions API theme={null} curl -X POST https://api.sailresearch.com/v1/chat/completions \ -H "Authorization: Bearer YOUR_SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "messages": [ {"role": "system", "content": ""}, {"role": "user", "content": "A different question."} ], "max_completion_tokens": 16, "metadata": { "completion_window": "balanced" } }' ``` A read does not extend the 24-hour lifetime. Only another explicit write starts a new 24-hour lifetime. ## Usage fields Completed Responses include two decimal-string metadata fields: ```json theme={null} { "usage": { "input_tokens": 4609, "input_tokens_details": { "cached_tokens": 3072 } }, "metadata": { "supercached_input_tokens": "2048", "supercache_write_input_tokens": "0" } } ``` * `metadata.supercached_input_tokens` is the number of tokens read from Supercache. * `metadata.supercache_write_input_tokens` is the number of tokens written to Supercache. * `usage.input_tokens_details.cached_tokens` includes both regular cached input and Supercache reads. In this example, 2,048 tokens came from Supercache and 1,024 tokens came only from regular cache. Chat Completions reports the aggregate cached count in `usage.prompt_tokens_details.cached_tokens`. It does not include the separate Supercache read and write counts. # API support matrix Source: https://docs.sailresearch.com/support What each Sail inference API supports today, and what's coming soon Sail provides inference endpoints compatible with the [OpenAI Responses API](https://developers.openai.com/api/reference/resources/responses/methods/create), the [OpenAI Chat Completions API](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create), and the [Anthropic Messages API](https://platform.claude.com/docs/en/api/messages/create). All three inference APIs accept the same [models](/models) and [completion windows](/completion-windows). Additionally, Sail offers a [Batch API](#batch-api) for running large numbers of [Responses API](#responses-api) requests efficiently in a single asynchronous job. | API | Endpoint | Maturity | | ----------------------- | --------------------------- | --------------------- | | OpenAI Responses | `POST /v1/responses` | Stable | | OpenAI Chat Completions | `POST /v1/chat/completions` | Stable | | Anthropic Messages | `POST /v1/messages` | Beta | | Batch | `POST /v1/batches` | Stable | *** ## Responses API
Recommended OpenAI SDK compatible API reference
### Supported features | Feature | Details | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Core parameters** | `model`, `input` (string or message array), `max_output_tokens`, `temperature`, `top_p`, `user`, `prompt_cache_key` | | **Instructions** | `instructions` is prepended to the input as a system message. | | **Structured outputs** | `text.format` with `type: "text"` or `type: "json_schema"` | | **Reasoning** | `reasoning.effort` (`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`; `max` selects the same top tier as `xhigh`), `reasoning.generate_summary` (`auto` / `concise` / `detailed`) | | **Function tools** | `tools` with `type: "function"` (client-side function calling with `name`, `description`, `parameters`, `strict`) | | **Custom tools** | `tools` with `type: "custom"` | | **Tool choice** | `tool_choice`: `"none"`, `"auto"`, `"required"`, or a specific function/custom tool | | **Background mode** | `background: true` returns `202` immediately; poll with `GET /v1/responses/{id}` | | **Streaming** | `stream: true` on foreground requests returns Server-Sent Events using OpenAI Responses event names. | | **Prompt cache routing** | `prompt_cache_key` is an optional routing hint for requests that share a large prompt prefix | | **Supercache** | [`metadata.supercache_write`](/supercache) stores a reusable prompt prefix for 24 hours. Matching reads are automatic. | | **Image input** | `input_image` content blocks on [multimodal models](/models). Non-multimodal models accept text only. | | **Video input** | `input_video` content blocks on models that declare video input support. The `video_url` value can be a URL string or an object with `url` and model-specific controls. | | **Output logprobs** | `include: ["message.output_text.logprobs"]` returns per-token logprobs (best effort; omitted when unavailable). Set `top_logprobs` (0-512, default 0) to also get the highest-scoring alternative tokens at each position; it requires the `include` opt-in. | ### Supercache usage When Supercache accounting data is available, completed Responses include `metadata.supercached_input_tokens` and `metadata.supercache_write_input_tokens` as decimal strings. Both fields are included when their value is zero. `usage.input_tokens_details.cached_tokens` includes tokens read from Supercache. The two metadata keys are reserved and cannot be set in a request. ### Response status and output validation Treat `completed`, `incomplete`, `failed`, and `cancelled` as terminal statuses when polling a background response. * `completed` means the response finished normally. * `incomplete` means generation stopped early. A reason of `"max_output_tokens"` is Sail's normalized truncation/cap reason: it is reported whenever generation stops because of a length-based limit, such as the request's `max_output_tokens`, the model's context window, or another provider length signal. It does not by itself prove the request's output limit alone was reached. A reason of `"content_filter"` means generation stopped because of a content filter. When present, `usage` is preserved. A response limited by `max_output_tokens` can include partial `output`; tokens reported in `usage` are billed normally. An incomplete response is not an API error. * `failed` includes generations that Sail rejects because the result does not satisfy the request after its retry attempts are exhausted. A request with `tool_choice: "required"` can fail with `error.code: "server_error"` and the message `"The model did not return a tool call required by the request."` A generation with no visible text, refusal, or tool call can fail with `error.code: "server_error"` and the message `"The model did not return visible text, a refusal, or a tool call."` instead of returning an empty completed response. * `cancelled` means no further output will be produced. Sail cannot currently guarantee `tool_choice: "required"` for `openai/gpt-oss-*` models. Choose another model when every successful response must contain a tool call. ### Not yet supported | Feature | Notes | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Conversation chaining** | `previous_response_id` and `conversation` are not supported. Send the full input each request. | | **Prompt templates** | The `prompt` parameter is not supported. | | **Server-side tools** | `web_search`, `web_search_preview`, and `image_generation` tools are accepted for OpenAI-client compatibility and removed from the request; the model has no such tool to call. `file_search`, `code_interpreter`, `computer_use`, `mcp`, `shell`, and `apply_patch` are not supported. | | **Multimodal input** | Audio and file input blocks are not supported. Image and video input are supported on models that declare those input modalities (see above). | | **Include** | Accepted for compatibility when it is an array of strings. `reasoning.encrypted_content` is accepted for OpenAI-client compatibility, but reasoning items are returned without encrypted content. Requests that include `web_search_call.action.sources`, `code_interpreter_call.outputs`, `computer_call_output.output.image_url`, or `file_search_call.results` are rejected. | | **Truncation** | Only `"disabled"` is accepted. Custom truncation strategies are not supported. | | **Parallel tool calls** | `parallel_tool_calls` is accepted for compatibility. Models decide their own tool-call cadence, so the field has no effect. | | **json\_object format** | `text.format.type: "json_object"` is not supported. Use `"json_schema"` instead. | | **Service tier** | Only `"auto"` is accepted. Use `metadata.completion_window` to control response timing instead. | | **Delete / cancel** | `DELETE /v1/responses/{id}` and cancel endpoints are not implemented. | *** ## Chat Completions API
OpenAI SDK compatible API reference
### Supported features | Feature | Details | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Core parameters** | `model`, `messages`, `max_completion_tokens`, `temperature`, `top_p`, `user`, `prompt_cache_key` | | **Message roles** | `system`, `user`, `assistant`, `tool`, `function` (deprecated), `developer` | | **Structured outputs** | `response_format` with `type: "text"`, `"json_object"`, or `"json_schema"` | | **Reasoning** | `reasoning_effort` (`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`; `max` selects the same top tier as `xhigh`) | | **Function tools** | `tools` with `type: "function"` (standard `{type, function: {name, description, parameters, strict}}` format) | | **Custom tools** | `tools` with `type: "custom"` | | **Tool choice** | `tool_choice`: `"none"`, `"auto"`, `"required"`, or a specific function/custom tool | | **Parallel tool calls** | `parallel_tool_calls` is passed through | | **Metadata** | `metadata` with string key-value pairs, including [`completion_window`](/completion-windows) and [`completion_webhook`](/webhooks) | | **Prompt cache routing** | `prompt_cache_key` is an optional routing hint for requests that share a large prompt prefix | | **Supercache** | [`metadata.supercache_write`](/supercache) stores a reusable prompt prefix for 24 hours. Matching reads are automatic. | | **Streaming** | `stream: true` returns Server-Sent Events (`chat.completion.chunk`); `stream_options.include_usage` adds a final usage chunk. Reasoning is streamed as `reasoning_content` deltas and tool calls are emitted atomically. | | **Image input** | `image_url` content parts on [multimodal models](/models). Non-multimodal models accept text only. | | **Video input** | `video_url` content parts on models that declare video input support. URL objects and model-specific controls such as `num_frames` and `fps` are preserved. | ### Not yet supported | Feature | Notes | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **Multiple choices** | `n` must be `1`. | | **Multimodal content** | Audio (`input_audio`) content parts are not supported. Image and video input are supported on models that declare those input modalities (see above). | | **Sampling controls** | `frequency_penalty`, `presence_penalty`, `logit_bias`, `stop`, `seed`, `top_logprobs`, `logprobs`, `verbosity` are not supported. | | **Audio modality** | `audio` and `modalities: ["audio"]` are not supported. | | **Predicted output** | `prediction` is not supported. | | **Web search** | `web_search_options` is not supported. | | **Service tier** | Only `"auto"` is accepted. | | **CRUD endpoints** | `GET`, `POST`, `DELETE` on stored completions are not implemented. | | **Deprecated fields** | `max_tokens`, `functions`, `function_call` are rejected. Use their modern replacements. | ### Response notes * Responses always contain exactly one choice (`n=1`). * `finish_reason` reflects the provider result when available, including `"stop"`, `"tool_calls"`, `"length"`, and `"content_filter"`. * `system_fingerprint` and `service_tier` are not included in responses. * `logprobs` is always `null`. *** ## Messages API
Anthropic Messages format Anthropic SDK compatible API reference
The Messages API is Anthropic-compatible for agentic use: system prompts, tool calling, and streaming (SSE) are supported. Prompt caching (`cache_control`) is accepted but not yet applied. ### Supported features | Feature | Details | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Core parameters** | `model`, `max_tokens`, `messages` | | **System prompt** | The top-level `system` parameter (string or array of text blocks) | | **Sampling** | `temperature` (0–1), `top_p` (0–1) | | **Tools** | `tools` and `tool_choice` (`"auto"`, `"any"`, `"tool"`, `"none"`). The model calls tools; responses include `tool_use` blocks, and text `tool_result` blocks round-trip. Tool results with `is_error: true` retain an error signal for the model. | | **Extended thinking** | `thinking` is translated to the model's reasoning | | **Streaming** | `stream: true` returns Anthropic Server-Sent Events (`message_start`, `content_block_delta`, `message_stop`, …) after generation completes. It is not incremental token delivery. | | **Structured outputs** | `output_config.format` with `type: "json_schema"` | | **Metadata** | `metadata` with string key-value pairs, including [`completion_window`](/completion-windows) and [`completion_webhook`](/webhooks) | | **Image input** | `image` content blocks on [multimodal models](/models). Non-multimodal models accept text only. | | **Request routing** | `routing.allowed_countries: ["US"]` restricts that request to United States capacity | | **Voyage attribution** | `X-Sail-Voyage-Id`, with optional span and agent headers, associates the model call with a [Voyage](/voyages-sdk) | | **Token counting** | `POST /v1/messages/count_tokens` returns the request's input token count without running the model | ### Not yet supported | Feature | Notes | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | **Prompt caching** | `cache_control` on content blocks is accepted but ignored (no cache read/write). | | **Stop sequences** | `stop_sequences` is not supported. | | **Top-K sampling** | `top_k` is not supported. | | **Multimodal content** | Document content blocks are not supported. Image input is supported on multimodal models (see above). | | **Tool result images** | Images inside `tool_result` are replaced with `[Image omitted: Sail does not yet support images inside tool results.]`. The request still succeeds. | | **Redacted thinking** | Replayed `redacted_thinking` blocks are accepted so conversations can continue, but the opaque reasoning is not forwarded to the model. | | **Service tier** | `service_tier` is not supported. | | **Inference geo** | `inference_geo` is not supported. | | **Batches** | `POST /v1/messages/batches` and related endpoints are not implemented. | ### Response notes * `stop_reason` reflects the outcome. Sail returns `"end_turn"` normally, `"tool_use"` for tool calls, `"max_tokens"` for token limits, and `"refusal"` when the provider reports a refusal. Sail returns `"model_context_window_exceeded"` when the provider reports that the model's context window was exceeded. * Responses contain `text` content blocks, plus `tool_use` blocks when the model calls a tool. * Cache-related usage fields (`cache_creation_input_tokens`, `cache_read_input_tokens`) are not included (prompt caching isn't applied yet). * Thinking output does not include an Anthropic cryptographic signature. `thinking.budget_tokens` is approximated as medium reasoning effort unless `output_config.effort` provides an explicit effort. * `thinking.type: "disabled"` uses the selected model's `none` reasoning control. Sail returns `400` when the selected model does not support disabling reasoning. The exact effect follows that model's `none` contract, so `disabled` does not bypass a model-specific lowest-effort approximation. `output_config.effort` overrides the effort level. A non-disabling override preserves thinking output requested by `thinking.type: "enabled"` or `"adaptive"`; an effort whose model mapping disables reasoning can remove it. * When reasoning is present, Messages content begins with a `thinking` block before the text block. Select content by block type rather than assuming `content[0]` is text. The Chat Completions response exposes the same reasoning through `reasoning_content`. * Token counting applies the same replay-block policy as message creation: redacted thinking is omitted and tool-result images count as the replacement text marker. * Non-streaming requests wait for up to nine minutes. If generation is still running, Sail returns a `408 timeout_error` and includes the response ID in `X-Sail-Message-Id`. Sail also returns `X-Should-Retry: false` so Anthropic SDKs do not create a second task. The original task continues. Retrieve it with `GET /v1/messages/{id}`, or use `stream: true` for long-running requests. Streaming requests use heartbeats and remain connected for up to 20 minutes. If that wait expires, the stream emits a timeout error with the response ID. The task continues and remains retrievable with `GET /v1/messages/{id}`. ### Compatibility notes * Sail accepts both the Anthropic `x-api-key` header and `Authorization: Bearer `. If both are present, `Authorization` takes precedence. ```python Python theme={null} from anthropic import Anthropic client = Anthropic( api_key="YOUR_SAIL_API_KEY", base_url="https://api.sailresearch.com", ) ``` ```typescript TypeScript theme={null} import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ apiKey: process.env.SAIL_API_KEY, baseURL: "https://api.sailresearch.com", }); ``` * The Anthropic Python and TypeScript SDKs type `metadata` with only `user_id`. Sail also accepts `completion_window`. Keep the additional cast or type assertion scoped to the `metadata` value: ```python theme={null} from typing import cast from anthropic.types import MetadataParam metadata = cast(MetadataParam, {"completion_window": "balanced"}) ``` ```typescript theme={null} metadata: { completion_window: "balanced", } as Anthropic.Messages.Metadata & { completion_window: string }, ``` * The `anthropic-version` header is not required or checked. * Errors use the Anthropic envelope `{"type":"error","error":{"type":"...","message":"..."},"request_id":"..."}` and Anthropic error types such as `invalid_request_error`, `rate_limit_error`, and `overloaded_error`. *** ## Batch API The Batch API runs large numbers of [Responses API](#responses-api) requests asynchronously. Every item targets `/v1/responses`. Batching `/v1/chat/completions` or `/v1/messages` is not currently supported. You can submit up to 100,000 requests in a single `POST /v1/batches` call, then poll `GET /v1/batches/{id}` for status and fetch each result by `custom_id`. See [Sending Requests at Scale](/requests_at_scale) for the end-to-end workflow and the [Batch API reference](/api-reference/batches-api/create-a-batch) for the request and response schemas. *** ## Cross-API behavior These behaviors apply across the inference API surfaces: * **Streaming:** The Chat Completions API supports `stream: true`, returning Server-Sent Events (`chat.completion.chunk`); set `stream_options.include_usage` for a final usage chunk. The Messages API supports `stream: true`, returning Anthropic SSE events after generation completes rather than incremental token delivery. The Responses API supports foreground `stream: true`, returning OpenAI Responses SSE events. `background: true` requests return `202` immediately and cannot be streamed; use polling or [webhooks](/webhooks) for long-running background work. * **Completion windows:** Express latency tolerance in exchange for lower token costs. See [Completion Windows](/completion-windows) and [Pricing](/pricing). * **Context windows:** For each text request, Sail reserves at least 512 tokens, or 0.5% on larger windows, from the model's published context window. The input token count plus the requested maximum output must fit in the remaining budget. This allowance covers model-specific request formatting that can add tokens when the request runs. On the non-batch Responses endpoint (`POST /v1/responses`), requests that provide `raw_prompt_tokens` skip text formatting and use the exact raw-token count without this reserve, requiring the raw token count plus `max_output_tokens` to fit within the model's context window. The Batch API does not yet apply this `raw_prompt_tokens` admission arithmetic. * **Webhooks:** Set `metadata.completion_webhook` to receive a POST when processing finishes. See [Webhooks](/webhooks). * **Response storage:** `store: false` is accepted for OpenAI compatibility, but does not change Sail's normal temporary request/response storage for processing, retries, polling, and idempotency. Customer Data remains governed by Sail's DPA retention and deletion terms. # Tinker Source: https://docs.sailresearch.com/tinker Sample from Tinker-trained LoRA checkpoints on Sail with SailTokenCompleter [Tinker](https://thinkingmachines.ai/tinker) is a training API for fine-tuning open-weight models with LoRA. Sail can run the sampling side of your Tinker training loop: `sail.SailTokenCompleter` is a drop-in [tinker-cookbook](https://github.com/thinking-machines-lab/tinker-cookbook) `TokenCompleter` that samples from your Tinker checkpoints on Sail, with no manual adapter upload step. Token IDs go in and token IDs come out. The completer sends your prompt token IDs to Sail verbatim and returns sampled token IDs with per-token logprobs, so there is no chat-template or re-tokenization drift between training and sampling. Each call creates a background Responses API request for the [completion window](/completion-windows) you choose (`balanced` by default) and polls it to completion, retrying transient failures with exponential backoff. We also have a [guide](tinker-rl) on how to use `sail.SailTokenCompleter` in a GRPO-style Tinker training loop. ## Install ```bash theme={null} pip install sail tinker tinker-cookbook export SAIL_API_KEY=sk_your_key_here export TINKER_API_KEY=your_tinker_key ``` ## Sample on Sail `SailTokenCompleter` works anywhere tinker-cookbook expects a `TokenCompleter` (i.e. RL rollouts, evals, or direct calls): ```python theme={null} import sail from tinker import types completer = sail.SailTokenCompleter( model="moonshotai/Kimi-K2.6", max_tokens=256, temperature=0.7, completion_window="balanced", ) prompt = types.ModelInput.from_ints(tokens=tokenizer.encode("Question: 2+2?\nAnswer:")) result = await completer(prompt, stop=["\n"]) result.tokens # sampled token IDs result.maybe_logprobs # per-token logprobs (None when request_logprobs=False) result.stop_reason ``` ### Parameters | Parameter | Default | Description | | ------------------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | (required) | Sail model ID. Must support LoRA serving when a LoRA source is set (see [LoRAs](/loras#supported-base-models)). | | `max_tokens` | (required) | Maximum sampled tokens per call. | | `temperature` | `1.0` | Sampling temperature. | | `top_p` | `1.0` | Nucleus sampling threshold. | | `completion_window` | `"balanced"` | [Completion window](/completion-windows) for each request. K2.6 LoRA requests use `balanced` by default; `flex` is available for background work and `priority` remains accepted for existing clients. LoRA requests cannot use `asap`; the selected window must be supported by the model. | | `lora` | `None` | Name or ID of a LoRA [uploaded to Sail](/loras). | | `tinker_lora_signed_url` | `None` | Signed Tinker checkpoint archive URL. Mutually exclusive with `lora`. | | `adapter_config` | `None` | PEFT `adapter_config.json` contents (dict or JSON string). Required with `tinker_lora_signed_url`. | | `tinker_lora_name` | `None` | Optional label for the Tinker checkpoint. | | `metadata` | `None` | Extra request metadata merged into each request. | | `timeout` | `None` | Per-HTTP-call timeout in seconds. | | `request_logprobs` | `True` | Request per-token logprobs with each sample. | The `stop` argument on the call itself accepts a string, a list of strings, or token IDs, matching the tinker-cookbook `TokenCompleter` contract. ## Sample from a Tinker checkpoint To sample from a LoRA you are training in Tinker, save sampler weights, resolve a signed archive URL, and pass both the URL and the adapter's PEFT config to the completer. Sail downloads the checkpoint archive and loads the adapter for your requests. ```python theme={null} import sail # 1. Save sampler weights for the current step save_future = await training_client.save_weights_for_sampler_async( "rl-step-7", ttl_seconds=3600, ) save_result = await save_future tinker_path = save_result.path # tinker:///sampler_weights/rl-step-7 # 2. Resolve a signed checkpoint archive URL signed_url = await sail.get_tinker_checkpoint_signed_url_async( service_client, tinker_path, ttl_seconds=3600, # optional: set/extend the checkpoint TTL ) # 3. Sample from the checkpoint on Sail completer = sail.SailTokenCompleter( model="moonshotai/Kimi-K2.6", max_tokens=256, completion_window="balanced", tinker_lora_signed_url=signed_url, adapter_config=adapter_config, # contents of the PEFT adapter_config.json tinker_lora_name="rl-step-7", ) ``` `adapter_config` is the PEFT adapter config for the LoRA Tinker is training. The same compatibility rules apply as for [uploaded LoRAs](/loras#adapter-requirements): the base model must match `model`, and the rank must be within the base model's limit. When `ttl_seconds` is passed to `get_tinker_checkpoint_signed_url_async`, the helper sets the Tinker checkpoint's TTL before resolving the URL, so per-step RL sampler checkpoints are cleaned up automatically instead of accumulating in your Tinker account. ### Using an uploaded LoRA instead If you have already [uploaded a LoRA to Sail](/loras), pass its name or ID as `lora` instead of a signed URL: ```python theme={null} completer = sail.SailTokenCompleter( model="moonshotai/Kimi-K2.6", max_tokens=256, completion_window="balanced", lora="funnier-v1", ) ``` ## Constraints * **Tinker checkpoints only apply through `SailTokenCompleter`.** The adapter is loaded on Sail's raw-token sampling path. A plain text Responses or Chat Completions request that happens to carry Tinker checkpoint metadata is served by the base model. Sample from Tinker checkpoints only via `SailTokenCompleter`. * **`lora` and `tinker_lora_signed_url` are mutually exclusive.** Pass one LoRA source per completer. * **`adapter_config` is required with `tinker_lora_signed_url`.** Sail needs the PEFT config to load the checkpoint weights. * **`model` must support LoRA serving** when a LoRA source is set (see [supported base models](/loras#supported-base-models)). * **Kimi K2.6 LoRA requests use `balanced` by default.** `SailTokenCompleter` selects it by default. `flex` is available for background LoRA and Tinker work, and `priority` remains accepted for existing clients. The `asap` completion window is not available for LoRA requests. * **Signed checkpoint URLs expire.** Resolve a fresh URL for each new checkpoint, and re-resolve if a long-running loop reuses an old one. * **tinker-cookbook must be installed.** Constructing a `SailTokenCompleter` without it raises an error; the rest of the `sail` SDK works without Tinker packages. # RL fine-tuning with Tinker Source: https://docs.sailresearch.com/tinker-rl Train a LoRA with Tinker while running every rollout on Sail This guide walks through a small GRPO-style reinforcement-learning training loop that trains a LoRA adapter with [Tinker](https://thinkingmachines.ai/tinker) while sampling every rollout from Sail. Tinker owns the optimizer; Sail serves each fresh checkpoint through [`SailTokenCompleter`](/tinker), so your rollouts run on Sail's inference fleet. The example fine-tunes Kimi K2.6 to solve grade-school math word problems, rewarding answers that land in `\boxed{}`. ## Prerequisites * Python 3.11+ * A Sail API key and a Tinker API key * The packages below ```bash theme={null} pip install sail tinker tinker-cookbook datasets export SAIL_API_KEY=sk_your_key_here export TINKER_API_KEY=your_tinker_key export HF_TOKEN=your_huggingface_token ``` ## How one step fits together Each training step is the same five moves: 1. **Snapshot** the current LoRA weights as a Tinker sampler checkpoint. 2. **Resolve** a signed URL to that checkpoint and wrap it in a `SailTokenCompleter`. 3. **Roll out** a batch of grouped completions through that completer on Sail. 4. **Score** the completions, turn them into advantages and Tinker training data. 5. **Train** one optimizer step on the Tinker client, then repeat with the updated weights. ## 1. Score a completion An *environment* renders one prompt and scores one sampled completion. tinker-cookbook calls `initial_observation` to get the prompt tokens (and stop sequences), then `step` to score the tokens Sail sampled. The reward here is 1 for a correct boxed answer, with a small penalty for missing the `\boxed{}` format. ```python theme={null} import re from dataclasses import dataclass from typing import Any import tinker from tinker_cookbook import renderers from tinker_cookbook.rl.types import StepResult QUESTION_SUFFIX = " Write your final answer in \\boxed{} format." def extract_boxed(text: str) -> str | None: match = re.search(r"\\boxed\s*\{([^{}]+)\}", text) return match.group(1) if match else None def answer_matches(completion: str, reference: str) -> bool: def norm(t: str | None) -> str: return re.sub(r"\s+", "", (t or "").strip().lower().replace(",", "")) return norm(extract_boxed(completion)) == norm(extract_boxed(reference) or reference) class MathEnv: def __init__(self, question: str, answer: str, renderer: Any) -> None: self.question = question self.answer = answer self.renderer = renderer async def initial_observation(self): convo = [{"role": "user", "content": self.question + QUESTION_SUFFIX}] return self.renderer.build_generation_prompt(convo), self.renderer.get_stop_sequences() async def step(self, action, *, extra=None): message, termination = self.renderer.parse_response(action) text = renderers.get_text_content(message) correct_format = float(extract_boxed(text) is not None and termination.is_clean) correct_answer = float(answer_matches(text, self.answer)) return StepResult( reward=correct_answer - 0.1 * (1.0 - correct_format), episode_done=True, next_observation=tinker.ModelInput.empty(), next_stop_condition=[], metrics={"correct": correct_answer, "format": correct_format}, logs={}, ) ``` A *group builder* fans one prompt out into `group_size` environments. ```python theme={null} @dataclass(frozen=True) class MathGroupBuilder: row: dict renderer: Any group_size: int async def make_envs(self): return [ MathEnv(self.row["question"], self.row["answer"], self.renderer) for _ in range(self.group_size) ] # The reward lives in MathEnv.step, so there is no extra group-level bonus. async def compute_group_rewards(self, trajectory_group, env_group): return [(0.0, {}) for _ in trajectory_group] async def cleanup(self): return None def logging_tags(self): return ["math"] ``` ## 2. Serve the latest checkpoint on Sail After each Tinker step, save the weights as a sampler checkpoint, resolve a signed archive URL, and build a `SailTokenCompleter` pointed at it. The `adapter_config` is the PEFT config matching the LoRA Tinker is training. This ready-made one fits this example (Kimi K2.6 at rank 32); save it as `tinker_adapter_config.json`. Passing `ttl_seconds` tells Tinker to expire the checkpoint, so per-step checkpoints clean themselves up instead of piling up. ```json tinker_adapter_config.json theme={null} { "base_model_name_or_path": "moonshotai/Kimi-K2.6", "bias": "none", "inference_mode": true, "lora_alpha": 32, "lora_dropout": 0.0, "peft_type": "LORA", "r": 32, "target_modules": [ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", "lm_head" ], "task_type": "CAUSAL_LM" } ``` ```python theme={null} import sail async def sail_policy(training_client, service_client, adapter_config, name): save_future = await training_client.save_weights_for_sampler_async( name, ttl_seconds=3600 ) save_result = await save_future signed_url = await sail.get_tinker_checkpoint_signed_url_async( service_client, save_result.path, ttl_seconds=3600 ) return sail.SailTokenCompleter( model="moonshotai/Kimi-K2.6", max_tokens=1024, temperature=1.0, completion_window="balanced", tinker_lora_signed_url=signed_url, adapter_config=adapter_config, tinker_lora_name=name, ) ``` ## 3. The training loop Wire it together: load the data, then each step snapshot → roll out on Sail → train. ```python theme={null} import asyncio import json import random import time from datasets import load_dataset from tinker_cookbook.rl.data_processing import assemble_training_data, compute_advantages from tinker_cookbook.rl.rollouts import do_group_rollout from tinker_cookbook.rl.train import train_step from tinker_cookbook.tokenizer_utils import get_tokenizer MODEL = "moonshotai/Kimi-K2.6" def mean_reward(groups): rewards = [float(r) for group in groups for r in group.get_total_rewards()] return sum(rewards) / len(rewards) if rewards else 0.0 def split_degenerate_groups(groups): kept = [] degenerate = [] for group in groups: rewards = [float(r) for r in group.get_total_rewards()] if rewards and len(set(rewards)) == 1: degenerate.append(group) else: kept.append(group) return kept, degenerate async def main(): adapter_config = json.load(open("tinker_adapter_config.json")) service_client = tinker.ServiceClient() training_client = await service_client.create_lora_training_client_async( base_model=MODEL, rank=32 ) renderer = renderers.get_renderer("kimi_k26", tokenizer=get_tokenizer(MODEL)) ds = load_dataset("microsoft/orca-math-word-problems-200k", split="train") train_rows = [ {"question": r["question"], "answer": r["answer"]} for r in ds.select(range(2000)) ] run_id = f"orca-math-{int(time.time())}" group_size = 4 groups_per_step = 16 # 16 prompts x 4 samples = 64 rollouts per step for step in range(7): # 1-2. Snapshot the current weights and serve them on Sail. policy = await sail_policy( training_client, service_client, adapter_config, f"{run_id}-step-{step}" ) # 3. Roll out a batch of grouped completions through Sail. rows = random.sample(train_rows, groups_per_step) builders = [MathGroupBuilder(row=r, renderer=renderer, group_size=group_size) for r in rows] groups = await asyncio.gather(*(do_group_rollout(b, policy) for b in builders)) # 4-5. GRPO-style advantages, then one Tinker optimizer step. reward = mean_reward(groups) training_groups, degenerate_groups = split_degenerate_groups(groups) degenerate_pct = 100.0 * len(degenerate_groups) / len(groups) if groups else 0.0 if not training_groups: raise RuntimeError("all rollout groups were degenerate") advantages = compute_advantages(training_groups) data, _ = assemble_training_data(training_groups, advantages) await train_step( data_D=data, training_client=training_client, learning_rate=1e-5, num_substeps=1, loss_fn="importance_sampling", metrics={}, ) print( f"Step {step:2d} | reward: {reward:.3f} | " f"degenerate: {degenerate_pct:.0f}% | datums: {len(data)}" ) asyncio.run(main()) ``` That's the whole loop. Tinker holds the optimizer state and applies each update; Sail samples every rollout from the checkpoint you just wrote. To scale the rollout batch, raise `group_size` and `groups_per_step`, and Sail runs the completions concurrently. To watch the reward climb, log the `metrics` returned by `compute_advantages`/`train_step`. ## 4. Sandboxed rollouts The math environment above only scores text, so it runs safely inside the training process. An environment that executes model-written code should not. tinker-cookbook has a seam for this: recipes that take a sandbox factory run each rollout's commands in an isolated sandbox. Pass Sail's factory and those sandboxes are Sailboxes: ```python theme={null} import functools import sail sandbox_factory = functools.partial(sail.tinker_sandbox_factory, size="s") ``` When `image_ref` is preset, the factory uses that registry image. Otherwise, it reads the `task.toml` next to the environment directory for `[environment].docker_image`. When the task does not name a registry image, Sail builds the environment's `Dockerfile` with the whole environment directory as its build context. The registry image or Dockerfile must produce a Debian- or Ubuntu-based filesystem. The factory creates a Sailbox from the selected image and returns a sandbox the cookbook drives: commands, file reads and writes, and cleanup all happen in the Sailbox, and the cookbook's timeout terminates it. It is a plain module-level function, so it survives the cookbook's pickling, and `functools.partial` keeps preset arguments pickleable too. Each group's sandboxes are created concurrently. The [Tinker reference](/sdk-tinker) documents the full sandbox API. ## Next steps * [Tinker](/tinker): the `SailTokenCompleter` reference (parameters, LoRA modes, and constraints). * [LoRAs](/loras): upload and serve a PEFT adapter directly, without a Tinker training loop. * [Completion Windows](/completion-windows): control the latency/cost tier your rollouts run on. # Trust Center Source: https://docs.sailresearch.com/trust-center # Overview Source: https://docs.sailresearch.com/usage Programmatic access to spend, usage, tokens, and latency The Usage API lets you query your Sail usage from scripts, CLIs, or dashboards, the same data shown on the dashboard in [app.sailresearch.com](https://app.sailresearch.com). It covers two families: * **Billing & cost**: combined spend, an Inference and Sailboxes product split, Sailbox line-item spend, credit balance, burn rate, days remaining, per-model cost rankings, per-API-key inference usage, and inference token counters. * **Operational activity & latency**: request counts and time series, recent requests, task activity, and turn/trajectory latency distributions. ## Base URL ``` https://api.sailresearch.com ``` ## Authentication All requests require a Bearer API key in the `Authorization` header. This is the same API key you use for the inference API at `api.sailresearch.com`. Create or manage keys from the [Sail dashboard](https://app.sailresearch.com). ```python theme={null} import requests headers = {"Authorization": "Bearer YOUR_SAIL_API_KEY"} params = {"bucket_size": "1h", "environment": "all"} resp = requests.get( "https://api.sailresearch.com/v2/usage", headers=headers, params=params, ) print(resp.json()) ``` ```bash theme={null} curl -s -H "Authorization: Bearer $SAIL_API_KEY" \ "https://api.sailresearch.com/v2/usage?bucket_size=1h&environment=all" | jq ``` ## Product-aware spend Use the summary endpoint to read combined spend and its exact product split: ```bash theme={null} curl -s -H "Authorization: Bearer $SAIL_API_KEY" \ "https://api.sailresearch.com/v2/usage/summary?range=30d" | \ jq '{period_spend, product_spend, sailbox_spend}' ``` `product_spend` always has exactly two buckets, `inference` and `sailboxes`, which add up to `period_spend`. Inference token, model, completion-window, and latency metrics exclude Sailbox usage. See [Usage API endpoints](/usage-endpoints#product-accounting) for the full accounting contract and field definitions. Full reference for every usage route: parameters, response shapes, error formats, and headers. Usage data may have a slight delay and is not real-time. Billing-derived fields (spend, balance, per-API-key usage, token counters) are sourced from metered billing data and can lag the most recent minutes more than the operational activity and latency endpoints. Monetary fields are fractional USD cents (floats), and token fields are raw token counts. See [Endpoints](/usage-endpoints) for the exact units per field. # Usage API endpoints Source: https://docs.sailresearch.com/usage-endpoints Reference for every usage route, including spend, tokens, activity, and latency All endpoints live under the base URL and require a Bearer API key (the same key used for the inference API). The org is derived from the key. ```text theme={null} https://api.sailresearch.com ``` ```bash theme={null} curl -s -H "Authorization: Bearer $SAIL_API_KEY" \ "https://api.sailresearch.com/v2/usage/summary?range=30d" | jq ``` Routes fall into two families: **Billing & cost** (spend, balance, and token counters) and **Operational activity & latency** (request counts, tasks, and latency distributions). ## Common parameters Most routes accept a rolling-window `range` and, for the operational routes, an `environment` filter. | Parameter | Values | Description | | ------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | Rolling window, or the current billing `period`. The default varies per route. Unknown values fall back to the default rather than erroring. | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | Customer-facing environment label (operational routes). Combine with a comma, e.g. `dev,prod`. Default `all`. | `range` never returns a 400 for an unrecognized value. It silently falls back to the endpoint's default window. Invalid `environment`, `date`, `start`, `end`, `bucket_size`, `status`, `kind`, or `after` values do return 400, as does an unsupported `sla` on `/tasks` and `/latency/timeseries`. On `GET /v2/usage`, an unrecognized `sla` or `model` is not rejected. It is applied as a filter that matches nothing. Monetary fields (`balance`, `period_spend`, `burn_rate`, `avg_cost_per_day`, `product_spend`, `sailbox_spend`, and breakdown `total`) are **fractional USD cents** expressed as floats. For example `73795.09` is roughly \$737.95. Token fields on the public endpoints are **raw inference token counts**. ## Product accounting Billing responses follow these guarantees: * `period_spend` and each breakdown bucket's `total` include all positive Inference and Sailbox charges. * `product_spend` always contains exactly two product families, `inference` and `sailboxes`. It never emits an `other` family, and the two values add up to the corresponding combined total. * `sailbox_spend` reports the currently itemized Sailbox charges. Sailbox products without a line item still count toward `product_spend.sailboxes`, so the line-item fields may sum to less than it. * Token, model, completion-window, request, and latency metrics are inference-only. Sailbox quantities and identifiers are not represented as tokens, models, or completion windows. * Only base input, output, and cached-input token products add token counts. Surcharges add inference spend without duplicating token quantities. Cached input is included in `input`, so `total = input + output` and cached tokens must not be added to `total` again. * Model and completion-window breakdowns include only explicitly attributed inference spend. Missing attribution stays unassigned instead of creating an `other` model or completion window. To calculate product percentages, divide each product amount by the combined total. When the combined total is zero, both percentages are zero. ```text theme={null} inference percentage = product_spend.inference / period_spend sailboxes percentage = product_spend.sailboxes / period_spend ``` ## Billing & cost ### GET /v2/usage/summary Combined Inference and Sailbox spend, credit balance, burn rate, days remaining, inference token totals, inference SLA spend mix, and a prior-period comparison. | Parameter | Values | Default | | --------- | ---------------------------------------- | ------- | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `30d` | ```json theme={null} { "object": "usage.summary", "available": true, "empty": false, "has_metronome_customer": true, "range": "30d", "balance": 73795.09, "balance_unavailable": false, "period_spend": 105392.94, "product_spend": { "inference": 87200.31, "sailboxes": 18192.63 }, "sailbox_spend": { "active_vcpu": 9240, "active_memory": 4720, "active_disk": 3210, "creation_s": 522.63, "creation_m": 500 }, "burn_rate": 3513.1, "days_remaining": 21, "model_count": 14, "total_requests": 139197, "tokens": { "total": 2637290778, "input": 2562190467, "output": 75100311, "cached": 2133342376 }, "avg_cost_per_day": 3513.1, "sla_mix": { "asap": 0.78, "standard": 0.21, "flex": 0.01 }, "prior_period": { "period_spend": 57216.45, "model_count": 13, "tokens": { "total": 1472254959, "input": 1402406455, "output": 69848504, "cached": 945778881 }, "avg_cost_per_day": 1907.21 } } ``` | Field | Type | Description | | ----------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `empty` | boolean | `true` when the org has a billing account but no positive billed Inference or Sailbox spend in the window. | | `has_metronome_customer` | boolean | `false` (with zeroed fields) when the org has no billing account. | | `balance` | number | Net credit balance, in fractional cents. | | `balance_unavailable` | boolean | `true` when the balance lookup failed; other fields are still returned. | | `period_spend` | number | Combined Inference and Sailbox spend over the range, in fractional cents. | | `product_spend` | object | Combined spend split into exactly `inference` and `sailboxes`. The values add up to `period_spend`. | | `product_spend.inference` | number | Positive Inference charges, including surcharges, in fractional cents. | | `product_spend.sailboxes` | number | All positive Sailbox charges, including charges without a visible `sailbox_spend` field, in fractional cents. | | `sailbox_spend` | object | Currently itemized Sailbox charges. These fields may sum to less than `product_spend.sailboxes`. | | `sailbox_spend.active_vcpu` | number | Active Sailbox vCPU-hour spend, in fractional cents. | | `sailbox_spend.active_memory` | number | Active Sailbox memory GiB-hour spend, in fractional cents. | | `sailbox_spend.active_disk` | number | Active Sailbox disk GiB-hour spend across supported architectures, in fractional cents. | | `sailbox_spend.creation_s` | number | Sailbox S creation spend, in fractional cents. | | `sailbox_spend.creation_m` | number | Sailbox M creation spend, in fractional cents. | | `burn_rate` | number | Combined spend per day over the range, in fractional cents. | | `days_remaining` | number \| null | `balance / burn_rate`, or `null` when unknown or above 365. | | `model_count` | number | Number of explicitly attributed inference models in the range. | | `total_requests` | number \| null | Total billed inference requests over the range, when available. | | `tokens` | object | `total`, `input`, `output`, and `cached` raw inference token counts. Cached input is a subset of input, and Sailbox quantities are excluded. | | `sla_mix` | object | Fraction of explicitly attributed inference spend per completion window. Unattributed inference spend is outside the denominator and does not create a synthetic tier. | | `prior_period` | object | Prior-window combined `period_spend` and `avg_cost_per_day`, plus inference-only `model_count` and token totals. | Plan tier is not exposed via the API; it is shown only on the dashboard. ### GET /v2/usage/breakdown Combined spend and inference token breakdowns per time bucket, plus inference model cost rankings. Use `range=day` with `date=YYYY-MM-DD` to drill into a single day at hourly granularity. | Parameter | Values | Default | Description | | --------- | ----------------------------------------------- | ------- | ------------------------------------------------ | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period`, `day` | `30d` | `1h`/`6h`/`24h`/`day` return hourly granularity. | | `date` | `YYYY-MM-DD` | none | Required when `range=day`. | ```json theme={null} { "object": "usage.breakdown", "available": true, "range": "7d", "granularity": "day", "data": [ { "timestamp": "2026-06-30", "total": 10984.59, "product_spend": { "inference": 10800, "sailboxes": 184.59 }, "models": { "zai-org/GLM-5.3": { "total": 10657.96, "tokens": 439626559, "input_tokens": 432900739, "output_tokens": 6725820, "cached_tokens": 419001344 }, "moonshotai/Kimi-K2.6": { "total": 22.33, "tokens": 283710, "input_tokens": 250852, "output_tokens": 32858, "cached_tokens": 194632 } }, "slas": { "asap": 10432.31, "standard": 366.8 } } ], "models": [ { "model": "zai-org/GLM-5.3", "total": 10657.96, "tokens": 439626559, "input_tokens": 432900739, "output_tokens": 6725820, "cached_tokens": 419001344, "slas": { "asap": 10432.31 }, "percentage": 0.998 } ] } ``` | Field | Type | Description | | ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `data[]` | array | One entry per time bucket. `total` includes Inference and Sailboxes. | | `data[].product_spend` | object | The bucket's `inference` and `sailboxes` spend, in fractional cents. The values add up to `data[].total`. | | `data[].models` | object | Inference spend and token counts keyed by explicitly attributed model ID. Unattributed spend does not create a synthetic model. | | `data[].slas` | object | Inference spend keyed by explicitly attributed completion window. Unattributed spend does not create a synthetic completion window. | | `models[]` | array | Per-model inference rankings across the whole range, sorted by `total` spend descending. | | `models[].percentage` | number | The model's fraction of explicitly model-attributed inference spend. Sailbox and unattributed inference spend are outside the denominator. | Inference spend without a completion-window attribution is the remainder below. It is plain inference spend, not an `other` product or completion window. ```text theme={null} unattributed inference = max(0, data[].product_spend.inference - sum(data[].slas values)) ``` Usage breakdowns report the `balanced` window under the key `standard`. ### GET /v2/usage/api-keys Per-API-key usage, broken down by `(api_key_id, model, sla)`, with a windowed time series. `display_name` and `display_prefix` are populated for keys that still exist; deleted keys return `null` for both (only the stable `api_key_id` remains). This endpoint reports inference usage only. Sailbox charges accrue over a Sailbox's lifetime and do not map reliably to a single API key, so Sailbox usage is excluded from the per-key rows. Use `/v2/usage/summary` or `/v2/usage/breakdown` for product-aware spend. | Parameter | Values | Default | | --------- | ---------------------------------------- | ------------------ | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `24h` | | `window` | `hour`, `day` | derived from range | ```json theme={null} { "object": "usage.api_keys", "available": true, "range": "7d", "granularity": "day", "keys": [ { "key_identity": "id:key_abc123", "api_key_id": "key_abc123", "display_name": "Production", "display_prefix": "sk_QpCO", "request_count": 4210, "total_tokens": 31000000, "input_tokens": 24000000, "output_tokens": 7000000, "cached_tokens": 1200000, "regular_cached_tokens": 900000, "supercached_tokens": 300000, "supercache_write_tokens": 50000, "model": "moonshotai/Kimi-K2.6", "sla": "asap" } ], "time_series": [ { "time_bucket": "2026-06-30T00:00:00Z", "key_identity": "id:key_abc123", "api_key_id": "key_abc123", "display_name": "Production", "display_prefix": "sk_QpCO", "request_count": 600, "total_tokens": 4400000, "input_tokens": 3400000, "output_tokens": 1000000, "cached_tokens": 150000, "regular_cached_tokens": 100000, "supercached_tokens": 50000, "supercache_write_tokens": 10000, "model": "moonshotai/Kimi-K2.6", "sla": "asap" } ] } ``` | Field | Type | Description | | ------------------------- | -------------- | ------------------------------------------------------------------------ | | `key_identity` | string | Stable identity used to correlate `keys` with `time_series` rows. | | `display_name` | string \| null | `null` for deleted keys. | | `display_prefix` | string \| null | Non-secret key prefix (for example, `sk_QpCO`); `null` for deleted keys. | | `input_tokens` | number | All input tokens, including cached input and Supercache writes. | | `cached_tokens` | number | All cached input tokens, including regular cache and Supercache reads. | | `regular_cached_tokens` | number | The `cached_tokens` subset billed at the regular cached-token rate. | | `supercached_tokens` | number | The `cached_tokens` subset billed at the Supercache read rate. | | `supercache_write_tokens` | number | The `input_tokens` subset billed at the Supercache write rate. | ### GET /v2/usage/tokens Inference token counters (input/output/cached) over the range, as raw counts. Only base token products contribute quantities. Sailbox usage and inference surcharges do not add tokens. Cached input is already included in `input`. | Parameter | Values | Default | | --------- | ---------------------------------------- | ------- | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `24h` | ```json theme={null} { "object": "usage.tokens", "available": true, "range": "24h", "tokens": { "total": 6159231, "input": 5582132, "output": 577099, "cached": 2050614 } } ``` ### GET /v2/usage/tokens/timeseries Raw inference token counts per time bucket, broken down by input/output/cached. The same base-product and cached-input rules as `/tokens` apply. | Parameter | Values | Default | | --------- | ---------------------------------------- | ------- | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `24h` | ```json theme={null} { "object": "usage.tokens.timeseries", "available": true, "range": "7d", "series": [ { "time_bucket": "2026-06-30T00:00:00Z", "total": 4400000, "input": 3400000, "output": 1000000, "cached": 150000 } ] } ``` ## Operational activity & latency These routes report request counts, task activity, and latency. They all accept the [`environment`](#common-parameters) filter. ### GET /v2/usage/activity Consolidated completed-request counts and average latency over rolling windows. | Parameter | Values | Default | | ------------- | ------------------------------------------------------- | ------- | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | ```json theme={null} { "object": "usage.activity", "available": true, "requests": { "last_1m": 1, "last_1h": 385, "last_24h": 3251, "last_7d": 55592 }, "latency": { "avg_1m_ms": 6114, "avg_1h_ms": 16494 } } ``` ### GET /v2/usage/activity/timeseries Per-model completed-request counts over time. | Parameter | Values | Default | | ------------- | ------------------------------------------------------- | ------- | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `24h` | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | ```json theme={null} { "object": "usage.activity.timeseries", "available": true, "metric": "requests", "range": "24h", "series": [ { "time_bucket": "2026-07-01T00:00:00Z", "model": "openai/gpt-oss-120b", "count": 420 } ] } ``` ### GET /v2/usage/recent The most recent requests, including active and finished requests when available. | Parameter | Values | Default | | ------------- | ------------------------------------------------------- | ------- | | `limit` | `1`–`50` | `10` | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | ```json theme={null} { "object": "usage.activity.recent", "available": true, "recent_requests": [ { "response_id": "resp_019f383e-efed-78c0-8f06-f14e80dd7a0d", "model": "zai-org/GLM-5.3", "sla": "standard", "status": "queued", "created_at": "2026-07-06T16:24:36.589Z", "updated_at": "2026-07-06T16:24:36.589Z" } ] } ``` `sla` is `null` when the request has no resolved completion window. ### GET /v2/usage/tasks Paginated task activity with per-task token breakdowns, spanning both active and finished requests. | Parameter | Values | Default | | ------------- | ------------------------------------------------------------------------------------------------------------- | --------- | | `range` | `1h`, `24h` | `24h` | | `status` | `queued`, `in_progress`, `completed`, `failed`, `cancelled` | all | | `model` | model ID | all | | `sla` | `asap`, `priority`, `balanced`, `flex` (`balanced` also covers requests sent with the legacy name `standard`) | all | | `sort` | `created`, `latency` | `created` | | `limit` | `1`–`100` | `25` | | `after` | cursor from `next_cursor` | none | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | Only `1h` and `24h` are valid for `range`; any other value resolves to `24h` (reported back as `effective_range`). ```json theme={null} { "object": "usage.activity.tasks", "available": true, "active_tasks_available": true, "tasks": [ { "response_id": "resp_019f383e-c2a9-74c6-bff5-2627e18abc4b", "model": "openai/gpt-oss-120b", "status": "completed", "sla": "asap", "created_at": "2026-07-06T16:24:25.001Z", "updated_at": "2026-07-06T16:24:31.114Z", "completed_at": "2026-07-06T16:24:31.114Z", "duration_ms": 6114, "input_tokens": 71, "cached_input_tokens": 70, "output_tokens": 39, "reasoning_tokens": 0, "total_tokens": 110 } ], "next_cursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA3LTA2VD...", "effective_range": "24h", "legacy_sla_filters": [] } ``` | Field | Type | Description | | ------------------------ | -------------- | ------------------------------------------------------------------------------------- | | `active_tasks_available` | boolean | `true` when in-flight (active) tasks could be merged into the page. | | `sla` | string \| null | Completion window; `null` when unresolved. | | `completed_at` | string \| null | `null` for tasks that have not finished. | | `duration_ms` | number \| null | End-to-end duration; `null` until the task finishes. | | `next_cursor` | string \| null | Pass as `after` to fetch the next page; `null` on the last page. | | `effective_range` | string | The range actually applied (`1h` or `24h`). | | `legacy_sla_filters` | array | Legacy SLA filter descriptors, when the org still has tasks under retired SLA labels. | ### GET /v2/usage/latency/turn Turn latency distribution for a single request-to-response time, with p50/p95/p99, aggregate and per-SLA. | Parameter | Values | Default | | ------------- | ------------------------------------------------------- | ------- | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `24h` | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | ```json theme={null} { "object": "usage.latency.turn", "available": true, "aggregate": { "n": 14210, "avg_ms": 2050, "p50_ms": 1800, "p95_ms": 4200, "p99_ms": 6100, "buckets": [ { "bucket_lo_ms": 0, "bucket_hi_ms": 1000, "count": 3200 }, { "bucket_lo_ms": 8000, "bucket_hi_ms": null, "count": 140 } ], "approx": false }, "by_sla": { "asap": { "n": 9000, "avg_ms": 1700, "p50_ms": 1500, "p95_ms": 3800, "p99_ms": 5400, "buckets": [], "approx": false } } } ``` | Field | Type | Description | | ------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------- | | `buckets[].bucket_hi_ms` | number \| null | Upper edge of the histogram bucket; `null` for the open-ended top bucket. | | `percentile_mode` | string | Optional: how percentiles were derived, when the server reports it. | | `approx` | boolean | `true` when percentiles were sampled rather than computed exactly (omitted when false in the time series). | ### GET /v2/usage/latency/trajectory Trajectory latency distribution for a multi-turn conversation, with the same shape as [`/latency/turn`](#get-v2-usage-latency-turn). Trajectory responses additionally carry `avg_turns_per_trajectory`, and may set `approx: true` when percentiles are sampled. | Parameter | Values | Default | | ------------- | ------------------------------------------------------- | ------- | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `24h` | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | ```json theme={null} { "object": "usage.latency.trajectory", "available": true, "aggregate": { "n": 3200, "avg_ms": 9400, "p50_ms": 7600, "p95_ms": 21000, "p99_ms": 34000, "buckets": [{ "bucket_lo_ms": 0, "bucket_hi_ms": 5000, "count": 900 }], "approx": false, "avg_turns_per_trajectory": 4.7 }, "by_sla": {} } ``` ### GET /v2/usage/latency/timeseries Latency percentiles over time for turns or trajectories. | Parameter | Values | Default | | ------------- | ------------------------------------------------------- | ------- | | `kind` | `turn`, `trajectory` | `turn` | | `range` | `1h`, `6h`, `24h`, `7d`, `30d`, `period` | `24h` | | `model` | model ID (only with `kind=turn`) | none | | `sla` | completion window (only with `kind=turn`) | none | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | `model` and `sla` filters are only supported for `kind=turn`. Passing either with `kind=trajectory` returns a 400. ```json theme={null} { "object": "usage.latency.timeseries", "available": true, "kind": "turn", "range": "24h", "series": [ { "time_bucket": "2026-07-06T10:00:00Z", "count": 420, "avg_ms": 2000, "p50_ms": 1800, "p95_ms": 4100, "p99_ms": 6000 } ] } ``` Series points include `approx` and `avg_turns_per_trajectory` only when relevant (both are omitted otherwise). ### GET /v2/usage Completed-request latency and counts in **fixed time buckets**, with one row per environment per bucket. Unlike the rolling-window routes above, this endpoint takes an explicit `start`/`end`/`bucket_size` window, making it suited to charting a specific time range at a fixed resolution. | Parameter | Values | Default | Description | | ------------- | ------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------- | | `start` | RFC3339 timestamp | 24h ago | Inclusive window start. | | `end` | RFC3339 timestamp | now | Exclusive window end. When omitted, aligned to the bucket boundary. | | `bucket_size` | `1m`, `1h` | auto | Defaults to `1m` for windows up to 6 hours, otherwise `1h`. | | `environment` | `all`, `dev`, `prod`, `beta`, or a comma-separated list | `all` | Customer-facing environment label. | | `model` | model ID | none | Optional exact model filter. Unrecognized values match nothing. | | `sla` | `asap`, `priority`, `balanced`, `flex` (`balanced` also covers requests sent with the legacy name `standard`) | none | Optional completion-window filter. Unrecognized values match nothing rather than returning a 400. | A window that would produce more than 10,000 buckets for the chosen `bucket_size` returns a 400. Widen `bucket_size` or shorten the window. ```json theme={null} { "object": "usage", "available": true, "start": "2026-07-05T16:00:00Z", "end": "2026-07-06T16:00:00Z", "bucket_size": "1h", "buckets": [ { "environment": "prod", "bucket_start": "2026-07-05T16:00:00Z", "bucket_end": "2026-07-05T17:00:00Z", "completed_count": 12, "latency_sum_ms": 24000, "latency_count": 12, "avg_latency_ms": 2000 } ] } ``` Empty buckets are included (with zeroed counts) so a series has no gaps. ## Errors Errors use the same envelope as the inference API. For usage-API errors, `code` is set to the same string as `type`, and `param` is always `null`: ```json theme={null} { "error": { "message": "status: must be one of: queued, in_progress, completed, failed, cancelled", "type": "invalid_request_error", "param": null, "code": "invalid_request_error" } } ``` Authentication and rate-limit failures are produced by the shared API middleware and may carry a distinct `code` (for example `invalid_api_key`) or `null` (a missing `Authorization` header). | HTTP status | `type` | `code` | When | | ----------- | ----------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `invalid_request_error` | `invalid_request_error` | Invalid `date`, `start`, `end`, `bucket_size`, `environment`, `status`, `kind`, or `after` cursor; an unsupported `sla` on `/tasks` or `/latency/timeseries`; a `model`/`sla` filter with `kind=trajectory`; or a window exceeding 10,000 buckets on `GET /v2/usage`. An unknown `range` is **not** rejected. It falls back to the default. | | 401 | `authentication_error` | `null` \| `invalid_api_key` | Missing (`code` null), invalid, or expired credentials. | | 402 | `billing_error` | `credits_exhausted` | API key disabled due to insufficient credits. The error also includes a `billing_url`. | | 403 | `missing_org_identity` | `missing_org_identity` | API key is not associated with an organization. | | 429 | `rate_limit_error` | `rate_limited` | Too many concurrent requests. | | 500 | `api_error` | `api_error` | Internal server error while fetching usage data. | | 503 | `usage_unavailable` | `usage_unavailable` | Usage data is temporarily unavailable for the requested query. | | 504 | `usage_query_timeout` | `usage_query_timeout` | Query timed out. The error object includes `"retryable": true` and the response sets a `Retry-After: 2` header. | Endpoints return an empty/zeroed payload with HTTP 200 (not an error) when the org has no billing account or no usage in the requested window. # Introduction Source: https://docs.sailresearch.com/voyages Observability and timeline for long-running background agents on Sail Voyages are Sail's telemetry layer for long-running background-agent tasks. Add a few SDK calls to your existing agent harness. Sail records the run as a trace of named agents, spans, events, Sail inference calls, and Sailbox execs. Every Voyage gets its own page in the dashboard at `app.sailresearch.com`, showing its recorded trajectory. The SDK returns the link from `sail.voyage.dashboard_url()`, and the API returns it as `dashboard_url`. While a Voyage is marked running, its page checks for newly received Voyage events every five seconds while the page is visible. You can pause these updates or refresh manually at any time. The **Event auto-refresh on** label describes dashboard updates, not whether the agent process is healthy. Open **Execution Trace → Raw events** to inspect the latest event window or page forward through the full event history. You keep control of the agent loop. Sail provides the runtime pieces (Sailbox and inference) plus the Voyage timeline. Voyages are not an agent framework. They do not own your planner, memory, orchestration, tool abstractions, prompts, or retry policy. Use them with your own Python scripts, LangGraph, CrewAI, job runners, subprocesses, or any custom loop where you want visibility into what happened. **Using a coding agent?** Start with the public [Sail skills package](https://github.com/sailresearchco/sail-skills). Tell your agent: "Use Sail Voyages to add automatic telemetry to my background agent workload. Keep my harness, use the `sail` package, wrap the run with `sail.voyage.run(..., version=1)`, add `@sail.agent` and `@sail.span` where they fit naturally, and attribute Sail inference calls plus any Sailbox execs the workload already uses." You do not need to know the individual skill names. The package gives your coding agent the right Voyage instructions when they are relevant. ## Install ```bash theme={null} pip install sail export SAIL_API_KEY=sk_your_key_here ``` Both paths authenticate SDK processes: the SDK reads `SAIL_API_KEY` first and falls back to the credential `sail auth login` stores under `~/.sail`. Use `SAIL_API_KEY` for CI and deployments; logging in once is enough for local scripts. ## When to use a Voyage Use a Voyage when: * An agent task runs for more than a few seconds and you want to see its trajectory in real time. * Multiple cooperating agents (Reviewer, TestRunner, GitHub-poster, etc.) contribute to one logical task. * You need a customer-visible record of what an agent did: events, model calls, Sailbox execs, terminal status, and other evidence for debugging or auditing. Do not use a Voyage for one-shot API calls. A single `sail.inference.responses.create()` outside a Voyage works fine and shows up in your inference dashboard without extra instrumentation. ## Mental model ```python theme={null} with sail.voyage.run(name="research-agent", version=1): researcher_step() ``` * A **Voyage** is one task. * An **agent** is a named participant within the task (e.g., "Reviewer"). * A **span** is a logical step the agent performs (e.g., "draft-response"). * An **event** is a timestamped marker within a span. * A **model call** is automatically recorded when you call Sail inference inside a Voyage. If no span is active, the SDK creates an auto-span. * A **Sailbox exec** is automatically recorded when you call `sb.exec()` inside a Voyage. If no span is active, the SDK creates an auto-span. ## Minimal example ```python theme={null} import sail @sail.agent("Researcher") @sail.span("draft answer") def draft_answer(topic: str): sail.voyage.event("research.started", payload={"topic": topic}) response = sail.inference.responses.create( model="zai-org/GLM-5.3", input=f"Give one concise research note about: {topic}", background=False, ) sail.voyage.event("research.finished", payload={"response_id": response["id"]}) with sail.voyage.run(name="research-demo", version=1): draft_answer("long-term health benefits of running") print(f"Voyage URL: {sail.voyage.dashboard_url()}") ``` Run this, then open the printed URL. You should see one Voyage with one `Researcher` agent, one explicit span, two events, one attributed model call, and terminal status `voyage completed`. ## What gets attributed automatically When you call Sail inference inside a Voyage, the SDK attaches active Voyage, agent, and span context so the model call appears in the dashboard. Decorators are the most direct way to attribute function-shaped work: ```python theme={null} @sail.agent("Reviewer") @sail.span("draft") def draft_review(): return sail.inference.responses.create( model="zai-org/GLM-5.3", input="Summarize this diff: ...", background=False, timeout=120, ) ``` When you call `sb.exec()` inside a Voyage, the SDK also carries the active Voyage, agent, and span context into the Sailbox command. `sb.exec(...)` returns a request handle. Call `.wait()` for foreground commands so your program observes completion, return code, stdout/stderr tails, and the attributed exec row: ```python theme={null} @sail.agent("TestRunner") @sail.span("unit-tests") def run_tests(): req = sb.exec("pytest -q", timeout=600) req.wait() ``` You do not need to pass IDs by hand. If no span is active, Sail inference and Sailbox execs still get a timed auto-span named from the calling code when possible. Auto-spans do not invent agents, so declare an agent when the dashboard should show ownership. ## Explicit vs auto spans Use an explicit span when the step name matters to humans: ```python theme={null} @sail.agent("Researcher") @sail.span("score evidence") def score_evidence(): sail.inference.responses.create(model="zai-org/GLM-5.3", input="...") ``` If you leave the span out, Sail still captures SDK-owned work. The model call below is recorded under the `Researcher` agent and wrapped in a timed auto-span: ```python theme={null} @sail.agent("Researcher") def score_evidence(): sail.inference.responses.create(model="zai-org/GLM-5.3", input="...") ``` Auto-spans are useful when you are adding telemetry to an existing agent harness. Add `@sail.span(...)` when you want a specific step name, payload, or parent/child shape. ## Multiple agents in one Voyage A real code-review agent has at least three distinct participants. A `GitHub` agent clones the repository, a `TestRunner` runs the suite in a Sailbox, and a `Reviewer` drafts the summary with inference. Declare each one with `@sail.agent(...)`, and the dashboard shows three named agents under one Voyage, each with its own spans and events. See the [Voyages Patterns](./voyages-patterns) guide for the full multi-agent example. ## Terminal status Every Voyage needs exactly one terminal event before the controller process exits. `with sail.voyage.run(...)` handles it: clean exit emits `voyage.completed`. An exception emits `voyage.failed` and re-raises. Terminal status is first-terminal-wins. Events after the terminal are best-effort delivery only. ```python theme={null} with sail.voyage.run(name="task"): do_work() ``` Controllers whose create and terminal sites live in different places use the `create()` primitive and call `voyage.complete()` / `voyage.fail()` themselves. ## Stop recording and delete Use `voyage.cancel()` or `sail.voyage.cancel()` to stop recording a running Voyage: ```python theme={null} voyage = sail.voyage.create(name="overnight-eval") ... voyage.cancel() ``` Cancel marks the server-side Voyage `cancelled` and stops this SDK instance from recording further events. The server does not reject late events from other sources, and cancel does **not** terminate external or non-Sailbox agent code. If you own that process, stop it separately. You can delete a finished Voyage from its dashboard detail page; this removes its history and content from the dashboard and API. Security audit records are retained. There is no API or SDK delete yet. ## What to read next * [Voyages Quickstart](./voyages-quickstart): copy-paste-ready 60-second example. * [Voyages Patterns](./voyages-patterns): multi-agent and child-attach for subprocesses. * [Sail skills package](https://github.com/sailresearchco/sail-skills): agent-ready playbooks for building, instrumenting, and debugging Voyage runs. * [Building Agents](./agents): Sail's Responses API and tool calling. * [Sailboxes](./sailboxes): the runtime substrate. # Patterns & best practices Source: https://docs.sailresearch.com/voyages-patterns Production patterns for multi-agent Sailbox work and subprocess attach Two patterns matter most once you move beyond the quickstart: recording multiple agents in one Voyage and attaching subprocesses to a parent Voyage. Use decorators for function-shaped work. Use a `with` span only when a temporary block is clearer than a named function. ## Multi-agent When one logical task involves multiple cooperating agents (a Reviewer, a TestRunner, a GitHub-poster), each agent gets its own context, spans, and attributed work. They all share one Voyage, so the dashboard shows one trace for the full background agent workload. ```python theme={null} import sail sb = sail.Sailbox.create( app=sail.App.find(name="review-agent", mint_if_missing=True), image=sail.Image.debian_arm64.apt_install("git").build(), ) voyage = sail.voyage.create( name="multi-agent-code-review", sailbox_id=sb.sailbox_id, metadata={"pr_number": 1234}, ) # create() is used here because the Sailbox is created before the Voyage # finishes. For simpler scripts, wrap the work in sail.voyage.run(...). @sail.agent("GitHub", role="source_control") @sail.span("clone") def clone_repo(): sb.exec( "git clone --depth 1 https://github.com/public/repo.git /tmp/repo", timeout=120, ).wait() @sail.agent("TestRunner", role="executor") @sail.span("unit-tests") def run_tests(): sb.exec("cd /tmp/repo && pytest -q", timeout=600).wait() @sail.agent("Reviewer", role="reviewer") @sail.span("draft-review") def draft_review(): response = sail.inference.responses.create( model="zai-org/GLM-5.3", input="Review the diff in /tmp/repo...", background=False, timeout=120, ) sail.voyage.event("review.drafted", payload={"response_id": response["id"]}) clone_repo() run_tests() draft_review() voyage.complete(message="review posted") ``` **Naming convention:** the first (and only required) argument is the display name ("Reviewer"). The stable attribution key is derived from it automatically. `role=` is the optional cohort taxonomy ("reviewer", "test\_runner", "source\_control", "executor"). The dashboard groups runs by name and offers role as a categorical filter. Pass `slug=` (advanced) to pin the attribution key across display renames. ## Sailbox exec attribution and `.wait()` Sailbox commands run as first-class Voyage evidence when they happen inside a Voyage. Keep the agent context active around the call so the dashboard can show which participant owned the command: ```python theme={null} @sail.agent("TestRunner", role="executor") @sail.span("unit-tests") def run_tests(): result = sb.exec("pytest -q", timeout=600).wait() sail.voyage.event("tests.finished", payload={"exit_code": result.exit_code}) ``` `sb.exec(...)` returns immediately with a request handle. For foreground commands, call `.wait()` to observe completion, return code, and output tails. If there is no active span, Sail creates an auto-span for the exec. For foreground commands, that span closes when `.wait()` observes the result. Use explicit spans for steps you want named in the product. Let auto-spans cover low-level calls when you are migrating an existing harness and only need attribution with minimal code changes. ## Child-process attach When the controller spawns subprocesses (e.g., a parallel test runner), the subprocess should attach to the parent's Voyage rather than create its own. The parent exports `SAIL_VOYAGE_ID`. The child calls `sail.voyage.attach()`, which reads it: ```python theme={null} # parent.py import os import subprocess import sail with sail.voyage.run(name="parent-with-children") as voyage: @sail.agent("Orchestrator", role="planner") @sail.span("spawn-workers") def spawn_workers(): subprocess.run( ["python", "worker.py"], env={**os.environ, **voyage.child_env()}, check=True, ) spawn_workers() ``` `child_env()` returns the handoff env (`SAIL_VOYAGE_ID`, plus the active agent context as the child's `SAIL_AGENT_*` defaults). It returns `{}` when telemetry is disabled, so the same code runs keyless. ```python theme={null} # worker.py import sail # Reads SAIL_VOYAGE_ID from env and joins the parent's Voyage. voyage = sail.voyage.attach() @sail.agent("Worker", role="executor") @sail.span("do-work") def do_work(): sail.voyage.event("worker.tick", payload={"step": 1}) do_work() # Note: do NOT call voyage.complete() in the child. # The parent owns terminal status. First-terminal-wins. ``` ## Common cross-pattern pitfalls * **Do not complete the Voyage from inside an agent block.** Call `voyage.complete()` at the top level, after all agent contexts have exited. * **Do not reuse a Voyage across logical tasks.** One Voyage per task. If the agent does N tasks, record N Voyages. * **Do not put secrets in event payloads.** Sail applies server-side redaction, but the safest pattern is to summarize or hash sensitive values before recording them. * **Use explicit handles when one execution context juggles Voyages.** Concurrent async tasks can each select their own current Voyage. A raw thread or other context that never selects one uses the process-wide fallback. When one context switches among multiple Voyages, call methods on the intended `Voyage` object and pass `voyage=` to inference wrappers instead of relying on implicit attribution. ## Reference * [Voyages overview](./voyages) * [Voyages quickstart](./voyages-quickstart) * [Sail skills package](https://github.com/sailresearchco/sail-skills) * [Sailboxes](./sailboxes) * SDK package: [`sail` on PyPI](https://pypi.org/project/sail/) # Quickstart Source: https://docs.sailresearch.com/voyages-quickstart Ship a Voyage-instrumented agent in 60 seconds This guide gets you from zero to a working Voyage in under a minute of reading. For the conceptual overview, see [Voyages](./voyages). Working through this with a coding agent? Point it at the [Sail skills package](https://github.com/sailresearchco/sail-skills) and ask: ```text theme={null} Build a small background agent on Sail with Voyage telemetry. Keep the agent harness simple, use the sail package, wrap the run with sail.voyage.run(..., version=1), use @sail.agent and @sail.span for attribution, make the dashboard trace show model calls, and attribute Sailbox execs only if the workload already uses a Sailbox. ``` ## Prerequisites * Python 3.9+ * A Sail API key * The Sail SDK: `pip install sail` ## Step 1: install + auth ```bash theme={null} pip install sail export SAIL_API_KEY=sk_your_key_here ``` `SAIL_API_KEY` always wins. When it is unset, the SDK uses the credential stored by `sail auth login` under `~/.sail`. ## Step 2: write the minimal Voyage ```python theme={null} # voyage_hello.py import sail @sail.agent("Researcher") @sail.span("say hello") def say_hello(): sail.voyage.event("hello.fired", payload={"message": "hi from sail"}) response = sail.inference.responses.create( model="zai-org/GLM-5.3", input="In one sentence, say hello.", background=False, ) sail.voyage.event("inference.done", payload={"response_id": response["id"]}) with sail.voyage.run( name="hello-voyage", version=1, metadata={"example": "quickstart"}, ): say_hello() print(f"Open: {sail.voyage.dashboard_url()}") ``` `run()` emits `voyage.completed` when the block exits cleanly. If the block raises, it emits `voyage.failed` and re-raises. No try/except needed. ## Step 3: run it ```bash theme={null} python voyage_hello.py ``` You should see one line printed: ``` Open: https://app.sailresearch.com/prod/voyages/voy_... ``` ## Step 4: verify in the dashboard Open the printed URL. You should see: * **Status:** `voyage completed` * **Agents:** 1 (`Researcher`) * **Events:** 2 (`hello.fired`, `inference.done`) * **Model calls:** 1 (`zai-org/GLM-5.3`, status `completed`) * **Execution Trace:** one agent block with one span, two events nested underneath, one model row. You now have a Voyage: a dashboard trace for a background agent run, including events, model-call attribution, and terminal status. ## Decorators For function-shaped work, decorate instead of nesting context managers. You get the same events, spans, and attribution: ```python theme={null} import sail @sail.agent("Researcher") @sail.span() def say_hello(): sail.voyage.event("greeting.sent", payload={"channel": "stdout"}) with sail.voyage.run(name="hello-voyage", version=1): say_hello() ``` Calls you do not wrap are still captured. Sail inference and Sailbox execs made inside a Voyage get automatic, timed spans named after your calling code and marked "auto" in the dashboard. ## What to do next * Add a Sailbox: see the [Sailboxes guide](./sailboxes) for creating a long-running sandboxed VM, then pass `sailbox_id=sb.sailbox_id` to `sail.voyage.create()` so the Voyage is bound to that Sailbox. * Prefer an agent-guided setup? Use the [Sail skills package](https://github.com/sailresearchco/sail-skills) and ask your coding agent to build or instrument a background agent with Voyage telemetry. * Add a second agent: see [Voyages Patterns → Multi-agent](./voyages-patterns#multi-agent). * Something looking wrong? Install the [Sail skills](https://github.com/sailresearchco/sail-skills) and use the `sail-voyage-debugging` skill. ## Common first-run gotchas * **Voyage doesn't appear in dashboard.** Make sure `SAIL_API_KEY` is set to a valid Sail API key (`sk_...`). Events are recorded against the key's org, so a missing or wrong-org key means nothing shows up. * **Process exits without a terminal event.** The Voyage stays "in progress" forever (a bounded best-effort flush at exit delivers trailing events, but can still drop them if the network is down. It also never marks the Voyage terminal). Use `with sail.voyage.run(...)` so the terminal state is emitted for you, or call `voyage.complete()` / `voyage.fail()` yourself. * **Unexpected "auto" spans.** If a Sail inference call or Sailbox exec runs inside a Voyage with no active span, the SDK creates a timed span for it automatically. That is expected. Add `@sail.span(...)` only when you want to choose the step name yourself. * **`CERTIFICATE_VERIFY_FAILED` on macOS Python.** Some python.org installs do not have a usable root CA bundle. Install `certifi` and point Python at it: `python -m pip install certifi`, then `export SSL_CERT_FILE="$(python -c 'import certifi; print(certifi.where())')"`. * **Unsupported model.** Use a model id from Sail, not necessarily the model your coding agent is using. Start with `zai-org/GLM-5.3`; to list your account's available models, call `GET https://api.sailresearch.com/v1/models` with your `SAIL_API_KEY`. # Voyages Source: https://docs.sailresearch.com/voyages-sdk The sail.voyage API: record agent and task trajectories `sail.voyage` is a flight recorder for long-running agent, evaluation, and background-task trajectories. Your harness owns the work loop; Sail records timeline events, spans, agent metadata, and correlated [inference calls](/voyages-sdk-inference). For a guided introduction, see the [Voyages guide](/voyages); this page is the API reference. ```python theme={null} import sail @sail.agent("Solver") @sail.span("call model") def solve(): sail.voyage.event("model.called", payload={"model": "zai-org/GLM-5.3"}) with sail.voyage.run(name="overnight-eval", version=1): solve() print(sail.voyage.id(), sail.voyage.dashboard_url()) ``` ## Two ways to call Every operation is available two ways: * **Module-level helpers** (`sail.voyage.event(...)`, `sail.voyage.span(...)`, …) act on the **current Voyage** of your execution context, set by `create()`/`attach()` (see the note below). * **Methods on the `Voyage` object** returned by `create()`/`attach()` act on that specific Voyage. They behave identically; the module-level helpers just save you from threading the `Voyage` object through your code. The current Voyage follows Python `contextvars` with a process-wide fallback: concurrent tasks that each start their own Voyage keep their own attribution, and a context that never started one (a raw `threading.Thread`, code after `asyncio.run` returns) uses the process's most recently started Voyage. Span and agent contexts are strictly context-scoped, so a raw thread does not inherit the active span/agent but can still use the current Voyage for inference correlation. ### Async forms Networked operations expose an async form through `.aio`. Module-level `create` and `attach` provide `create.aio()` and `attach.aio()`. The `complete`, `fail`, `cancel`, and `flush` methods provide `.aio` forms both on the module and on a `Voyage` object. `run()` supports `async with`: ```python theme={null} async with sail.voyage.run(name="async-eval", version=1) as voyage: await sail.inference.responses.create.aio( model="zai-org/GLM-5.3", input="Summarize this run.", ) await voyage.flush.aio() ``` ## sail.voyage.run ```python theme={null} def run( name: str, *, version: int | None = None, metadata: dict | None = None, sailbox_id: str | None = None, ) -> ContextManager[Voyage | NoopVoyage] ``` This is the recommended entry point. It wraps one block of work in a single Voyage and handles the terminal lifecycle for you. Use `with` for synchronous code or `async with` when the work runs in an event loop. ```python theme={null} with sail.voyage.run("code-review", version=1) as voyage: do_work() ``` Creates the Voyage on enter (same arguments and semantics as [`create()`](#sail-voyage-create); always creates, never reads `SAIL_VOYAGE_ID`), emits `voyage.completed` on clean exit, and on an exception emits `voyage.failed` with the exception's type and message, then re-raises. Terminal delivery is the same bounded best-effort flush as `complete()`/`fail()`. A `voyage.flush()` inside the block confirms only events emitted before that call, not the terminal event emitted on exit. Use `create()` plus `complete()`/`fail()` and a following `flush()` when terminal delivery must be raise-on-failure confirmed. Without `SAIL_API_KEY` the block runs with telemetry disabled. If your start and finish happen in different parts of your code, use `create()` with `complete()`/`fail()` instead. ## sail.voyage.create ```python theme={null} def create( name: str, *, version: int | None = None, metadata: dict | None = None, sailbox_id: str | None = None, ) -> Voyage | NoopVoyage ``` Creates a new Voyage, makes it the current Voyage for the execution context, and updates the process-wide fallback used by contexts that have not selected one. It emits `voyage.started`. Always creates, even when `SAIL_VOYAGE_ID` is set in the environment; a child process joining its parent's Voyage uses [`attach()`](#sail-voyage-attach) instead. | Parameter | Default | Description | | ------------ | ------- | -------------------------------------------------------------------------- | | `name` | | Required. The series name the dashboard groups runs under. | | `version` | `None` | Optional positive integer; bump when the harness/prompts/model mix change. | | `metadata` | `None` | JSON object (≤ 64 KiB) attached to the Voyage. | | `sailbox_id` | `None` | Bind the Voyage to a Sailbox. Falls back to `SAILBOX_ID`. | **Returns** a [`Voyage`](#the-voyage-object). When `SAIL_API_KEY` is absent it returns a no-op Voyage instead (see below). ## sail.voyage.attach ```python theme={null} def attach(voyage_id: str | None = None) -> Voyage | NoopVoyage ``` Attaches to an existing Voyage, makes it current for the execution context, and updates the process-wide fallback. `voyage_id` defaults to `SAIL_VOYAGE_ID`, the handoff a parent process sets so its children join the parent's Voyage (see [child-process attach](/voyages-patterns#child-process-attach)). Raises `ValueError` when neither is provided and telemetry is enabled; without `SAIL_API_KEY` it returns a no-op Voyage instead, so a keyless child keeps running with telemetry disabled. Attaching does not emit a second `voyage.started`. ### No-op when unauthenticated If `SAIL_API_KEY` is not set, `create()` and `attach()` return a `NoopVoyage`: no Voyage is created, no network calls are made, and `id()` / `dashboard_url()` return `None`. Every method is a safe no-op. This lets the same script run locally without credentials. (Sailbox and inference APIs still require `SAIL_API_KEY`.) ## The Voyage object `create()` and `attach()` return a `Voyage` with these attributes: | Attribute | Type | Description | | --------------- | ------------- | --------------------------------------- | | `id` | `str \| None` | Voyage id (`None` on a no-op Voyage). | | `dashboard_url` | `str \| None` | Dashboard URL for the Voyage. | | `status` | `str \| None` | Latest known terminal/lifecycle status. | | `name` | `str \| None` | Voyage name. | | `version` | `int \| None` | Voyage version. | | `sailbox_id` | `str \| None` | Bound Sailbox, if any. | | `metadata` | `dict` | Metadata supplied at creation. | It exposes the same operations as the module-level helpers below (`event`, `span`, `agent`, `error`, `complete`, `fail`, `flush`, `headers`). ## event ```python theme={null} def event( kind: str, level: str = "info", message: str | None = None, payload: dict | None = None, *, span_id: str | None = None, parent_span_id: str | None = None, error_type: str | None = None, occurred_at: str | None = None, sequence_id: int | None = None, ) -> None ``` Records a timestamped event on the current span/agent. Agent attribution comes from the enclosing [`agent()`](#agent) context, or from the `SAIL_AGENT_*` env defaults when no context is active. There is no per-event override; a one-shot attributed event is `with voyage.agent(...): voyage.event(...)`. Events are buffered locally and flushed by a background thread; `event()` validates input, enqueues quickly, and does not raise network errors. | Parameter | Default | Description | | ---------------------------- | -------- | ----------------------------------------------------------- | | `kind` | required | Event kind/name (≤ 128 characters, non-empty). | | `level` | `"info"` | One of `debug`, `info`, `warn`, `error`. | | `message` | `None` | Human-readable message. Truncated at 4 KiB with a warning. | | `payload` | `None` | JSON object (≤ 64 KiB). | | `span_id` / `parent_span_id` | `None` | Override span placement; default from the active span. | | `error_type` | `None` | Error class name for error events. | | `occurred_at` | `None` | RFC3339 timestamp with timezone; defaults to now. | | `sequence_id` | `None` | Explicit monotonic ordering id; auto-assigned when omitted. | ## span ```python theme={null} def span( span_name: str | None = None, *, message: str | None = None, payload: dict | None = None, span_id: str | None = None, parent_span_id: str | None = None, ) -> ContextManager # also usable as a decorator ``` Usable as a context manager or a **decorator**. `@sail.span()` names the span after the decorated function's `__qualname__`; the `with` form requires a name. The decorator resolves the current Voyage at call time, so module-level decoration before `create()` attributes correctly. Decorating a generator function raises `TypeError` (the context would close at generator creation); `async def` is fully supported. ```python theme={null} @sail.span() def fetch_sources(urls): ... ``` Returns a context manager that emits `span.started` on enter and `span.completed` (or `span.failed`, with the exception type) on exit. Spans nest: a span opened inside another becomes its child automatically. A span carries no agent identity of its own. Wrap it in [`agent()`](#agent) to attribute it and everything inside it to a named agent. ### Span outcomes The yielded span object accepts outcome data via `merge_payload()`. The terminal event carries the started payload shallow-merged with everything merged during the span; outcome keys win on conflict, and repeated calls accumulate. The `span.started` event is unchanged, and outcomes ride `span.failed` too. Partial results recorded before a crash are kept. ```python theme={null} with voyage.span("score-subject", payload={"subject": "tennis"}) as s: result = score(subject) s.merge_payload({"score": result.score, "verdict": result.verdict}) ``` ```python theme={null} with voyage.agent("Reviewer"): with voyage.span("draft-review"): voyage.event("review.drafted") ``` ### Auto spans When Sail inference or a Sailbox exec runs inside a current Voyage with no active span, the SDK synthesizes a timed span for that operation. The generated span is marked as auto in the dashboard and named from the calling code when possible. Explicit spans always win; auto-spans only fill gaps where you declared nothing. ```python theme={null} @sail.agent("Researcher") def collect(): # No active span: this call gets a timed auto-span. sail.inference.responses.create(model="zai-org/GLM-5.3", input="...") ``` Sailbox commands are covered the same way. A foreground command's auto-span lasts until the command finishes; a background command's auto-span covers only starting it, not the detached run. ## agent ```python theme={null} def agent( name: str, *, role: str | None = None, slug: str | None = None, ) -> ContextManager ``` Returns a context manager that marks all events and inference calls inside it as belonging to a named agent. `name` (the only required argument) is the display identity shown in the dashboard; the stable attribution key is derived from it automatically (lowercased, ASCII, hyphenated). `role=` is an optional grouping label for filtering across workflows. `slug=` (advanced) pins the attribution key explicitly. Use it when renaming a display name should keep one identity, or when a child process must attach as the same agent. ```python theme={null} with voyage.agent("Reviewer"): ... ``` ### Decorator form `agent()` and `span()` also work as decorators, which is the most direct way to instrument a whole function. `sail.agent` and `sail.span` are top-level re-exports of the same objects. ```python theme={null} import sail @sail.agent("Researcher") @sail.span() def collect_sources(urls): return [fetch(url) for url in urls] ``` Each call enters a fresh agent/span frame (concurrent calls don't share state) and emits the same events as the `with` form. ## error ```python theme={null} def error( error_type: str | None = None, message: str | None = None, payload: dict | None = None, ) -> None ``` Records an error-level `voyage.error` event without terminating the Voyage. ## complete ```python theme={null} def complete(message: str | None = None, payload: dict | None = None) -> None ``` Emits `voyage.completed` and performs a bounded (10s) best-effort flush. Always call `complete()` (or `fail()`) before your process exits. The first terminal event wins; any events emitted after it are delivered best-effort. On delivery failure it warns and returns (the event stays buffered for background/atexit retry) instead of raising; call [`flush()`](#flush) afterwards if you need raise-on-failure delivery confirmation. ## fail ```python theme={null} def fail( error_type: str = "harness_error", message: str | None = None, payload: dict | None = None, ) -> None ``` Emits `voyage.failed` and performs the same bounded best-effort flush as `complete()`. It warns instead of raising on delivery failure. `error_type` must be non-empty. ```python theme={null} voyage = sail.voyage.create(name="task") try: do_work() voyage.complete(message="ok") except Exception as exc: voyage.fail(error_type=exc.__class__.__name__, message=str(exc)) raise ``` ## flush ```python theme={null} def flush(timeout: float | None = None) -> None ``` Blocks until all buffered events are delivered, raising on delivery failure. A bounded best-effort flush also runs automatically at process exit, but that is not a substitute for `complete()`/`fail()` for product-critical terminal state. ## headers ```python theme={null} def headers(existing: Mapping[str, str] | None = None) -> dict[str, str] ``` Returns a copy of `existing` with the full attribution context set: `X-Sail-Voyage-Id` for the current Voyage, plus `X-Sail-Voyage-Span-Id` and `X-Sail-Voyage-Agent-Id` for the span/agent active at call time. Use this to correlate a raw HTTP/OpenAI client with the Voyage when you can't use the [inference wrappers](/voyages-sdk-inference). Compute it per request, never once at client construction, so each call carries the context actually active when it is made. ## child\_env ```python theme={null} def child_env(*, agent: bool = True) -> dict[str, str] ``` Env vars a child process needs to [`attach()`](#sail-voyage-attach) to the current Voyage: merge into the child's environment instead of exporting `SAIL_VOYAGE_ID` by hand. With `agent=True` (default) the active `agent()` context rides along as the child's `SAIL_AGENT_*` defaults. Returns `{}` when telemetry is disabled, so the handoff is keyless-safe. ```python theme={null} subprocess.run(["python", "worker.py"], env={**os.environ, **sail.voyage.child_env()}) ``` ## disable ```python theme={null} def disable() -> NoopVoyage ``` Disables Voyage telemetry for this process by installing a no-op current Voyage, the public form of the state `create()`/`attach()` enter when `SAIL_API_KEY` is absent. For controllers that catch a startup telemetry failure and choose to continue unobserved. ## Module helpers ```python theme={null} def id() -> str | None def dashboard_url() -> str | None def cancel() -> None ``` `sail.voyage.id()` and `sail.voyage.dashboard_url()` return the current Voyage's id and dashboard URL (or `None` when there is no current Voyage or it is a no-op). `sail.voyage.cancel()` delegates to the current Voyage and marks it cancelled on the server. It does not emit a `voyage.cancelled` event. With no current Voyage, or when the current Voyage is a no-op because telemetry is disabled, it returns without network I/O. ## Environment variables | Variable | Purpose | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SAIL_API_KEY` | Required for real Voyage events and inference. A Sail API key (`sk_...`). | | `SAILBOX_ID` | Default `sailbox_id` attached to new Voyages. | | `SAIL_VOYAGE_ID` | Default voyage id for `attach()`. | | `SAIL_AGENT_ID` | Default event `agent_id`; normalized to the derived slug form, so it matches `agent()` in a parent process. | | `SAIL_AGENT_NAME` | Default event `agent_name`. | | `SAIL_AGENT_ROLE` | Default event `agent_role`. | | `SAIL_VOYAGE_AUTO_SPANS` | Set to `0`, `false`, `no`, or `off` to disable synthesized spans for un-spanned Sail inference and Sailbox execs. | | `SAIL_VOYAGE_DEBUG` | Warn on every occurrence of a degradation, not just the first. By default each kind of degradation (no-op mode, dropped events, stubbed payloads, failed flushes) warns once. | ## Validation and delivery semantics * **Validation:** `kind` ≤ 128 characters; `level` one of `debug`/`info`/`warn`/`error`; `payload` and `metadata` must be JSON objects; `occurred_at` must be RFC3339 with a timezone; `version` must be a positive integer. Oversized human text does not raise: a `message` or span name over 4 KiB is truncated, and a `payload` over 64 KiB is replaced by a `{"_truncated": true, "_original_bytes": N}` stub, each with a warning. `metadata` over 64 KiB raises at `create()` (startup is when failing is cheapest). `create()` and `attach()` validate their arguments before the no-key gate, so a malformed call raises even when telemetry is disabled. * **Buffering:** events go to a bounded local buffer. When it is full, the oldest non-terminal events are dropped first and a `sdk.events_dropped` notice is emitted; lifecycle events (`voyage.started` plus the terminal `voyage.completed`/`voyage.failed`) are preserved. * **Delivery semantics:** `flush()` blocks and raises delivery errors (`sail.VoyageError` and subclasses), the strict primitive. `complete()` and `fail()` perform a bounded best-effort flush and warn instead of raising. `cancel()` calls the dedicated cancel endpoint and raises `sail.VoyageError` subclasses on HTTP failure. `event()` never raises network errors. * **Cancel semantics:** `cancel()` marks the server-side Voyage cancelled without enqueueing or posting a `voyage.cancelled` event. It stops recording future trace updates; it does not terminate external agent code. Without `SAIL_API_KEY`, `NoopVoyage.cancel()` is a safe no-op. # Errors Source: https://docs.sailresearch.com/voyages-sdk-errors Voyage and inference exception taxonomy Voyage and inference exceptions derive from `sail.SailError`, the base for every SDK error. `flush()` raises these on delivery failure; `complete()` and `fail()` warn instead of raising (the terminal event stays buffered for retry); `event()` never raises network errors. ```text theme={null} SailError ├─ VoyageError │ └─ VoyageHTTPError │ └─ VoyageNotFoundError ├─ InferenceError │ └─ InferenceHTTPError ``` ```python theme={null} import sail try: sail.voyage.complete(message="done") sail.voyage.flush() # raise-on-failure delivery confirmation except sail.VoyageNotFoundError: ... # the Voyage no longer exists for this API key except sail.VoyageError as exc: ... # any other Voyage delivery failure ``` All SDK network calls are bounded: inference requests default to a 600s timeout, voyage lifecycle calls to 10s, and `flush()` bounds each batch send (60s) even when called without a timeout. A wedged connection surfaces as one of the errors below rather than blocking forever. ## Recommended lifecycle Most agents should use `with sail.voyage.run(...):`. It emits `voyage.completed` on a clean exit, emits `voyage.failed` and re-raises on an exception, and performs the same bounded best-effort terminal delivery as the manual helpers. Use `create()` / `complete()` / `fail()` only when your controller's start and terminal sites are separated, or when you need strict raise-on-failure confirmation of the terminal lifecycle event. Call `flush()` after the terminal helper so `voyage.completed` or `voyage.failed` is already in the delivery buffer: ```python theme={null} voyage = sail.voyage.create(name="nightly-research", version=1) try: do_work() except BaseException as exc: voyage.fail(error_type="harness_error", message=str(exc)) voyage.flush() raise else: voyage.complete(message="done") voyage.flush() ``` ## VoyageError Base class for Voyage SDK delivery errors, such as a flush that times out or cannot deliver a required terminal event. Subclass of `SailError`. ## VoyageHTTPError Raised when the Voyage API returns an HTTP error. Subclass of `VoyageError`. | Attribute | Type | Description | | ------------- | -------------- | --------------------------- | | `status_code` | `int` | HTTP status code. | | `response` | `dict \| None` | Parsed error response body. | ## VoyageNotFoundError Raised when a Voyage cannot be found for the current API key (HTTP 404). Subclass of `VoyageHTTPError`, so it carries `status_code` and `response`. ## InferenceError Base class for [inference](/voyages-sdk-inference) wrapper errors. Raised, for example, when an unsupported wrapper option such as `stream=True` is passed, or when no API key is configured. Subclass of `SailError`. ## InferenceHTTPError Raised when a Sail inference endpoint returns a non-2xx response. Subclass of `InferenceError`. | Attribute | Type | Description | | ------------- | -------------- | --------------------------- | | `status_code` | `int` | HTTP status code. | | `response` | `dict \| None` | Parsed error response body. | # Inference Source: https://docs.sailresearch.com/voyages-sdk-inference Voyage-correlated wrappers over Sail's inference endpoints `sail.inference` provides thin wrappers over Sail's hosted inference endpoints. They POST the JSON payload as given and return the raw JSON response as a `dict`. When a [Voyage](/voyages-sdk) is active, the wrappers attach correlation headers so the model call shows up on the Voyage timeline, scoped to the active span and agent. Raw clients can attach the same headers to Responses, Chat Completions, or Anthropic Messages requests. ```python theme={null} import sail resp = sail.inference.responses.create( model="zai-org/GLM-5.3", input="Say hello in one sentence.", ) chat = sail.inference.chat.completions.create( model="zai-org/GLM-5.3", messages=[{"role": "user", "content": "hello"}], ) ``` Async code uses the same methods through `.aio`: ```python theme={null} resp = await sail.inference.responses.create.aio( model="zai-org/GLM-5.3", input="Say hello in one sentence.", ) ``` ## Context-window admission For text input sent through `responses.create` or `chat.completions.create`, Sail reserves at least 512 tokens, or 0.5% on larger context windows, for model-specific request formatting. The input token count plus the requested maximum output must fit in the remaining budget. Responses requests that provide `raw_prompt_tokens` use their exact raw-token count without this reserve. ## responses.create ```python theme={null} def create( *, voyage: Voyage | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, **payload, ) -> dict ``` POSTs `payload` to `/v1/responses` and returns the raw JSON response dict. When polling a background response, treat `completed`, `incomplete`, `failed`, and `cancelled` as terminal. An `incomplete` response can include `output` and `usage`; inspect `incomplete_details.reason` before deciding whether to use the output or submit another request. Partial output from a `max_output_tokens` stop may be usable. Handle a `content_filter` stop separately. | Parameter | Default | Description | | ----------- | ------- | ------------------------------------------------------------------ | | `**payload` | | The request body (e.g. `model`, `input`). Sent as-is. | | `voyage` | `None` | Correlate with an explicit Voyage. Defaults to the current Voyage. | | `headers` | `None` | Extra request headers to merge. | | `timeout` | `None` | Request timeout in seconds; defaults to a bounded 600s. | ## responses.retrieve ```python theme={null} def retrieve( response_id: str, *, voyage: Voyage | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, ) -> dict ``` Fetches `/v1/responses/{response_id}` and returns the raw JSON response dict. The wrapper does not poll automatically. Call it again only while `status` is nonterminal. ## chat.completions.create ```python theme={null} def create( *, voyage: Voyage | None = None, headers: Mapping[str, str] | None = None, timeout: float | None = None, **payload, ) -> dict ``` POSTs `payload` to `/v1/chat/completions` and returns the raw JSON response dict. Same parameters as `responses.create`. ## Voyage correlation If a current Voyage exists (or you pass `voyage=`), the wrappers add the `X-Sail-Voyage-Id` header plus the active span/agent context so the dashboard attributes the model call to the right place on the timeline. Pass `voyage=` to correlate with a specific Voyage, or call inference with no active Voyage for ordinary uncorrelated inference. ```python theme={null} with voyage.agent("Reviewer"): with voyage.span("draft"): # Auto-attributed to this agent/span. sail.inference.responses.create(model="zai-org/GLM-5.3", input="...") ``` **Auto-spans:** a wrapper call made with *no* active span gets a real, timed span created around it automatically (named after the calling function when derivable), so the model call is attributed to a span instead of appearing unscoped. Auto-spans are marked as auto in the dashboard. Explicit spans always win, so synthesis happens only where you declared nothing. Set `SAIL_VOYAGE_AUTO_SPANS=0` to disable. ```python theme={null} @sail.agent("Reviewer") @sail.span("draft review") def explicit_span(): sail.inference.responses.create(model="zai-org/GLM-5.3", input="...") @sail.agent("Reviewer") def auto_span(): # No active span here, so Sail creates a timed auto-span for this model call. sail.inference.responses.create(model="zai-org/GLM-5.3", input="...") ``` Auto-spans do not infer an agent. Wrap the function or block in `@sail.agent(...)` / `with voyage.agent(...)` when ownership should appear in the dashboard. ## Streaming with the SDK wrapper The high-level `sail.inference.*` wrappers return parsed JSON objects and do not expose a streaming iterator. Passing `stream=True` to those wrappers raises `sail.InferenceError` before sending the request. Use a raw HTTP client, the OpenAI SDK, or the Anthropic SDK pointed at Sail when you need API-level streaming. ## Raw HTTP, OpenAI, and Anthropic clients Wrap an OpenAI-style client pointed at Sail's API once, and every call attributes itself. Headers are computed at call time, so there is no construction-time snapshot that can go stale. Un-spanned calls get the same synthesized auto-spans as the `sail.inference` wrappers. ```python theme={null} import os from openai import OpenAI import sail client = sail.voyage.wrap_openai( OpenAI( base_url="https://api.sailresearch.com/v1", api_key=os.environ["SAIL_API_KEY"], ) ) with sail.voyage.agent("Reviewer"): client.responses.create(model="zai-org/GLM-5.3", input="...") # auto-attributed ``` `wrap_openai` wraps `responses.create`, `responses.retrieve`, and `chat.completions.create` in place (whichever exist), is idempotent, and resolves the context-local current Voyage on each call with the process-wide fallback. Pass `voyage=` to pin one. For the Anthropic SDK, attach the Voyage headers to each Messages request: ```python theme={null} import os from anthropic import Anthropic import sail client = Anthropic( base_url="https://api.sailresearch.com", api_key=os.environ["SAIL_API_KEY"], ) with sail.voyage.run(name="messages-client", version=1) as voyage: with voyage.agent("Reviewer"): with voyage.span("call"): message = client.messages.create( model="zai-org/GLM-5.3", max_tokens=256, messages=[{"role": "user", "content": "Review this change."}], extra_headers=sail.voyage.headers(), ) ``` The Messages call appears in the same Voyage model-call views as Responses and Chat Completions calls. For any other HTTP client, call the endpoint directly and attach the attribution headers yourself with [`sail.voyage.headers()`](/voyages-sdk#headers). The helper carries the full context (voyage id plus the span/agent active at call time), so compute it per request, never once at client construction: ```python theme={null} import json, os, urllib.request import sail sail.voyage.create(name="raw-client") api_url = "https://api.sailresearch.com" headers = sail.voyage.headers({"Content-Type": "application/json"}) headers["Authorization"] = "Bearer " + os.environ["SAIL_API_KEY"] req = urllib.request.Request( api_url.rstrip("/") + "/v1/responses", data=json.dumps({"model": "zai-org/GLM-5.3", "input": "hello"}).encode(), headers=headers, method="POST", ) ``` ## Voyage limitations Voyages is telemetry only. It records and attributes your runs; it is not an agent framework and adds no orchestration or tool abstractions. ## Errors Inference wrappers raise `sail.InferenceError` (e.g. for unsupported wrapper options such as `stream=True`, or a missing API key) and `sail.InferenceHTTPError` for non-2xx responses. See [Errors](/voyages-sdk-errors). # Webhooks Source: https://docs.sailresearch.com/webhooks Receive completion notifications via completion_webhook and webhook_token When you create a request through `POST /v1/responses`, `POST /v1/chat/completions`, or `POST /v1/messages`, you can provide a **completion webhook** in request metadata. When processing finishes, Sail will POST the full response payload to your URL so you can process it without polling. ## Enabling a completion webhook Include a `completion_webhook` URL in the `metadata` object of your create request. The URL must be `http` or `https`. ```python theme={null} from openai import OpenAI client = OpenAI( api_key="YOUR_SAIL_API_KEY", base_url="https://api.sailresearch.com/v1", ) response = client.responses.create( model="zai-org/GLM-5.3", input="Summarize this document.", background=True, metadata={ "completion_webhook": "https://your-server.com/sail-completion", }, ) ``` ```bash theme={null} curl -X POST https://api.sailresearch.com/v1/responses \ -H "Authorization: Bearer YOUR_SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "Summarize this document.", "background": true, "metadata": { "completion_webhook": "https://your-server.com/sail-completion" } }' ``` If `metadata.completion_webhook` is omitted or invalid, no webhook request is sent. The create call and the response itself are unchanged; webhooks are optional and best-effort. For Chat Completions and Anthropic Messages, pass the same metadata keys (`completion_webhook`, `webhook_token`) on the request body. ## Webhook payload Sail sends a **POST** request to your URL with: * **Content-Type:** `application/json` * **Body:** The same general JSON object returned by `GET /v1/responses/{response_id}`. Webhook payloads can omit `metadata.supercached_input_tokens` and `metadata.supercache_write_input_tokens`. A Responses API completion webhook can carry `status: "completed"` or `status: "incomplete"`. An incomplete payload is a successful webhook delivery, not a webhook error. Its body preserves `usage`, `incomplete_details`, and any partial `output`. Process that payload once and do not keep polling the response for another status. ## Securing webhooks with a token To verify that incoming requests are from Sail, set `webhook_token` in the `metadata`. Sail will send the value of `webhook_token` as a Bearer token in the `Authorization` header of the webhook POST. ```python theme={null} response = client.responses.create( model="zai-org/GLM-5.3", input="Summarize this document.", background=True, metadata={ "completion_webhook": "https://your-server.com/sail-completion", "webhook_token": "your-secret-token", }, ) ``` Your server can check `Authorization: Bearer your-secret-token` and reject requests that don't match. ## Delivery behavior * **Duplicates:** Sail may occasionally deliver the same webhook more than once. Log the response `id` from the webhook body and ignore events you have already processed. * **Retries:** Sail retries failed deliveries (a non-2xx status or a network error) in rounds. A round makes up to **3** attempts back to back within a **30-second** budget, and failed rounds are repeated with increasing delays of up to a few minutes, for at most **20** rounds. A persistently failing endpoint can receive up to 60 requests for one response. Respond with a **2xx** status as soon as you have accepted the payload so that Sail stops retrying. * **Best-effort:** Webhook failures are logged but do not affect the response or the API. The response remains available via `GET /v1/responses/{response_id}` even if the webhook never succeeds. ## Full example Here's a full, end-to-end example using ngrok: **1. Start a local webhook listener** that prints the payload and returns 200: ```bash theme={null} python -c " from http.server import HTTPServer, BaseHTTPRequestHandler; import json class H(BaseHTTPRequestHandler): def do_POST(self): print(json.dumps(json.loads(self.rfile.read(int(self.headers['Content-Length']))), indent=2)) self.send_response(200); self.end_headers() HTTPServer(('127.0.0.1', 8765), H).serve_forever() " ``` **2. In a second terminal, expose it with ngrok:** ```bash theme={null} ngrok http 8765 ``` Copy the `https://xxxx.ngrok-free.app` forwarding URL from the output. **3. In a third terminal, create a response with the webhook:** ```bash theme={null} curl -X POST https://api.sailresearch.com/v1/responses \ -H "Authorization: Bearer YOUR_SAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "zai-org/GLM-5.3", "input": "What is 2+2? Reply with just the number.", "background": true, "metadata": { "completion_webhook": "https://xxxx.ngrok-free.app" } }' ``` When the response completes, Sail POSTs the full payload to your listener.