Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions docs/ref/extensions/sandbox/islo/mounts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `Mounts`

::: agents.extensions.sandbox.islo.mounts
3 changes: 3 additions & 0 deletions docs/ref/extensions/sandbox/islo/sandbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `Sandbox`

::: agents.extensions.sandbox.islo.sandbox
8 changes: 8 additions & 0 deletions docs/sandbox/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ For provider-specific setup notes and links for the checked-in extension example
| `CloudflareSandboxClient` | `openai-agents[cloudflare]` | [Cloudflare runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/cloudflare_runner.py) |
| `DaytonaSandboxClient` | `openai-agents[daytona]` | [Daytona runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/daytona/daytona_runner.py) |
| `E2BSandboxClient` | `openai-agents[e2b]` | [E2B runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/e2b_runner.py) |
| `IsloSandboxClient` | `openai-agents[islo]` | [Islo runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/islo_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) |
| `VercelSandboxClient` | `openai-agents[vercel]` | [Vercel runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/vercel_runner.py) |
Expand All @@ -112,11 +113,17 @@ Hosted sandbox clients expose provider-specific mount strategies. Choose the bac
| `BlaxelSandboxClient` | Supports cloud bucket mounts with `BlaxelCloudBucketMountStrategy` on `S3Mount`, `R2Mount`, and `GCSMount`. Also supports persistent Blaxel Drives with `BlaxelDriveMount` and `BlaxelDriveMountStrategy` from `agents.extensions.sandbox.blaxel`. |
| `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`. |
| `IsloSandboxClient` | Supports rclone-backed cloud storage mounts with `IsloCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. Exposed-port URL resolution is not currently exposed. |
| `RunloopSandboxClient` | Supports rclone-backed cloud storage mounts with `RunloopCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. |
| `VercelSandboxClient` | No hosted-specific mount strategy is currently exposed. Use manifest files, repos, or other workspace inputs instead. |

</div>

`IsloSandboxClient` accepts a `base_url` value for the Islo control API and
defaults to `ISLO_BASE_URL` or `https://api.islo.dev`. Newer Islo SDK versions
may also accept `compute_url` for compute-plane traffic; leave it unset when
using an Islo SDK version that does not expose `AsyncIslo(compute_url=...)`.

The table below summarizes which remote storage entries each backend can mount directly.

<div class="sandbox-nowrap-first-column-table" markdown="1">
Expand All @@ -129,6 +136,7 @@ The table below summarizes which remote storage entries each backend can mount d
| `BlaxelSandboxClient` | ✓ | ✓ | ✓ | - | - | - |
| `DaytonaSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `E2BSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `IsloSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `RunloopSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - |
| `VercelSandboxClient` | - | - | - | - | - | - |

Expand Down
1 change: 1 addition & 0 deletions examples/run_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"examples/sandbox/docker/mounts/s3_files_mount_read_write.py",
"examples/sandbox/docker/mounts/s3_mount_read_write.py",
"examples/sandbox/extensions/daytona/usaspending_text2sql/setup_db.py",
"examples/sandbox/extensions/islo_runner.py",
"examples/sandbox/extensions/temporal/temporal_sandbox_agent.py",
"examples/sandbox/extensions/vercel_runner.py",
"examples/sandbox/memory_s3.py",
Expand Down
49 changes: 48 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, Blaxel, Vercel, or Islo.

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 @@ -163,6 +163,53 @@ Cloudflare sandboxes support native cloud bucket mounts through
`CloudflareBucketMountStrategy` on `S3Mount`, `R2Mount`, and HMAC-authenticated
`GCSMount`.

## Islo

### Setup

Install the repo extra:

```bash
uv sync --extra islo
```

Export the required environment variables:

```bash
export OPENAI_API_KEY=...
export ISLO_API_KEY=...
```

If you are using a non-default Islo control API endpoint, also export
`ISLO_BASE_URL` or pass `--base-url`. Newer Islo SDK versions may also support
`ISLO_COMPUTE_URL` or `--compute-url` for compute-plane traffic.

