Skip to content

Commit 841db75

Browse files
committed
Add Declaw sandbox extension
Wires declaw as an optional sandbox backend for the Agents SDK, matching the file layout and install shape already used by the other third-party extensions in this repo. All adapter code lives in the declaw PyPI package; this PR is a thin re-export + pip extra + docs row + smoke test. Users pick the backend with one command: pip install "openai-agents[declaw]" And import the flat shape consistent with the other extensions: from agents.extensions.sandbox import ( DeclawSandboxClient, DeclawSandboxClientOptions, ) Changes: - src/agents/extensions/sandbox/declaw/__init__.py (new, thin re-export) - src/agents/extensions/sandbox/__init__.py (+_HAS_DECLAW block + __all__ extension) - pyproject.toml (new [project.optional-dependencies] entry: declaw>=1.0.4) - tests/extensions/test_sandbox_declaw.py (new, re-export / discriminator smoke) - examples/sandbox/extensions/declaw_runner.py (new, minimal runnable example) - docs/sandbox/clients.md (+ one table row pointing at docs.declaw.ai) CI: the declaw test only imports when the `[declaw]` extra is installed; declaw is pinned >=1.0.4 (already on PyPI) so the existing extras-matrix job resolves it cleanly. Maintenance: the adapter source lives in the declaw package under declaw/openai/. Any breaking change on the declaw side bumps the minimum pin in this file. Reviewers only need to keep the re-export plumbing green — no declaw-specific logic in this repo.
1 parent e80d2d2 commit 841db75

File tree

7 files changed

+272
-1
lines changed

7 files changed

+272
-1
lines changed

