@@ -266,10 +266,114 @@ def create_daytona_sandbox(
266266 console .print (f"[yellow]⚠ Cleanup failed: { e } [/yellow]" )
267267
268268
269+ @contextmanager
270+ def create_docker_sandbox (
271+ * , sandbox_id : str | None = None , setup_script_path : str | None = None
272+ ) -> Generator [SandboxBackendProtocol , None , None ]:
273+ """Create or connect to Docker sandbox.
274+
275+ Args:
276+ sandbox_id: Optional existing sandbox ID to reuse
277+ setup_script_path: Optional path to setup script to run after sandbox starts
278+
279+ Yields:
280+ (DockerBackend, sandbox_id)
281+
282+ Raises:
283+ ImportError: Docker SDK not installed
284+ Exception: Sandbox creation/connection failed
285+ FileNotFoundError: Setup script not found
286+ RuntimeError: Setup script failed
287+ """
288+ import docker
289+
290+ from deepagents_cli .integrations .docker import DockerBackend
291+
292+ sandbox_exists = sandbox_id != None
293+ console .print (f"[yellow]{ "Connecting to" if sandbox_exists else "Starting" } Docker sandbox...[/yellow]" )
294+
295+ # Create ephemeral app (auto-cleans up on exit)
296+ client = docker .from_env ()
297+
298+ image_name = "python:3.12-slim"
299+ project_level_deepagents_dir = f"{ os .getcwd ()} /.deepagents"
300+ try :
301+ container = client .containers .get (sandbox_id ) if sandbox_exists else client .containers .run (
302+ image_name ,
303+ command = "tail -f /dev/null" , # Keep container running
304+ detach = True ,
305+ environment = {"HOME" : os .path .expanduser ('~' )},
306+ tty = True ,
307+ mem_limit = "512m" ,
308+ cpu_quota = 50000 , # Limits CPU usage (e.g., 50% of one core)
309+ pids_limit = 100 , # Limit number of processes
310+ # Temporarily allow network and root access for setup
311+ network_mode = "bridge" ,
312+ # No user restriction for install step
313+ read_only = False , # Temporarily allow writes
314+ tmpfs = {"/tmp" : "rw,size=64m,noexec,nodev,nosuid" }, # Writable /tmp
315+ volumes = {
316+ os .path .expanduser ('~/.deepagents' ): {"bind" : os .path .expanduser ('~/.deepagents' ), 'mode' : 'rw' },
317+ os .getcwd (): {"bind" : "/workspace" , 'mode' : 'rw' },
318+ ** ({project_level_deepagents_dir : {"bind" : project_level_deepagents_dir , 'mode' : 'rw' }} if os .path .isdir (project_level_deepagents_dir ) else {}), # Needed for project skills to work
319+ },
320+ )
321+ except docker .errors .ImageNotFound as e :
322+ print (f"Error: The specified image '{ image_name } ' was not found." )
323+ print (f"Details: { e } " )
324+ exit ()
325+ except docker .errors .ContainerError as e :
326+ # This exception is raised if the container exits with a non-zero exit code
327+ # and detach is False.
328+ print (f"Error: The container exited with a non-zero exit code ({ e .exit_status } )." )
329+ print (f"Command run: { e .command } " )
330+ print (f"Container logs: { e .logs .decode ('utf-8' )} " )
331+ print (f"Details: { e } " )
332+ exit ()
333+ except docker .errors .APIError as e :
334+ # This covers other server-related errors, like connection issues or permission problems.
335+ print (f"Error: A Docker API error occurred." )
336+ print (f"Details: { e } " )
337+ exit ()
338+ except docker .errors .NotFound as e :
339+ print ("Container not found or not running." )
340+ exit ()
341+ except Exception as e :
342+ # General exception handler for any other unexpected errors
343+ print (f"An unexpected error occurred: { e } " )
344+ exit ()
345+
346+ sandbox_id = container .id
347+
348+ backend = DockerBackend (container )
349+ console .print (f"[green]✓ Docker sandbox ready: { backend .id } [/green]" )
350+
351+ # Run setup script if provided
352+ if setup_script_path :
353+ _run_sandbox_setup (backend , setup_script_path )
354+ try :
355+ yield backend
356+ finally :
357+ if not sandbox_exists :
358+ try :
359+ console .print (f"[dim]Terminating Docker sandbox { sandbox_id } ...[/dim]" )
360+ try :
361+ container .stop (timeout = 5 )
362+ container .remove (force = True )
363+ except docker .errors .NotFound :
364+ print (f"Container { sandbox_id } already removed." )
365+ except docker .errors .APIError as e :
366+ print (f"Error during container cleanup { sandbox_id } : { e } " )
367+ console .print (f"[dim]✓ Docker sandbox { sandbox_id } terminated[/dim]" )
368+ except Exception as e :
369+ console .print (f"[yellow]⚠ Cleanup failed: { e } [/yellow]" )
370+
371+
269372_PROVIDER_TO_WORKING_DIR = {
270373 "modal" : "/workspace" ,
271374 "runloop" : "/home/user" ,
272375 "daytona" : "/home/daytona" ,
376+ "docker" : "/workspace" ,
273377}
274378
275379
@@ -278,6 +382,7 @@ def create_daytona_sandbox(
278382 "modal" : create_modal_sandbox ,
279383 "runloop" : create_runloop_sandbox ,
280384 "daytona" : create_daytona_sandbox ,
385+ "docker" : create_docker_sandbox ,
281386}
282387
283388
@@ -294,7 +399,7 @@ def create_sandbox(
294399 the appropriate provider-specific context manager.
295400
296401 Args:
297- provider: Sandbox provider ("modal", "runloop", "daytona")
402+ provider: Sandbox provider ("modal", "runloop", "daytona", "docker" )
298403 sandbox_id: Optional existing sandbox ID to reuse
299404 setup_script_path: Optional path to setup script to run after sandbox starts
300405
@@ -318,7 +423,7 @@ def get_available_sandbox_types() -> list[str]:
318423 """Get list of available sandbox provider types.
319424
320425 Returns:
321- List of sandbox type names (e.g., ["modal", "runloop", "daytona"])
426+ List of sandbox type names (e.g., ["modal", "runloop", "daytona", "docker" ])
322427 """
323428 return list (_SANDBOX_PROVIDERS .keys ())
324429
@@ -327,7 +432,7 @@ def get_default_working_dir(provider: str) -> str:
327432 """Get the default working directory for a given sandbox provider.
328433
329434 Args:
330- provider: Sandbox provider name ("modal", "runloop", "daytona")
435+ provider: Sandbox provider name ("modal", "runloop", "daytona", "docker" )
331436
332437 Returns:
333438 Default working directory path as string
0 commit comments