### Run

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

Useful flags:

- `--workspace-persistence tar`
- `--workspace-persistence snapshot`
- `--base-url <control-api-url>`
- `--compute-url <compute-api-url>` (requires Islo SDK support)
- `--image <image>`
- `--vcpus 4`
- `--memory-mb 8192`
- `--disk-gb 20`
- `--snapshot-name <snapshot-name>`
- `--pause-on-exit`

The Islo example covers command execution, workspace materialization, and
stop/resume persistence. Islo cloud bucket mounts are available through
`IsloCloudBucketMountStrategy` on `S3Mount`, `R2Mount`, `GCSMount`,
`AzureBlobMount`, and `BoxMount`. PTY and exposed-port URL resolution are
intentionally not demonstrated until the Islo SDK exposes typed APIs for those
features.

## What to expect

Each script asks the model to inspect a small workspace and summarize it. A
Expand Down
281 changes: 281 additions & 0 deletions examples/sandbox/extensions/islo_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
"""
Minimal Islo-backed sandbox example for manual validation.

This example creates a tiny workspace, verifies stop/resume persistence, and lets
the agent inspect the workspace through one shell tool.
"""

import argparse
import asyncio
import io
import os
import sys
import tempfile
from pathlib import Path
from typing import Literal

from openai.types.responses import ResponseTextDeltaEvent

from agents import ModelSettings, Runner
from agents.run import RunConfig
from agents.sandbox import LocalSnapshotSpec, 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 agents.extensions.sandbox import (
IsloSandboxClient,
IsloSandboxClientOptions,
)
except Exception as exc: # pragma: no cover - import path depends on optional extras
raise SystemExit(
"Islo sandbox examples require the optional repo extra.\n"
"Install it with: uv sync --extra islo"
) from exc


DEFAULT_QUESTION = "Summarize this Islo sandbox workspace in 2 sentences."
SNAPSHOT_CHECK_PATH = Path("snapshot-check.txt")
SNAPSHOT_CHECK_CONTENT = "islo snapshot round-trip ok\n"


