Skip to content
Draft
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
1 change: 1 addition & 0 deletions docs/sandbox/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ For provider-specific setup notes and links for the checked-in extension example
| `BlaxelSandboxClient` | `openai-agents[blaxel]` | [Blaxel runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/blaxel_runner.py) |
| `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) |
| `DeclawSandboxClient` | `openai-agents[declaw]` | [Declaw runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/declaw_runner.py) ([docs](https://docs.declaw.ai)) |
| `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) |
Expand Down
121 changes: 121 additions & 0 deletions examples/sandbox/extensions/declaw_runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Minimal declaw-backed sandbox example for manual validation.

Creates a tiny workspace, lets the agent inspect it through the shell
tool, and prints a short answer. Exercises the ``declaw`` backend end
to end via the ``[declaw]`` repo extra.

Credentials (env):
DECLAW_API_KEY your declaw API key
DECLAW_DOMAIN e.g. ``api.declaw.ai``
OPENAI_API_KEY for the OpenAI model

Install:
uv sync --extra declaw --extra dev
"""

from __future__ import annotations

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

from agents import Runner, set_tracing_disabled
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

try:
from agents.extensions.sandbox import (
DeclawSandboxClient,
DeclawSandboxClientOptions,
)
except Exception as exc: # pragma: no cover - depends on optional extras
raise SystemExit(
"Declaw sandbox examples require the optional repo extra.\n"
"Install it with: uv sync --extra declaw"
) from exc


DEFAULT_QUESTION = "Summarize the files in this workspace in 2 sentences."
DEFAULT_TEMPLATE = "base"


def _build_manifest() -> Manifest:
return text_manifest(
{
"README.md": (
"# Notes\n\nSmall workspace used to validate the declaw sandbox backend.\n"
),
"todo.md": (
"# Todo\n\n"
"- Confirm the agent can read files.\n"
"- Confirm the shell tool round-trips cleanly.\n"
),
}
)


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


async def _run(*, question: str, template: str, timeout: int) -> None:
# Tracing goes to OpenAI's trace endpoint by default. ZDR-enabled
# orgs get a 403 on every upload — silence the noise.
set_tracing_disabled(True)

for env in ("DECLAW_API_KEY", "DECLAW_DOMAIN", "OPENAI_API_KEY"):
_require_env(env)

client = DeclawSandboxClient()
options = DeclawSandboxClientOptions(template=template, timeout=timeout)

session = await client.create(options=options, manifest=_build_manifest())

try:
agent = SandboxAgent(
name="declaw-runner",
model="gpt-5-mini",
instructions=(
"You are running inside a declaw sandbox. Use the shell "
"tool to list files under /workspace and read anything "
"relevant. Keep the final answer to two sentences."
),
)

result = await Runner.run(
agent,
question,
run_config=RunConfig(sandbox=SandboxRunConfig(session=session)),
max_turns=15,
)
print(result.final_output)
finally:
await client.delete(session=session)


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--question", default=DEFAULT_QUESTION)
parser.add_argument("--template", default=DEFAULT_TEMPLATE)
parser.add_argument("--timeout", type=int, default=300)
args = parser.parse_args()

asyncio.run(
_run(
question=args.question,
template=args.template,
timeout=args.timeout,
)
)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mongodb = ["pymongo>=4.14"]
docker = ["docker>=6.1"]
blaxel = ["blaxel>=0.2.50", "aiohttp>=3.12,<4"]
daytona = ["daytona>=0.155.0"]
declaw = ["declaw>=1.0.4"]
cloudflare = ["aiohttp>=3.12,<4"]
e2b = ["e2b==2.20.0", "e2b-code-interpreter==2.4.1"]
modal = ["modal==1.3.5"]
Expand Down
28 changes: 28 additions & 0 deletions src/agents/extensions/sandbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@
except Exception: # pragma: no cover
_HAS_DAYTONA = False

try:
from .declaw import (
DeclawCloudBucketMountStrategy as DeclawCloudBucketMountStrategy,
DeclawSandboxClient as DeclawSandboxClient,
DeclawSandboxClientOptions as DeclawSandboxClientOptions,
DeclawSandboxSession as DeclawSandboxSession,
DeclawSandboxSessionState as DeclawSandboxSessionState,
DeclawSandboxTimeouts as DeclawSandboxTimeouts,
DeclawSandboxType as DeclawSandboxType,
)

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

try:
from .blaxel import (
DEFAULT_BLAXEL_WORKSPACE_ROOT as DEFAULT_BLAXEL_WORKSPACE_ROOT,
Expand Down Expand Up @@ -149,6 +164,19 @@
]
)

if _HAS_DECLAW:
__all__.extend(
[
"DeclawCloudBucketMountStrategy",
"DeclawSandboxClient",
"DeclawSandboxClientOptions",
"DeclawSandboxSession",
"DeclawSandboxSessionState",
"DeclawSandboxTimeouts",
"DeclawSandboxType",
]
)

if _HAS_BLAXEL:
__all__.extend(
[
Expand Down
38 changes: 38 additions & 0 deletions src/agents/extensions/sandbox/declaw/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Declaw sandbox backend for the OpenAI Agents SDK.

Install:
pip install "openai-agents[declaw]"

Credentials (env):
DECLAW_API_KEY your declaw API key
DECLAW_DOMAIN e.g. ``api.declaw.ai``

Docs:
https://docs.declaw.ai

This module is a thin re-export of the adapter that lives in the
``declaw`` PyPI package. Declaw owns the adapter code; breaking
changes bump the declaw pin, not this package.
"""

