Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
09dc197
Add Sailbox sandbox provider
nirvikbaruah May 25, 2026
6b3d25e
Bump Sail SDK sandbox dependency
nirvikbaruah May 25, 2026
1142136
Expand Sailbox sandbox provider tests
nirvikbaruah May 25, 2026
a8e285e
Update locked Sail SDK version
nirvikbaruah May 25, 2026
c281aa9
Address Sailbox sandbox review comments
nirvikbaruah May 25, 2026
2cb8fe7
Wrap Sailbox resume start failures
nirvikbaruah May 25, 2026
e355b6f
Preserve Sailbox client option defaults
nirvikbaruah May 25, 2026
afac09e
Make Sailbox app options round-trippable
nirvikbaruah May 25, 2026
f72814d
Add Sailbox review regression coverage
nirvikbaruah May 25, 2026
0cebae9
Honor Sailbox write user identity
nirvikbaruah May 25, 2026
624e3fe
Fix Sailbox user write without sudo
nirvikbaruah May 25, 2026
7818c48
Cover Sailbox user writes without sudo
nirvikbaruah May 25, 2026
6b9de01
Cover Sailbox user write ownership
nirvikbaruah May 25, 2026
5de60b2
Normalize Sailbox exec output
nirvikbaruah May 25, 2026
96ca917
Restore Sailbox image options from JSON
nirvikbaruah May 25, 2026
019516f
Bypass sudo for Sailbox read user checks
nirvikbaruah May 26, 2026
3bcba0f
Recheck Sailbox liveness before reporting running
nirvikbaruah May 26, 2026
6c19643
Terminate Sailbox resources on delete
nirvikbaruah May 26, 2026
9175c7e
Normalize Sailbox command error output
nirvikbaruah May 26, 2026
b8e6f32
Resume paused Sailbox after reconnect
nirvikbaruah May 26, 2026
fb99393
Gate Sailbox client option test imports
nirvikbaruah May 26, 2026
75ac18f
Mark resumed Sailbox sessions preserved
nirvikbaruah May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/ref/extensions/sandbox/sailbox/sandbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: agents.extensions.sandbox.sailbox.sandbox
3 changes: 3 additions & 0 deletions docs/sandbox/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ For provider-specific setup notes and links for the checked-in extension example
| `E2BSandboxClient` | `openai-agents[e2b]` | [E2B runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/e2b_runner.py) |
| `ModalSandboxClient` | `openai-agents[modal]` | [Modal runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/modal_runner.py) |
| `RunloopSandboxClient` | `openai-agents[runloop]` | [Runloop runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/runloop/runner.py) |
| `SailboxSandboxClient` | `openai-agents[sailbox]` | [Sailbox runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/sailbox_runner.py) |
| `VercelSandboxClient` | `openai-agents[vercel]` | [Vercel runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/vercel_runner.py) |

</div>
Expand All @@ -113,6 +114,7 @@ Hosted sandbox clients expose provider-specific mount strategies. Choose the bac
| `DaytonaSandboxClient` | Supports rclone-backed cloud storage mounts with `DaytonaCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `E2BSandboxClient` | Supports rclone-backed cloud storage mounts with `E2BCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `RunloopSandboxClient` | Supports rclone-backed cloud storage mounts with `RunloopCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `SailboxSandboxClient` | No hosted-specific mount strategy is currently exposed. Use manifest files, repos, or other workspace inputs instead. |
| `VercelSandboxClient` | No hosted-specific mount strategy is currently exposed. Use manifest files, repos, or other workspace inputs instead. |

</div>
Expand All @@ -130,6 +132,7 @@ The table below summarizes which remote storage entries each backend can mount d
| `DaytonaSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `E2BSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `RunloopSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `SailboxSandboxClient` | - | - | - | - | - | - |
| `VercelSandboxClient` | - | - | - | - | - | - |

</div>
Expand Down
32 changes: 31 additions & 1 deletion examples/sandbox/extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ They intentionally keep the flow simple:

1. Build a tiny manifest in memory.
2. Create a `SandboxAgent` that inspects that workspace through one shell tool.
3. Run the agent against E2B, Modal, Daytona, Cloudflare, Runloop, Blaxel, or Vercel.
3. Run the agent against E2B, Modal, Daytona, Cloudflare, Runloop, Sailbox, Blaxel, or Vercel.

All of these examples require `OPENAI_API_KEY`, because they call the model through the normal
`Runner` path. Each cloud backend also needs its own provider credentials.
Expand Down Expand Up @@ -328,6 +328,36 @@ the default home and working directory become `/root`, so the example also uses
`/root` as its manifest workspace root. If you configure root launch in your
own code, either rely on that root-mode default or explicitly choose a
`manifest.root` under `/root`.

## Sailbox

### Setup

Install the repo extra:

```bash
uv sync --extra sailbox
```

Export the required environment variables:

```bash
export OPENAI_API_KEY=...
export SAIL_API_KEY=...
```

### Run

```bash
uv run python examples/sandbox/extensions/sailbox_runner.py --stream
```

Useful flags:

- `--image debian-arm64`
- `--image debian-amd64`
- `--pause-on-exit`

## Blaxel

### Setup
Expand Down
162 changes: 162 additions & 0 deletions examples/sandbox/extensions/sailbox_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""
Minimal Sailbox-backed sandbox example for manual validation.