def _build_manifest() -> Manifest:
return text_manifest(
{
"README.md": (
"# Renewal Notes\n\n"
"This workspace contains a tiny account review packet for manual sandbox testing.\n"
),
"customer.md": (
"# Customer\n\n"
"- Name: Northwind Health.\n"
"- Renewal date: 2026-04-15.\n"
"- Risk: unresolved SSO setup.\n"
),
"next_steps.md": (
"# Next steps\n\n"
"1. Finish the SSO fix.\n"
"2. Confirm legal language before procurement review.\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 _options(
*,
base_url: str | None,
compute_url: str | None,
image: str | None,
vcpus: int | None,
memory_mb: int | None,
disk_gb: int | None,
snapshot_name: str | None,
pause_on_exit: bool,
workspace_persistence: Literal["tar", "snapshot"],
) -> IsloSandboxClientOptions:
return IsloSandboxClientOptions(
base_url=base_url,
compute_url=compute_url,
image=image,
vcpus=vcpus,
memory_mb=memory_mb,
disk_gb=disk_gb,
snapshot_name=snapshot_name,
pause_on_exit=pause_on_exit,
workspace_persistence=workspace_persistence,
)


async def _verify_stop_resume(
*,
base_url: str | None,
compute_url: str | None,
image: str | None,
vcpus: int | None,
memory_mb: int | None,
disk_gb: int | None,
snapshot_name: str | None,
pause_on_exit: bool,
workspace_persistence: Literal["tar", "snapshot"],
) -> None:
client = IsloSandboxClient(base_url=base_url, compute_url=compute_url)
with tempfile.TemporaryDirectory(prefix="islo-snapshot-example-") as snapshot_dir:
sandbox = await client.create(
manifest=_build_manifest(),
snapshot=LocalSnapshotSpec(base_path=Path(snapshot_dir)),
options=_options(
base_url=base_url,
compute_url=compute_url,
image=image,
vcpus=vcpus,
memory_mb=memory_mb,
disk_gb=disk_gb,
snapshot_name=snapshot_name,
pause_on_exit=pause_on_exit,
workspace_persistence=workspace_persistence,
),
)

try:
await sandbox.start()
await sandbox.write(
SNAPSHOT_CHECK_PATH,
io.BytesIO(SNAPSHOT_CHECK_CONTENT.encode("utf-8")),
)
await sandbox.stop()
finally:
await sandbox.shutdown()

resumed_sandbox = await client.resume(sandbox.state)
try:
await resumed_sandbox.start()
restored = await resumed_sandbox.read(SNAPSHOT_CHECK_PATH)
restored_text = restored.read()
if isinstance(restored_text, bytes):
restored_text = restored_text.decode("utf-8")
if restored_text != SNAPSHOT_CHECK_CONTENT:
raise RuntimeError(
"Snapshot resume verification failed: "
f"expected {SNAPSHOT_CHECK_CONTENT!r}, got {restored_text!r}"
)
finally:
await resumed_sandbox.shutdown()

print(f"snapshot round-trip ok (islo, {workspace_persistence})")


async def main(
*,
model: str,
question: str,
base_url: str | None,
compute_url: str | None,
image: str | None,
vcpus: int | None,
memory_mb: int | None,
disk_gb: int | None,
snapshot_name: str | None,
pause_on_exit: bool,
workspace_persistence: Literal["tar", "snapshot"],
stream: bool,
) -> None:
_require_env("OPENAI_API_KEY")
_require_env("ISLO_API_KEY")

await _verify_stop_resume(
base_url=base_url,
compute_url=compute_url,
image=image,
vcpus=vcpus,
memory_mb=memory_mb,
disk_gb=disk_gb,
snapshot_name=snapshot_name,
pause_on_exit=pause_on_exit,
workspace_persistence=workspace_persistence,
)

agent = SandboxAgent(
name="Islo 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=_build_manifest(),
capabilities=[WorkspaceShellCapability()],
model_settings=ModelSettings(tool_choice="required"),
)

run_config = RunConfig(
sandbox=SandboxRunConfig(
client=IsloSandboxClient(base_url=base_url, compute_url=compute_url),
options=_options(
base_url=base_url,
compute_url=compute_url,
image=image,
vcpus=vcpus,
memory_mb=memory_mb,
disk_gb=disk_gb,
snapshot_name=snapshot_name,
pause_on_exit=pause_on_exit,
workspace_persistence=workspace_persistence,
),
),
workflow_name="Islo 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("--base-url", default=None, help="Optional Islo API base URL.")
parser.add_argument("--compute-url", default=None, help="Optional Islo compute API base URL.")
parser.add_argument("--image", default=None, help="Optional Islo sandbox image.")
parser.add_argument("--vcpus", type=int, default=None, help="Optional Islo vCPU count.")
parser.add_argument("--memory-mb", type=int, default=None, help="Optional Islo memory in MB.")
parser.add_argument("--disk-gb", type=int, default=None, help="Optional Islo disk in GB.")
parser.add_argument(
"--snapshot-name",
default=None,
help="Optional Islo snapshot name to use as the sandbox base filesystem.",
)
parser.add_argument(
"--pause-on-exit",
action="store_true",
default=False,
help="Pause the sandbox on shutdown instead of deleting it.",
)
parser.add_argument(
"--workspace-persistence",
default="tar",
choices=["tar", "snapshot"],
help="Workspace persistence mode for the Islo sandbox.",
)
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,
base_url=args.base_url,
compute_url=args.compute_url,
image=args.image,
vcpus=args.vcpus,
memory_mb=args.memory_mb,
disk_gb=args.disk_gb,
snapshot_name=args.snapshot_name,
pause_on_exit=args.pause_on_exit,
workspace_persistence=args.workspace_persistence,
stream=args.stream,
)
)
Loading