44
55import atexit
66import shlex
7- import subprocess
87import tempfile
98import uuid
109from pathlib import Path
1110
1211from deepagents .backends .filesystem import FilesystemBackend
1312from 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
2322DEFAULT_EXECUTE_TIMEOUT = 120
2423DEFAULT_IMAGE = "python:3.12-bookworm"
2524CONTAINER_WORKDIR = "/workspace"
2625
2726
2827class 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 )
0 commit comments