This mirrors the other cloud extension examples: it creates a tiny workspace, asks a sandboxed
agent to inspect it through one shell tool, and prints a short answer.
"""

from __future__ import annotations

import argparse
import asyncio
import os
import sys
from pathlib import Path
from typing import Literal, cast

from openai.types.responses import ResponseTextDeltaEvent

from agents import ModelSettings, Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig

if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))

from examples.sandbox.misc.example_support import text_manifest
from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability

try:
from sail.image import Image, ImageDefinition

from agents.extensions.sandbox import SailboxSandboxClient, SailboxSandboxClientOptions
except Exception as exc: # pragma: no cover - import path depends on optional extras
raise SystemExit(
"Sailbox sandbox examples require the optional repo extra.\n"
"Install it with: uv sync --extra sailbox"
) from exc


DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences."


def _build_manifest() -> Manifest:
return text_manifest(
{
"README.md": (
"# Sailbox Demo Workspace\n\n"
"This workspace exists to validate the Sailbox sandbox backend manually.\n"
),
"handoff.md": (
"# Handoff\n\n"
"- Customer: Northwind Traders.\n"
"- Goal: validate Sailbox sandbox exec and workspace flows.\n"
"- Current status: the OpenAI Agents SDK provider is wired for manual smoke tests.\n"
),
"todo.md": (
"# Todo\n\n"
"1. Inspect the workspace files.\n"
"2. Summarize the current status in two sentences.\n"
),
}
)


def _require_env(name: str) -> None:
if os.environ.get(name):
return
raise SystemExit(f"{name} must be set before running this example.")


def _image_from_name(name: Literal["debian-arm64", "debian-amd64"]) -> ImageDefinition:
if name == "debian-amd64":
return Image.debian_amd64
return Image.debian_arm64


async def main(
*,
model: str,
question: str,
image: Literal["debian-arm64", "debian-amd64"],
pause_on_exit: bool,
stream: bool,
) -> None:
_require_env("OPENAI_API_KEY")
_require_env("SAIL_API_KEY")

manifest = _build_manifest()
agent = SandboxAgent(
name="Sailbox Sandbox Assistant",
model=model,
instructions=(
"Answer questions about the sandbox workspace. Inspect the files before answering "
"and keep the response concise. "
"Do not invent files or statuses that are not present in the workspace. Cite the "
"file names you inspected."
),
default_manifest=manifest,
capabilities=[WorkspaceShellCapability()],
model_settings=ModelSettings(tool_choice="required"),
)

client = SailboxSandboxClient()
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=client,
options=SailboxSandboxClientOptions(
image=_image_from_name(image),
pause_on_exit=pause_on_exit,
),
),
workflow_name="Sailbox sandbox example",
)

if not stream:
result = await Runner.run(agent, question, run_config=run_config)
print(result.final_output)
return

stream_result = Runner.run_streamed(agent, question, run_config=run_config)
saw_text_delta = False
async for event in stream_result.stream_events():
if event.type == "raw_response_event" and isinstance(
event.data, ResponseTextDeltaEvent
):
if not saw_text_delta:
print("assistant> ", end="", flush=True)
saw_text_delta = True
print(event.data.delta, end="", flush=True)

if saw_text_delta:
print()


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="gpt-5.5", help="Model name to use.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
parser.add_argument(
"--image",
choices=("debian-arm64", "debian-amd64"),
default="debian-arm64",
help="Sailbox base image to use.",
)
parser.add_argument(
"--pause-on-exit",
action="store_true",
default=False,
help="Pause the Sailbox on shutdown instead of terminating it.",
)
parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.")
args = parser.parse_args()

asyncio.run(
main(
model=args.model,
question=args.question,
image=cast(Literal["debian-arm64", "debian-amd64"], args.image),
pause_on_exit=args.pause_on_exit,
stream=args.stream,
)
)
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ cloudflare = ["aiohttp>=3.12,<4"]
e2b = ["e2b==2.20.0", "e2b-code-interpreter==2.4.1"]
modal = ["modal==1.3.5"]
runloop = ["runloop_api_client>=1.16.0,<2.0.0"]
sailbox = ["sail-sdk>=0.1.32"]
vercel = ["vercel>=0.5.6,<0.6"]
s3 = ["boto3>=1.34"]
temporal = [
Expand Down Expand Up @@ -164,6 +165,10 @@ ignore_missing_imports = true
module = ["vercel", "vercel.*"]
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = ["sail", "sail.*"]
ignore_missing_imports = true

[tool.coverage.run]
source = ["src/agents"]
omit = [
Expand Down
22 changes: 22 additions & 0 deletions src/agents/extensions/sandbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@
except Exception: # pragma: no cover
_HAS_RUNLOOP = False

try:
from .sailbox import (
SailboxSandboxClient as SailboxSandboxClient,
SailboxSandboxClientOptions as SailboxSandboxClientOptions,
SailboxSandboxSession as SailboxSandboxSession,
SailboxSandboxSessionState as SailboxSandboxSessionState,
)

_HAS_SAILBOX = True
except Exception: # pragma: no cover
_HAS_SAILBOX = False

try:
from .vercel import (
VercelSandboxClient as VercelSandboxClient,
Expand Down Expand Up @@ -177,6 +189,16 @@
]
)

if _HAS_SAILBOX:
__all__.extend(
[
"SailboxSandboxClient",
"SailboxSandboxClientOptions",
"SailboxSandboxSession",
"SailboxSandboxSessionState",
]
)

if _HAS_VERCEL:
__all__.extend(
[
Expand Down
15 changes: 15 additions & 0 deletions src/agents/extensions/sandbox/sailbox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

from .sandbox import (
SailboxSandboxClient,
SailboxSandboxClientOptions,
SailboxSandboxSession,
SailboxSandboxSessionState,
)

__all__ = [
"SailboxSandboxClient",
"SailboxSandboxClientOptions",
"SailboxSandboxSession",
"SailboxSandboxSessionState",
]
Loading