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
4 changes: 4 additions & 0 deletions .github/scripts/run_integration_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,10 @@ def main() -> None:
additional_env["OPENAI_AGENTS_INTEGRATION_REQUIRED_OPTIONAL_EXTRA"] = (
installation.extra
)
if installation.distribution is not None:
additional_env[
"OPENAI_AGENTS_INTEGRATION_REQUIRED_OPTIONAL_DISTRIBUTION"
] = installation.distribution
run_suite(
python,
wheel,
Expand Down
2 changes: 1 addition & 1 deletion examples/sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Most examples call a model through `Runner`, so set `OPENAI_API_KEY` in the repo

## Cloud backend examples

Cloud-provider examples live under [`extensions/`](./extensions/). They cover E2B, Modal, and Daytona sandbox backends and require provider-specific credentials in addition to `OPENAI_API_KEY`.
Cloud-provider examples live under [`extensions/`](./extensions/). They cover CreateOS, E2B, Modal, Daytona, Cloudflare, Runloop, Blaxel, and Vercel sandbox backends and require provider-specific credentials in addition to `OPENAI_API_KEY`.

## Tutorial scaffold

Expand Down
20 changes: 19 additions & 1 deletion examples/sandbox/extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,28 @@ 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 CreateOS, E2B, Modal, Daytona, Cloudflare, Runloop, 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.

## CreateOS

Install the CreateOS extra and configure the provider API key:

```bash
uv sync --extra createos
export CREATEOS_API_KEY=...
export OPENAI_API_KEY=...
```

Run the minimal agent example:

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

The example defaults to the `s-4vcpu-4gb` shape and `devbox:1` root filesystem. Override them with `--shape` and `--rootfs` when your CreateOS environment uses different catalog entries. Add `--pause-on-exit` to preserve the sandbox for a later resumed run; otherwise the runner destroys it during cleanup.

## E2B

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

import argparse
import asyncio
import os
import sys
from pathlib import Path

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 agents.extensions.sandbox import (
DEFAULT_CREATEOS_WORKSPACE_ROOT,
CreateOSSandboxClient,
CreateOSSandboxClientOptions,
)
except Exception as exc: # pragma: no cover - import path depends on optional extras
raise SystemExit(
"CreateOS sandbox examples require the optional repo extra.\n"
"Install it with: uv sync --extra createos"
) from exc


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


def _manifest() -> Manifest:
manifest = text_manifest(
{
"README.md": (
"# CreateOS Demo Workspace\n\n"
"This workspace validates the CreateOS sandbox backend for the Agents SDK.\n"
),
"status.md": (
"# Status\n\n"
"- Sandbox creation is configured.\n"
"- Command execution and file transfer are ready for validation.\n"
),
}
)
return manifest.model_copy(update={"root": DEFAULT_CREATEOS_WORKSPACE_ROOT})


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


async def main(
*,
model: str,
question: str,
shape: str,
rootfs: str | None,
pause_on_exit: bool,
stream: bool,
) -> None:
_require_env("OPENAI_API_KEY")
api_key = _require_env("CREATEOS_API_KEY")

agent = SandboxAgent(
name="CreateOS Sandbox Assistant",
model=model,
instructions=(
"Inspect the sandbox workspace before answering. Keep the answer concise and cite "
"the file names you inspected."
),
default_manifest=_manifest(),
capabilities=[WorkspaceShellCapability()],
model_settings=ModelSettings(tool_choice="required"),
)
client = CreateOSSandboxClient(api_key=api_key)
run_config = RunConfig(
sandbox=SandboxRunConfig(
client=client,
options=CreateOSSandboxClientOptions(
shape=shape,
rootfs=rootfs,
pause_on_exit=pause_on_exit,
),
),
workflow_name="CreateOS sandbox example",
)

try:
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()
finally:
await client.close()


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send.")
parser.add_argument("--shape", default="s-4vcpu-4gb", help="CreateOS sandbox shape.")
parser.add_argument("--rootfs", default="devbox:1", help="CreateOS root filesystem.")
parser.add_argument("--pause-on-exit", action="store_true")
parser.add_argument("--stream", action="store_true")
args = parser.parse_args()
asyncio.run(main(**vars(args)))
12 changes: 11 additions & 1 deletion integration_tests/_contract_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class OptionalDependencyInstallation:
extra: str | None = None
requirement: str | None = None
unsupported_platforms: tuple[str, ...] = ()
distribution: str | None = None

def is_supported_on_current_platform(self) -> bool:
return sys.platform not in self.unsupported_platforms
Expand Down Expand Up @@ -113,7 +114,7 @@ def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
f"optional dependency installation for {module_name} must be an object"
)
unknown_fields = sorted(
set(installation) - {"extra", "requirement", "unsupported_platforms"}
set(installation) - {"extra", "requirement", "unsupported_platforms", "distribution"}
)
if unknown_fields:
raise ValueError(
Expand All @@ -133,6 +134,14 @@ def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
f"optional dependency installation {field_name} for {module_name} must be a "
"non-empty string"
)
distribution = installation.get("distribution")
if distribution is not None and (
field_name != "extra" or type(distribution) is not str or not distribution
):
raise ValueError(
f"optional dependency installation distribution for {module_name} must be a "
"non-empty string declared with an extra"
)
unsupported_platforms = installation.get("unsupported_platforms", [])
if (
not isinstance(unsupported_platforms, list)
Expand All @@ -149,6 +158,7 @@ def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
extra=install_value if field_name == "extra" else None,
requirement=install_value if field_name == "requirement" else None,
unsupported_platforms=tuple(unsupported_platforms),
distribution=distribution,
)
)