from __future__ import annotations

from declaw.openai import ( # type: ignore[import-untyped]
DeclawCloudBucketMountStrategy as DeclawCloudBucketMountStrategy,
DeclawSandboxClient as DeclawSandboxClient,
DeclawSandboxClientOptions as DeclawSandboxClientOptions,
DeclawSandboxSession as DeclawSandboxSession,
DeclawSandboxSessionState as DeclawSandboxSessionState,
DeclawSandboxTimeouts as DeclawSandboxTimeouts,
DeclawSandboxType as DeclawSandboxType,
)

__all__ = [
"DeclawCloudBucketMountStrategy",
"DeclawSandboxClient",
"DeclawSandboxClientOptions",
"DeclawSandboxSession",
"DeclawSandboxSessionState",
"DeclawSandboxTimeouts",
"DeclawSandboxType",
]
64 changes: 64 additions & 0 deletions tests/extensions/test_sandbox_declaw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Re-export / registration smoke tests for the declaw sandbox extension.

100% of the adapter code lives in the declaw package; this file only
verifies that the thin re-export wiring works and that the parent
``agents.extensions.sandbox`` namespace exposes declaw the same way it
exposes the other backends.
"""

from agents.extensions.sandbox import (
DeclawSandboxClient,
DeclawSandboxClientOptions,
DeclawSandboxSession,
DeclawSandboxSessionState,
DeclawSandboxType,
)
from agents.extensions.sandbox.declaw import (
DeclawSandboxClient as BackendClient,
)
from agents.sandbox.manifest import Manifest
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
from agents.sandbox.session.sandbox_client import (
BaseSandboxClient,
BaseSandboxClientOptions,
)
from agents.sandbox.session.sandbox_session_state import SandboxSessionState
from agents.sandbox.snapshot import NoopSnapshot


def test_declaw_backend_id() -> None:
assert DeclawSandboxClient.backend_id == "declaw"


def test_declaw_client_is_base_subclass() -> None:
assert issubclass(DeclawSandboxClient, BaseSandboxClient)


def test_declaw_session_is_base_subclass() -> None:
assert issubclass(DeclawSandboxSession, BaseSandboxSession)


def test_declaw_options_discriminator() -> None:
opts = DeclawSandboxClientOptions(template="base")
assert opts.type == "declaw"
assert isinstance(opts, BaseSandboxClientOptions)


def test_declaw_state_discriminator() -> None:
state = DeclawSandboxSessionState(
sandbox_id="sbx-test",
snapshot=NoopSnapshot(id="snap-test"),
manifest=Manifest(),
)
assert state.type == "declaw"
assert isinstance(state, SandboxSessionState)


def test_parent_and_backend_module_point_at_same_class() -> None:
"""The parent-level re-export and the backend submodule must
expose the same object, not two independent copies."""
assert DeclawSandboxClient is BackendClient


def test_sandbox_type_enum_default() -> None:
assert DeclawSandboxType.DEFAULT.value == "default"
20 changes: 19 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading