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.
- deepagents_docker/__init__.py +7 -0
- deepagents_docker/_docker.py +82 -0
- deepagents_docker/backend.py +292 -0
- deepagents_docker-0.0.1.dist-info/METADATA +139 -0
- deepagents_docker-0.0.1.dist-info/RECORD +7 -0
- deepagents_docker-0.0.1.dist-info/WHEEL +4 -0
- deepagents_docker-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -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)
|
|
33
|
+
[](https://www.python.org/downloads/)
|
|
34
|
+
[](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,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.
|