deepagents-docker 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ """Docker-backed sandbox backend for DeepAgents."""
2
+
3
+ from deepagents_docker.backend import (
4
+ DockerSandbox,
5
+ )
6
+
7
+ __all__ = ["DockerSandbox"]
@@ -0,0 +1,82 @@
1
+ """Low-level helpers for invoking the Docker CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from collections.abc import Sequence
8
+ from dataclasses import dataclass
9
+
10
+
11
+ class DockerError(RuntimeError):
12
+ """Raised when a Docker CLI invocation fails."""
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class DockerRunResult:
17
+ """Captured output from a Docker CLI subprocess."""
18
+
19
+ returncode: int
20
+ stdout: str
21
+ stderr: str
22
+
23
+
24
+ def run_docker(
25
+ args: Sequence[str],
26
+ *,
27
+ timeout: float | None = None,
28
+ input_text: str | None = None,
29
+ ) -> DockerRunResult:
30
+ """Run `docker` with the given arguments."""
31
+ try:
32
+ completed = subprocess.run( # noqa: S603
33
+ ["docker", *args],
34
+ check=False,
35
+ capture_output=True,
36
+ text=True,
37
+ timeout=timeout,
38
+ input=input_text,
39
+ )
40
+ except subprocess.TimeoutExpired as exc:
41
+ msg = f"docker command timed out after {timeout} seconds"
42
+ raise DockerError(msg) from exc
43
+ except FileNotFoundError as exc:
44
+ msg = "docker executable not found on PATH"
45
+ raise DockerError(msg) from exc
46
+
47
+ return DockerRunResult(
48
+ returncode=completed.returncode,
49
+ stdout=completed.stdout or "",
50
+ stderr=completed.stderr or "",
51
+ )
52
+
53
+
54
+ def docker_available() -> bool:
55
+ """Return True when the Docker daemon responds to `docker info`."""
56
+ result = run_docker(["info", "--format", "{{.ServerVersion}}"])
57
+ return result.returncode == 0
58
+
59
+
60
+ def inspect_container_id(container_name: str) -> str:
61
+ """Return the container ID for a running container name."""
62
+ result = run_docker(
63
+ ["inspect", "--format", "{{.Id}}", container_name],
64
+ )
65
+ if result.returncode != 0:
66
+ msg = result.stderr.strip() or f"failed to inspect container {container_name!r}"
67
+ raise DockerError(msg)
68
+ return result.stdout.strip()
69
+
70
+
71
+ def format_docker_error(result: DockerRunResult) -> str:
72
+ """Combine stderr/stdout into a single error string."""
73
+ detail = (result.stderr or result.stdout).strip()
74
+ if not detail:
75
+ detail = f"exit code {result.returncode}"
76
+ try:
77
+ payload = json.loads(detail)
78
+ except json.JSONDecodeError:
79
+ return detail
80
+ if isinstance(payload, dict) and "message" in payload:
81
+ return str(payload["message"])
82
+ return detail
@@ -0,0 +1,292 @@
1
+ """DockerSandbox: isolated shell execution with host-backed workspace files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import shlex
7
+ import subprocess
8
+ import tempfile
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ from deepagents.backends.filesystem import FilesystemBackend
13
+ from deepagents.backends.protocol import ExecuteResponse, SandboxBackendProtocol
14
+
15
+ from deepagents_docker._docker import (
16
+ DockerError,
17
+ docker_available,
18
+ format_docker_error,
19
+ inspect_container_id,
20
+ run_docker,
21
+ )
22
+
23
+ DEFAULT_EXECUTE_TIMEOUT = 120
24
+ DEFAULT_IMAGE = "python:3.12-bookworm"
25
+ CONTAINER_WORKDIR = "/workspace"
26
+
27
+
28
+ 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
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ *,
45
+ image: str = DEFAULT_IMAGE,
46
+ allow_outbound_traffic: bool = True,
47
+ workspace_dir: str | Path | None = None,
48
+ timeout: int = DEFAULT_EXECUTE_TIMEOUT,
49
+ max_output_bytes: int = 100_000,
50
+ memory: str = "512m",
51
+ cpus: float = 1.0,
52
+ pids_limit: int = 128,
53
+ auto_remove: bool = True,
54
+ extra_run_args: list[str] | None = None,
55
+ ) -> None:
56
+ """Create a sandbox container and workspace directory.
57
+
58
+ Args:
59
+ image: Docker image for command execution (default: official ``python:3.12-bookworm``).
60
+ workspace_dir: Host directory for agent files. A temporary directory is
61
+ created when omitted.
62
+ timeout: Default command timeout in seconds.
63
+ max_output_bytes: Maximum combined stdout/stderr captured per command.
64
+ memory: Docker memory limit (for example ``"512m"``).
65
+ cpus: Docker CPU limit.
66
+ pids_limit: Maximum number of PIDs inside the container.
67
+ outbound_traffic: Allow/deny outbound network traffic (default: allow).
68
+ auto_remove: Remove the container on ``close()``.
69
+ extra_run_args: Additional ``docker run`` flags appended before the image.
70
+ """
71
+ if timeout <= 0:
72
+ msg = f"timeout must be positive, got {timeout}"
73
+ raise ValueError(msg)
74
+ if cpus <= 0:
75
+ msg = f"cpus must be positive, got {cpus}"
76
+ raise ValueError(msg)
77
+ if pids_limit <= 0:
78
+ msg = f"pids_limit must be positive, got {pids_limit}"
79
+ raise ValueError(msg)
80
+
81
+ self._owns_workspace = workspace_dir is None
82
+ self._workspace = Path(
83
+ tempfile.mkdtemp(prefix="deepagents-docker-")
84
+ if workspace_dir is None
85
+ else workspace_dir,
86
+ ).resolve()
87
+ self._workspace.mkdir(parents=True, exist_ok=True)
88
+
89
+ super().__init__(
90
+ root_dir=self._workspace,
91
+ virtual_mode=True,
92
+ max_file_size_mb=10,
93
+ )
94
+
95
+ self._image = image
96
+ self._default_timeout = timeout
97
+ self._max_output_bytes = max_output_bytes
98
+ self._memory = memory
99
+ self._cpus = cpus
100
+ self._pids_limit = pids_limit
101
+ self._network_mode = "bridge" if allow_outbound_traffic else "none"
102
+ self._auto_remove = auto_remove
103
+ self._extra_run_args = list(extra_run_args or [])
104
+
105
+ self._container_name = f"deepagents-docker-{uuid.uuid4().hex[:12]}"
106
+ self._container_id: str | None = None
107
+ self._closed = False
108
+
109
+ self._start_container()
110
+ atexit.register(self.close)
111
+
112
+ @property
113
+ def workspace_dir(self) -> Path:
114
+ """Host path backing the agent workspace."""
115
+ return self._workspace
116
+
117
+ @property
118
+ def id(self) -> str:
119
+ """Unique identifier for this sandbox instance."""
120
+ return self._container_id or self._container_name
121
+
122
+ def _start_container(self) -> None:
123
+ if not docker_available():
124
+ msg = (
125
+ "Docker is not available. Install Docker, ensure the daemon is running, "
126
+ f"and pull the default image with `docker pull {DEFAULT_IMAGE}`"
127
+ )
128
+ raise DockerError(msg)
129
+
130
+ run_args = [
131
+ "run",
132
+ "-d",
133
+ "--name",
134
+ self._container_name,
135
+ "--network",
136
+ self._network_mode,
137
+ "--cpus",
138
+ str(self._cpus),
139
+ "--memory",
140
+ self._memory,
141
+ "--pids-limit",
142
+ str(self._pids_limit),
143
+ "--security-opt",
144
+ "no-new-privileges",
145
+ "--cap-drop",
146
+ "ALL",
147
+ "--read-only",
148
+ "--tmpfs",
149
+ "/tmp:rw,noexec,nosuid,size=64m",
150
+ "--tmpfs",
151
+ "/var/tmp:rw,noexec,nosuid,size=64m",
152
+ "-v",
153
+ f"{self._workspace}:{CONTAINER_WORKDIR}:rw",
154
+ "-w",
155
+ CONTAINER_WORKDIR,
156
+ *self._extra_run_args,
157
+ self._image,
158
+ "sleep",
159
+ "infinity",
160
+ ]
161
+ result = run_docker(run_args)
162
+ if result.returncode != 0:
163
+ msg = format_docker_error(result)
164
+ raise DockerError(f"failed to start sandbox container: {msg}")
165
+
166
+ self._container_id = inspect_container_id(self._container_name)
167
+
168
+ def _wrap_command(self, command: str) -> str:
169
+ """Run agent commands from the container workspace directory."""
170
+ return f"cd {shlex.quote(CONTAINER_WORKDIR)} && {command}"
171
+
172
+ def execute(
173
+ self,
174
+ command: str,
175
+ *,
176
+ timeout: int | None = None,
177
+ ) -> ExecuteResponse:
178
+ """Execute a shell command inside the sandbox container."""
179
+ if self._closed:
180
+ return ExecuteResponse(
181
+ output="Error: Sandbox has been closed.",
182
+ exit_code=1,
183
+ truncated=False,
184
+ )
185
+
186
+ if not command or not isinstance(command, str):
187
+ return ExecuteResponse(
188
+ output="Error: Command must be a non-empty string.",
189
+ exit_code=1,
190
+ truncated=False,
191
+ )
192
+
193
+ effective_timeout = timeout if timeout is not None else self._default_timeout
194
+ if effective_timeout <= 0:
195
+ msg = f"timeout must be positive, got {effective_timeout}"
196
+ raise ValueError(msg)
197
+
198
+ wrapped = self._wrap_command(command)
199
+ docker_args = [
200
+ "exec",
201
+ self._container_name,
202
+ "sh",
203
+ "-c",
204
+ wrapped,
205
+ ]
206
+
207
+ 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."
220
+ )
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
234
+ return ExecuteResponse(
235
+ output=f"Error executing command ({type(exc).__name__}): {exc}",
236
+ exit_code=1,
237
+ truncated=False,
238
+ )
239
+
240
+ output_parts: list[str] = []
241
+ if completed.stdout:
242
+ output_parts.append(completed.stdout)
243
+ if completed.stderr:
244
+ stderr_lines = completed.stderr.strip().split("\n")
245
+ output_parts.extend(f"[stderr] {line}" for line in stderr_lines)
246
+
247
+ output = "\n".join(output_parts) if output_parts else "<no output>"
248
+ truncated = False
249
+ if len(output) > self._max_output_bytes:
250
+ output = output[: self._max_output_bytes]
251
+ output += f"\n\n... Output truncated at {self._max_output_bytes} bytes."
252
+ truncated = True
253
+
254
+ if completed.returncode != 0:
255
+ output = f"{output.rstrip()}\n\nExit code: {completed.returncode}"
256
+
257
+ return ExecuteResponse(
258
+ output=output,
259
+ exit_code=completed.returncode,
260
+ truncated=truncated,
261
+ )
262
+
263
+ def close(self) -> None:
264
+ """Stop and remove the sandbox container."""
265
+ if self._closed:
266
+ return
267
+ self._closed = True
268
+
269
+ stop = run_docker(["stop", "-t", "2", self._container_name], timeout=30)
270
+ if stop.returncode != 0 and "No such container" not in stop.stderr:
271
+ # Best-effort shutdown; container may already be gone.
272
+ pass
273
+
274
+ if self._auto_remove:
275
+ run_docker(["rm", "-f", self._container_name], timeout=30)
276
+
277
+ if self._owns_workspace:
278
+ import shutil
279
+
280
+ shutil.rmtree(self._workspace, ignore_errors=True)
281
+
282
+ def __enter__(self) -> DockerSandbox:
283
+ return self
284
+
285
+ def __exit__(self, *_exc: object) -> None:
286
+ self.close()
287
+
288
+ def __del__(self) -> None:
289
+ try:
290
+ self.close()
291
+ except Exception: # noqa: BLE001, S110
292
+ pass
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepagents-docker
3
+ Version: 0.0.1
4
+ Summary: Docker-backed sandbox backend for DeepAgents
5
+ Project-URL: Homepage, https://github.com/andybbruno/deepagents-docker
6
+ Project-URL: Repository, https://github.com/andybbruno/deepagents-docker
7
+ Author: Andrea Bruno
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,deepagents,docker,langchain,langgraph,sandbox
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: deepagents>=0.6.7
20
+ Description-Content-Type: text/markdown
21
+
22
+ <p align="center">
23
+ <img src="assets/deepagents-docker-banner.png" width="800" />
24
+ </p>
25
+ <div align="center">
26
+ <h3>Docker sandbox backend for <a href="https://github.com/langchain-ai/deepagents">Deep Agents</a>.</h3>
27
+ </div>
28
+
29
+
30
+ <div align="center">
31
+
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
33
+ [![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://www.python.org/downloads/)
34
+ [![deepagents](https://img.shields.io/badge/built%20for-deepagents-orange)](https://github.com/langchain-ai/deepagents)
35
+
36
+ </div>
37
+
38
+
39
+ ## Quickstart
40
+
41
+ Requires [Docker](https://docs.docker.com/get-docker/) on your machine.
42
+
43
+ Install with `uv`:
44
+ ```bash
45
+ uv add deepagents-docker
46
+ ```
47
+ or with `pip`:
48
+ ```bash
49
+ pip install deepagents-docker
50
+ ```
51
+
52
+ ```python
53
+ from deepagents import create_deep_agent
54
+ from deepagents_docker import DockerSandbox
55
+
56
+ agent = create_deep_agent(
57
+ model="openai:gpt-5.5",
58
+ backend=DockerSandbox(),
59
+ system_prompt="You are a research assistant.",
60
+ )
61
+
62
+ result = agent.invoke({"messages": "Research the latest trends in AI and write a summary."})
63
+ ```
64
+
65
+ ## Configuration
66
+
67
+ Constructor options let you change the image, workspace path, command timeout, resource limits, outbound network access, and any extra `docker run` flags:
68
+
69
+ ```python
70
+ DockerSandbox(
71
+ image="python:3.12-bookworm", # default image (Debian-based, includes curl, etc.)
72
+ allow_outbound_traffic=True, # False → no network; True (default) → allow outbound traffic
73
+ workspace_dir="/path/to/project", # host dir for agent files; see note below
74
+ timeout=120, # per-command timeout (seconds)
75
+ max_output_bytes=100_000, # combined stdout/stderr cap per command
76
+ memory="512m",
77
+ cpus=1.0,
78
+ pids_limit=128,
79
+ auto_remove=True, # remove container on close()
80
+ extra_run_args=["--env", "FOO=bar"],
81
+ )
82
+ ```
83
+
84
+ > [!NOTE]
85
+ > When `workspace_dir` is omitted, a temporary directory is created under the host temp folder and **removed on `close()`** when the sandbox owns it. Pass an explicit path to keep files after the container stops.
86
+
87
+
88
+ ## How it works
89
+
90
+ `DockerSandbox` implements the Deep Agents backend protocol by splitting work across the host and a container:
91
+
92
+ - **File tools** (`read`, `write`, `edit`, `grep`, `glob`, `ls`) run against a workspace directory on your machine.
93
+ - **`execute`** runs shell commands in a long-lived Docker container. The same directory is bind-mounted at `/workspace`, so files stay in sync between tools and commands.
94
+
95
+ On startup, the sandbox creates a container with conservative defaults:
96
+
97
+ - [`python:3.12-bookworm`](https://hub.docker.com/_/python) as the default image
98
+ - Outbound traffic allowed by default
99
+ - No elevated Linux privileges
100
+ - Read-only root filesystem (with small `tmpfs` mounts for `/tmp` and `/var/tmp`)
101
+ - Memory, CPU, and PID limits
102
+
103
+
104
+ > [!NOTE]
105
+ > The container is stopped and removed automatically when the Python process exits (`atexit`). Use a context manager (below) to tear down earlier.
106
+
107
+
108
+ ### Using a context manager
109
+
110
+ Use a context manager when you want the container stopped and removed as soon as you leave the block:
111
+
112
+ ```python
113
+ from deepagents import create_deep_agent
114
+ from deepagents_docker import DockerSandbox
115
+
116
+ with DockerSandbox() as backend:
117
+ agent = create_deep_agent(model="openai:gpt-5.5", backend=backend)
118
+ agent.invoke({"messages": "..."})
119
+
120
+ # Container stopped and removed here.
121
+ print("Done!")
122
+ ```
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ git clone https://github.com/andybbruno/deepagents-docker.git
128
+ cd deepagents-docker
129
+ uv sync
130
+ uv run pytest
131
+ ```
132
+
133
+ ## Security
134
+
135
+ Use this for trusted workloads and development, not as a hard multi-tenant boundary. Do not put secrets in the workspace. See [Deep Agents security](https://github.com/langchain-ai/deepagents?tab=security-ov-file).
136
+
137
+ ## License
138
+
139
+ MIT — [LICENSE](LICENSE).
@@ -0,0 +1,7 @@
1
+ deepagents_docker/__init__.py,sha256=9rLXj5-z9zLXbcCmxWatLYJR2xs2SQn30-fugCdlKhM,143
2
+ deepagents_docker/_docker.py,sha256=LShf7lHkefxDc4LZPgttCpnBZVg_jSCRP8EcEjqdFRk,2363
3
+ deepagents_docker/backend.py,sha256=4PuPPLCbwtLaTI3csA8fNx5PSoAxz-vAqLODU7lWJSQ,10089
4
+ deepagents_docker-0.0.1.dist-info/METADATA,sha256=85MCxDk-3QF-JIo3aLd7u9AXSuq-otgXbqwW8_btyCk,4705
5
+ deepagents_docker-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ deepagents_docker-0.0.1.dist-info/licenses/LICENSE,sha256=gjFKP2GBEc-AJ8kf_hWZ4UQ_bWoEpy1PbJoTenKEqSo,1095
7
+ deepagents_docker-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepAgents Docker Sandbox contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.