Expand Down
30 changes: 29 additions & 1 deletion integration_tests/packaging/test_released_api_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
REQUIRED_OPTIONAL_DEPENDENCIES_ENV = "OPENAI_AGENTS_INTEGRATION_REQUIRED_OPTIONAL_DEPENDENCIES"
OPTIONAL_DEPENDENCY_INSTALLATION_ENV = "OPENAI_AGENTS_INTEGRATION_OPTIONAL_DEPENDENCY_INSTALLATION"
REQUIRED_OPTIONAL_EXTRA_ENV = "OPENAI_AGENTS_INTEGRATION_REQUIRED_OPTIONAL_EXTRA"
REQUIRED_OPTIONAL_DISTRIBUTION_ENV = "OPENAI_AGENTS_INTEGRATION_REQUIRED_OPTIONAL_DISTRIBUTION"


def _distributions_declared_by_extra(requirement_strings: list[str], extra: str) -> set[str]:
Expand All @@ -38,6 +39,7 @@ def _extra_metadata_error(
*,
extra: str,
dependency_module: str,
distribution: str | None = None,
provided_extras: list[str],
requirement_strings: list[str],
) -> str | None:
Expand All @@ -51,7 +53,7 @@ def _extra_metadata_error(
"the extra under [project.optional-dependencies]."
)

distribution_name = canonicalize_name(dependency_module)
distribution_name = canonicalize_name(distribution or dependency_module)
declared_distributions = _distributions_declared_by_extra(requirement_strings, extra)
if distribution_name not in declared_distributions:
return (
Expand Down Expand Up @@ -126,6 +128,7 @@ def test_artifact_extra_declares_its_policy_dependency() -> None:
error = _extra_metadata_error(
extra=extra,
dependency_module=dependency_module,
distribution=os.environ.get(REQUIRED_OPTIONAL_DISTRIBUTION_ENV),
provided_extras=metadata("openai-agents").get_all("Provides-Extra") or [],
requirement_strings=requires("openai-agents") or [],
)
Expand Down Expand Up @@ -173,6 +176,31 @@ def test_extra_metadata_provenance_rejects_unknown_extra() -> None:
)


def test_extra_metadata_provenance_uses_declared_distribution() -> None:
requirement_strings = ['createos-sandbox>=0.1.0,<0.2; extra == "createos"']

assert (
_extra_metadata_error(
extra="createos",
dependency_module="createos",
distribution="createos-sandbox",
provided_extras=["createos"],
requirement_strings=requirement_strings,
)
is None
)
assert (
_extra_metadata_error(
extra="createos",
dependency_module="createos",
distribution="createos-sandbox",
provided_extras=["createos"],
requirement_strings=['other-package>=1; extra == "createos"'],
)
is not None
)


@pytest.mark.packaging_dependency
def test_installed_distribution_preserves_released_public_api_contract() -> None:
contract = load_api_contract(CONTRACT)
Expand Down
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ dapr = [
]
mongodb = ["pymongo>=4.14"]
docker = ["docker>=6.1"]
createos = ["createos-sandbox>=0.1.0,<0.2"]
blaxel = ["blaxel>=0.2.50", "aiohttp>=3.14.3,<4"]
daytona = [
"daytona>=0.155.0",
Expand Down Expand Up @@ -195,6 +196,10 @@ disallow_untyped_calls = false
module = "sounddevice.*"
ignore_missing_imports = true

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

[[tool.mypy.overrides]]
module = ["modal", "modal.*"]
ignore_missing_imports = true
Expand Down Expand Up @@ -268,10 +273,10 @@ format-command = "ruff format --stdin-filename {filename}"

[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { openai = false }
exclude-newer-package = { createos-sandbox = false, openai = false }
index-strategy = "first-index"

[tool.uv.pip]
exclude-newer = "7 days"
exclude-newer-package = { openai = false }
exclude-newer-package = { createos-sandbox = false, openai = false }
index-strategy = "first-index"
30 changes: 30 additions & 0 deletions src/agents/extensions/sandbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
from importlib.util import find_spec

try:
if find_spec("createos") is None:
raise ImportError("The optional CreateOS dependency is not installed")
from .createos import (
DEFAULT_CREATEOS_WORKSPACE_ROOT as DEFAULT_CREATEOS_WORKSPACE_ROOT,
CreateOSSandboxClient as CreateOSSandboxClient,
CreateOSSandboxClientOptions as CreateOSSandboxClientOptions,
CreateOSSandboxSession as CreateOSSandboxSession,
CreateOSSandboxSessionState as CreateOSSandboxSessionState,
CreateOSSandboxTimeouts as CreateOSSandboxTimeouts,
)

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

try:
from .e2b import (
E2BCloudBucketMountStrategy as E2BCloudBucketMountStrategy,
Expand Down Expand Up @@ -113,6 +131,18 @@

__all__: list[str] = []

if _HAS_CREATEOS:
__all__.extend(
[
"DEFAULT_CREATEOS_WORKSPACE_ROOT",
"CreateOSSandboxClient",
"CreateOSSandboxClientOptions",
"CreateOSSandboxSession",
"CreateOSSandboxSessionState",
"CreateOSSandboxTimeouts",
]
)

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

from .sandbox import (
DEFAULT_CREATEOS_WORKSPACE_ROOT,
CreateOSSandboxClient,
CreateOSSandboxClientOptions,
CreateOSSandboxSession,
CreateOSSandboxSessionState,
CreateOSSandboxTimeouts,
)

__all__ = [
"DEFAULT_CREATEOS_WORKSPACE_ROOT",
"CreateOSSandboxClient",
"CreateOSSandboxClientOptions",
"CreateOSSandboxSession",
"CreateOSSandboxSessionState",
"CreateOSSandboxTimeouts",
]
Loading
Loading