Skip to content

Commit ea454c4

Browse files
authored
Merge pull request #3 from andybbruno/fix/refactor+tests
Fix/refactor+tests
2 parents f8b9b6f + af13c51 commit ea454c4

11 files changed

Lines changed: 713 additions & 94 deletions

File tree

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [master, main]
7+
8+
jobs:
9+
lint:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: astral-sh/setup-uv@v5
15+
16+
- name: Install dependencies
17+
run: uv sync
18+
19+
- name: Ruff check
20+
run: uv run ruff check .
21+
22+
- name: Ruff format
23+
run: uv run ruff format --check .
24+
25+
test:
26+
runs-on: ubuntu-latest
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
python-version: ["3.12", "3.13"]
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- uses: astral-sh/setup-uv@v5
35+
with:
36+
python-version: ${{ matrix.python-version }}
37+
38+
- name: Install dependencies
39+
run: uv sync
40+
41+
- name: Run tests
42+
run: uv run pytest -v

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,43 @@ with DockerSandbox() as backend:
102102
print("Done!")
103103
```
104104

105+
## Example
106+
107+
The [pizza agent](examples/pizza_agent.py) searches the web for a Neapolitan pizza recipe and writes it to a file in the workspace:
108+
109+
```python
110+
from deepagents import create_deep_agent
111+
from deepagents_docker import DockerSandbox
112+
113+
backend = DockerSandbox(
114+
workspace_dir="examples/data",
115+
allow_outbound_traffic=True,
116+
)
117+
118+
agent = create_deep_agent(
119+
model="openai:gpt-5.5",
120+
backend=backend,
121+
system_prompt="You are a pizza chef.",
122+
)
123+
124+
for step in agent.stream(
125+
{"messages": "Find the best neapolitan pizza recipe and write it to the recipe.md file."},
126+
stream_mode="updates",
127+
):
128+
for update in step.values():
129+
if update and (messages := update.get("messages")):
130+
for message in messages:
131+
message.pretty_print()
132+
```
133+
134+
From a clone of this repo (requires an OpenAI API key):
135+
136+
```bash
137+
uv run python examples/pizza_agent.py
138+
```
139+
140+
The agent writes `recipe.md` under `examples/data/`.
141+
105142
## Development
106143

107144
```bash

examples/pizza_agent.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from deepagents import create_deep_agent
2+
3+
from deepagents_docker import DockerSandbox
4+
5+
backend = DockerSandbox(
6+
workspace_dir="examples/data",
7+
allow_outbound_traffic=True,
8+
)
9+
10+
agent = create_deep_agent(
11+
model="openai:gpt-5.5",
12+
backend=backend,
13+
system_prompt="You are a pizza chef.",
14+
)
15+
16+
if __name__ == "__main__":
17+
for step in agent.stream(
18+
{"messages": "Find the best neapolitan pizza recipe and write it to the recipe.md file."},
19+
stream_mode="updates",
20+
):
21+
for update in step.values():
22+
if update and (messages := update.get("messages")):
23+
for message in messages:
24+
message.pretty_print()

pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ classifiers = [
1919
"Programming Language :: Python :: 3.12",
2020
"Programming Language :: Python :: 3.13",
2121
"Topic :: Software Development :: Libraries :: Python Modules",
22+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
2223
]
2324
dependencies = [
2425
"deepagents>=0.6.7",
@@ -28,6 +29,7 @@ dependencies = [
2829
dev = [
2930
"build>=1.2.0",
3031
"pytest>=9.0.0",
32+
"ruff>=0.9.0",
3133
"twine>=6.0.0",
3234
]
3335

@@ -41,4 +43,18 @@ packages = ["src/deepagents_docker"]
4143
[tool.pytest.ini_options]
4244
testpaths = ["tests"]
4345

46+
[tool.ruff]
47+
target-version = "py312"
48+
line-length = 100
4449

50+
[tool.ruff.lint]
51+
select = [
52+
# isort
53+
"I",
54+
55+
# Pyflakes
56+
"F",
57+
58+
# Pyupgrade
59+
"UP",
60+
]

src/deepagents_docker/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Docker-backed sandbox backend for DeepAgents."""
22

3-
from deepagents_docker.backend import (
4-
DockerSandbox,
5-
)
3+
from .backend import DockerSandbox
4+
from .errors import DockerError
65

7-
__all__ = ["DockerSandbox"]
6+
__all__ = ["DockerError", "DockerSandbox"]

src/deepagents_docker/_docker.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
from collections.abc import Sequence
88
from dataclasses import dataclass
99

10-
11-
class DockerError(RuntimeError):
12-
"""Raised when a Docker CLI invocation fails."""
10+
from .errors import DockerError
1311

1412

1513
@dataclass(frozen=True)

src/deepagents_docker/backend.py

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,28 @@
44

55
import atexit
66
import shlex
7-
import subprocess
87
import tempfile
98
import uuid
109
from pathlib import Path
1110

1211
from deepagents.backends.filesystem import FilesystemBackend
1312
from deepagents.backends.protocol import ExecuteResponse, SandboxBackendProtocol
1413

15-
from deepagents_docker._docker import (
16-
DockerError,
14+
from ._docker import (
1715
docker_available,
1816
format_docker_error,
1917
inspect_container_id,
2018
run_docker,
2119
)
20+
from .errors import DockerError
2221

2322
DEFAULT_EXECUTE_TIMEOUT = 120
2423
DEFAULT_IMAGE = "python:3.12-bookworm"
2524
CONTAINER_WORKDIR = "/workspace"
2625

2726

2827
class DockerSandbox(FilesystemBackend, SandboxBackendProtocol):
29-
"""Filesystem backend with shell commands executed inside a Docker container.
30-
31-
File operations (`ls`, `read`, `write`, `edit`, `grep`, `glob`) run against a
32-
dedicated workspace directory on the host via `FilesystemBackend` with
33-
`virtual_mode=True`. The same directory is bind-mounted into the container at
34-
`/workspace`, and the `execute` tool runs commands there with Docker resource
35-
and security limits.
36-
37-
This is defense in depth, not a perfect isolation boundary. Do not mount
38-
secrets into the workspace, keep Docker patched, and prefer microVMs for
39-
hostile multi-tenant workloads.
40-
"""
28+
"""Docker-backed sandbox backend for DeepAgents."""
4129

4230
def __init__(
4331
self,
@@ -47,8 +35,8 @@ def __init__(
4735
workspace_dir: str | Path | None = None,
4836
timeout: int = DEFAULT_EXECUTE_TIMEOUT,
4937
max_output_bytes: int = 100_000,
50-
memory: str = "512m",
51-
cpus: float = 1.0,
38+
memory: str = "256m",
39+
cpus: float = 0.5,
5240
pids_limit: int = 128,
5341
auto_remove: bool = True,
5442
extra_run_args: list[str] | None = None,
@@ -57,14 +45,14 @@ def __init__(
5745
5846
Args:
5947
image: Docker image for command execution (default: official ``python:3.12-bookworm``).
48+
allow_outbound_traffic: Allow/deny outbound network traffic (default: allow).
6049
workspace_dir: Host directory for agent files. A temporary directory is
6150
created when omitted.
6251
timeout: Default command timeout in seconds.
6352
max_output_bytes: Maximum combined stdout/stderr captured per command.
64-
memory: Docker memory limit (for example ``"512m"``).
53+
memory: Docker memory limit (for example ``"256m"``).
6554
cpus: Docker CPU limit.
6655
pids_limit: Maximum number of PIDs inside the container.
67-
outbound_traffic: Allow/deny outbound network traffic (default: allow).
6856
auto_remove: Remove the container on ``close()``.
6957
extra_run_args: Additional ``docker run`` flags appended before the image.
7058
"""
@@ -146,9 +134,9 @@ def _start_container(self) -> None:
146134
"ALL",
147135
"--read-only",
148136
"--tmpfs",
149-
"/tmp:rw,noexec,nosuid,size=64m",
137+
"/tmp:rw,noexec,nosuid,size=512m",
150138
"--tmpfs",
151-
"/var/tmp:rw,noexec,nosuid,size=64m",
139+
"/var/tmp:rw,noexec,nosuid,size=512m",
152140
"-v",
153141
f"{self._workspace}:{CONTAINER_WORKDIR}:rw",
154142
"-w",
@@ -198,41 +186,41 @@ def execute(
198186
wrapped = self._wrap_command(command)
199187
docker_args = [
200188
"exec",
189+
"-w",
190+
CONTAINER_WORKDIR,
201191
self._container_name,
202192
"sh",
203193
"-c",
204194
wrapped,
205195
]
206196

207197
try:
208-
completed = subprocess.run( # noqa: S602
209-
["docker", *docker_args],
210-
check=False,
211-
capture_output=True,
212-
text=True,
213-
timeout=effective_timeout,
214-
)
215-
except subprocess.TimeoutExpired:
216-
if timeout is not None:
217-
msg = (
218-
f"Error: Command timed out after {effective_timeout} seconds "
219-
"(custom timeout). The command may be stuck or require more time."
198+
completed = run_docker(docker_args, timeout=effective_timeout)
199+
except DockerError as exc:
200+
detail = str(exc)
201+
if "timed out" in detail:
202+
if timeout is not None:
203+
msg = (
204+
f"Error: Command timed out after {effective_timeout} seconds "
205+
"(custom timeout). The command may be stuck or require more time."
206+
)
207+
else:
208+
msg = (
209+
f"Error: Command timed out after {effective_timeout} seconds. "
210+
"For long-running commands, re-run using the timeout parameter."
211+
)
212+
return ExecuteResponse(output=msg, exit_code=124, truncated=False)
213+
if "not found on PATH" in detail:
214+
return ExecuteResponse(
215+
output=(
216+
"Error executing command (FileNotFoundError): "
217+
"docker executable not found on PATH"
218+
),
219+
exit_code=1,
220+
truncated=False,
220221
)
221-
else:
222-
msg = (
223-
f"Error: Command timed out after {effective_timeout} seconds. "
224-
"For long-running commands, re-run using the timeout parameter."
225-
)
226-
return ExecuteResponse(output=msg, exit_code=124, truncated=False)
227-
except FileNotFoundError:
228-
return ExecuteResponse(
229-
output="Error executing command (FileNotFoundError): docker executable not found on PATH",
230-
exit_code=1,
231-
truncated=False,
232-
)
233-
except Exception as exc: # noqa: BLE001
234222
return ExecuteResponse(
235-
output=f"Error executing command ({type(exc).__name__}): {exc}",
223+
output=f"Error executing command (DockerError): {exc}",
236224
exit_code=1,
237225
truncated=False,
238226
)

src/deepagents_docker/errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class DockerError(RuntimeError):
2+
"""Raised when a Docker CLI invocation fails."""

0 commit comments

Comments
 (0)