docs/sandbox/clients.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ For provider-specific setup notes and links for the checked-in extension example
9393
| `BlaxelSandboxClient` | `openai-agents[blaxel]` | [Blaxel runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/blaxel_runner.py) |
9494
| `CloudflareSandboxClient` | `openai-agents[cloudflare]` | [Cloudflare runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/cloudflare_runner.py) |
9595
| `DaytonaSandboxClient` | `openai-agents[daytona]` | [Daytona runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/daytona/daytona_runner.py) |
96+
| `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)) |
9697
| `E2BSandboxClient` | `openai-agents[e2b]` | [E2B runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/e2b_runner.py) |
9798
| `ModalSandboxClient` | `openai-agents[modal]` | [Modal runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/modal_runner.py) |
9899
| `RunloopSandboxClient` | `openai-agents[runloop]` | [Runloop runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/runloop/runner.py) |
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Minimal declaw-backed sandbox example for manual validation.
2+
3+
Creates a tiny workspace, lets the agent inspect it through the shell
4+
tool, and prints a short answer. Exercises the ``declaw`` backend end
5+
to end via the ``[declaw]`` repo extra.
6+
7+
Credentials (env):
8+
DECLAW_API_KEY your declaw API key
9+
DECLAW_DOMAIN e.g. ``api.declaw.ai``
10+
OPENAI_API_KEY for the OpenAI model
11+
12+
Install:
13+
uv sync --extra declaw --extra dev
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import argparse
19+
import asyncio
20+
import os
21+
import sys
22+
from pathlib import Path
23+
24+
from agents import Runner, set_tracing_disabled
25+
from agents.run import RunConfig
26+
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
27+
28+
if __package__ is None or __package__ == "":
29+
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
30+
31+
from examples.sandbox.misc.example_support import text_manifest
32+
33+
try:
34+
from agents.extensions.sandbox import (
35+
DeclawSandboxClient,
36+
DeclawSandboxClientOptions,
37+
)
38+
except Exception as exc: # pragma: no cover - depends on optional extras
39+
raise SystemExit(
40+
"Declaw sandbox examples require the optional repo extra.\n"
41+
"Install it with: uv sync --extra declaw"
42+
) from exc
43+
44+
45+
DEFAULT_QUESTION = "Summarize the files in this workspace in 2 sentences."
46+
DEFAULT_TEMPLATE = "base"
47+
48+
49+
def _build_manifest() -> Manifest:
50+
return text_manifest(
51+
{
52+
"README.md": (
53+
"# Notes\n\nSmall workspace used to validate the declaw sandbox backend.\n"
54+
),
55+
"todo.md": (
56+
"# Todo\n\n"
57+
"- Confirm the agent can read files.\n"
58+
"- Confirm the shell tool round-trips cleanly.\n"
59+
),
60+
}
61+
)
62+
63+
64+
def _require_env(name: str) -> None:
65+
if not os.environ.get(name):
66+
raise SystemExit(f"{name} must be set before running this example.")
67+
68+
69+
async def _run(*, question: str, template: str, timeout: int) -> None:
70+
# Tracing goes to OpenAI's trace endpoint by default. ZDR-enabled
71+
# orgs get a 403 on every upload — silence the noise.
72+
set_tracing_disabled(True)
73+
74+
for env in ("DECLAW_API_KEY", "DECLAW_DOMAIN", "OPENAI_API_KEY"):
75+
_require_env(env)
76+
77+
client = DeclawSandboxClient()
78+
options = DeclawSandboxClientOptions(template=template, timeout=timeout)
79+
80+
session = await client.create(options=options, manifest=_build_manifest())
81+
82+
try:
83+
agent = SandboxAgent(
84+
name="declaw-runner",
85+
model="gpt-5-mini",
86+
instructions=(
87+
"You are running inside a declaw sandbox. Use the shell "
88+
"tool to list files under /workspace and read anything "
89+
"relevant. Keep the final answer to two sentences."
90+
),
91+
)
92+
93+
result = await Runner.run(
94+
agent,
95+
question,
96+
run_config=RunConfig(sandbox=SandboxRunConfig(session=session)),
97+
max_turns=15,
98+
)
99+
print(result.final_output)
100+
finally:
101+
await client.delete(session=session)
102+
103+
104+
def main() -> None:
105+
parser = argparse.ArgumentParser(description=__doc__)
106+
parser.add_argument("--question", default=DEFAULT_QUESTION)
107+
parser.add_argument("--template", default=DEFAULT_TEMPLATE)
108+
parser.add_argument("--timeout", type=int, default=300)
109+
args = parser.parse_args()
110+
111+
asyncio.run(
112+
_run(
113+
question=args.question,
114+
template=args.template,
115+
timeout=args.timeout,
116+
)
117+
)
118+
119+
120+
if __name__ == "__main__":
121+
main()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ mongodb = ["pymongo>=4.14"]
4848
docker = ["docker>=6.1"]
4949
blaxel = ["blaxel>=0.2.50", "aiohttp>=3.12,<4"]
5050
daytona = ["daytona>=0.155.0"]
51+
declaw = ["declaw>=1.0.4"]
5152
cloudflare = ["aiohttp>=3.12,<4"]
5253
e2b = ["e2b==2.20.0", "e2b-code-interpreter==2.4.1"]
5354
modal = ["modal==1.3.5"]

src/agents/extensions/sandbox/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@
4242
except Exception: # pragma: no cover
4343
_HAS_DAYTONA = False
4444

45+
try:
46+
from .declaw import (
47+
DeclawCloudBucketMountStrategy as DeclawCloudBucketMountStrategy,
48+
DeclawSandboxClient as DeclawSandboxClient,
49+
DeclawSandboxClientOptions as DeclawSandboxClientOptions,
50+
DeclawSandboxSession as DeclawSandboxSession,
51+
DeclawSandboxSessionState as DeclawSandboxSessionState,
52+
DeclawSandboxTimeouts as DeclawSandboxTimeouts,
53+
DeclawSandboxType as DeclawSandboxType,
54+
)
55+
56+
_HAS_DECLAW = True
57+
except Exception: # pragma: no cover
58+
_HAS_DECLAW = False
59+
4560
try:
4661
from .blaxel import (
4762
DEFAULT_BLAXEL_WORKSPACE_ROOT as DEFAULT_BLAXEL_WORKSPACE_ROOT,
@@ -149,6 +164,19 @@
149164
]
150165
)
151166

167+
if _HAS_DECLAW:
168+
__all__.extend(
169+
[
170+
"DeclawCloudBucketMountStrategy",
171+
"DeclawSandboxClient",
172+
"DeclawSandboxClientOptions",
173+
"DeclawSandboxSession",
174+
"DeclawSandboxSessionState",
175+
"DeclawSandboxTimeouts",
176+
"DeclawSandboxType",
177+
]
178+
)
179+
152180
if _HAS_BLAXEL:
153181
__all__.extend(
154182
[
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Declaw sandbox backend for the OpenAI Agents SDK.
2+
3+
Install:
4+
pip install "openai-agents[declaw]"
5+
6+
Credentials (env):
7+
DECLAW_API_KEY your declaw API key
8+
DECLAW_DOMAIN e.g. ``api.declaw.ai``
9+
10+
Docs:
11+
https://docs.declaw.ai
12+
13+
This module is a thin re-export of the adapter that lives in the
14+
``declaw`` PyPI package. Declaw owns the adapter code; breaking
15+
changes bump the declaw pin, not this package.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from declaw.openai import ( # type: ignore[import-untyped]
21+
DeclawCloudBucketMountStrategy as DeclawCloudBucketMountStrategy,
22+
DeclawSandboxClient as DeclawSandboxClient,
23+
DeclawSandboxClientOptions as DeclawSandboxClientOptions,
24+
DeclawSandboxSession as DeclawSandboxSession,
25+
DeclawSandboxSessionState as DeclawSandboxSessionState,
26+
DeclawSandboxTimeouts as DeclawSandboxTimeouts,
27+
DeclawSandboxType as DeclawSandboxType,
28+
)
29+
30+
__all__ = [
31+
"DeclawCloudBucketMountStrategy",
32+
"DeclawSandboxClient",
33+
"DeclawSandboxClientOptions",
34+
"DeclawSandboxSession",
35+
"DeclawSandboxSessionState",
36+
"DeclawSandboxTimeouts",
37+
"DeclawSandboxType",
38+
]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Re-export / registration smoke tests for the declaw sandbox extension.
2+
3+
100% of the adapter code lives in the declaw package; this file only
4+
verifies that the thin re-export wiring works and that the parent
5+
``agents.extensions.sandbox`` namespace exposes declaw the same way it
6+
exposes the other backends.
7+
"""
8+
9+
from agents.extensions.sandbox import (
10+
DeclawSandboxClient,
11+
DeclawSandboxClientOptions,
12+
DeclawSandboxSession,
13+
DeclawSandboxSessionState,
14+
DeclawSandboxType,
15+
)
16+
from agents.extensions.sandbox.declaw import (
17+
DeclawSandboxClient as BackendClient,
18+
)
19+
from agents.sandbox.manifest import Manifest
20+
from agents.sandbox.session.base_sandbox_session import BaseSandboxSession
21+
from agents.sandbox.session.sandbox_client import (
22+
BaseSandboxClient,
23+
BaseSandboxClientOptions,
24+
)
25+
from agents.sandbox.session.sandbox_session_state import SandboxSessionState
26+
from agents.sandbox.snapshot import NoopSnapshot
27+
28+
29+
def test_declaw_backend_id() -> None:
30+
assert DeclawSandboxClient.backend_id == "declaw"
31+
32+
33+
def test_declaw_client_is_base_subclass() -> None:
34+
assert issubclass(DeclawSandboxClient, BaseSandboxClient)
35+
36+
37+
def test_declaw_session_is_base_subclass() -> None:
38+
assert issubclass(DeclawSandboxSession, BaseSandboxSession)
39+
40+
41+
def test_declaw_options_discriminator() -> None:
42+
opts = DeclawSandboxClientOptions(template="base")
43+
assert opts.type == "declaw"
44+
assert isinstance(opts, BaseSandboxClientOptions)
45+
46+
47+
def test_declaw_state_discriminator() -> None:
48+
state = DeclawSandboxSessionState(
49+
sandbox_id="sbx-test",
50+
snapshot=NoopSnapshot(id="snap-test"),
51+
manifest=Manifest(),
52+
)
53+
assert state.type == "declaw"
54+
assert isinstance(state, SandboxSessionState)
55+
56+
57+
def test_parent_and_backend_module_point_at_same_class() -> None:
58+
"""The parent-level re-export and the backend submodule must
59+
expose the same object, not two independent copies."""
60+
assert DeclawSandboxClient is BackendClient
61+
62+
63+
def test_sandbox_type_enum_default() -> None:
64+
assert DeclawSandboxType.DEFAULT.value == "default"

uv.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)