@@ -266,10 +266,113 @@ 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+ try :
300+ container = client .containers .get (sandbox_id ) if sandbox_exists else client .containers .run (
301+ image_name ,
302+ command = "tail -f /dev/null" , # Keep container running
303+ detach = True ,
304+ environment = {"HOME" : os .path .expanduser ('~' )},
305+ tty = True ,
306+ mem_limit = "512m" ,
307+ cpu_quota = 50000 , # Limits CPU usage (e.g., 50% of one core)
308+ pids_limit = 100 , # Limit number of processes
309+ # Temporarily allow network and root access for setup
310+ network_mode = "bridge" ,
311+ # No user restriction for install step
312+ read_only = False , # Temporarily allow writes
313+ tmpfs = {"/tmp" : "rw,size=64m,noexec,nodev,nosuid" }, # Writable /tmp
314+ volumes = {
315+ os .path .expanduser ('~/.deepagents' ): {"bind" : os .path .expanduser ('~/.deepagents' ), 'mode' : 'rw' },
316+ os .getcwd (): {"bind" : "/workspace" , 'mode' : 'rw' },
317+ os .getcwd () + "/.deepagents" : {"bind" : os .getcwd () + "/.deepagents" , 'mode' : 'rw' }, # Needed for project skills to work
318+ },
319+ )
320+ except docker .errors .ImageNotFound as e :
321+ print (f"Error: The specified image '{ image_name } ' was not found." )
322+ print (f"Details: { e } " )
323+ exit ()
324+ except docker .errors .ContainerError as e :
325+ # This exception is raised if the container exits with a non-zero exit code
326+ # and detach is False.
327+ print (f"Error: The container exited with a non-zero exit code ({ e .exit_status } )." )
328+ print (f"Command run: { e .command } " )
329+ print (f"Container logs: { e .logs .decode ('utf-8' )} " )
330+ print (f"Details: { e } " )
331+ exit ()
332+ except docker .errors .APIError as e :
333+ # This covers other server-related errors, like connection issues or permission problems.
334+ print (f"Error: A Docker API error occurred." )
335+ print (f"Details: { e } " )
336+ exit ()
337+ except docker .errors .NotFound as e :
338+ print ("Container not found or not running." )
339+ exit ()
340+ except Exception as e :
341+ # General exception handler for any other unexpected errors
342+ print (f"An unexpected error occurred: { e } " )
343+ exit ()
344+
345+ sandbox_id = container .id
346+
347+ backend = DockerBackend (container )
348+ console .print (f"[green]✓ Docker sandbox ready: { backend .id } [/green]" )
349+
350+ # Run setup script if provided
351+ if setup_script_path :
352+ _run_sandbox_setup (backend , setup_script_path )
353+ try :
354+ yield backend
355+ finally :
356+ if not sandbox_exists :
357+ try :
358+ console .print (f"[dim]Terminating Docker sandbox { sandbox_id } ...[/dim]" )
359+ try :
360+ container .stop (timeout = 5 )
361+ container .remove (force = True )
362+ except docker .errors .NotFound :
363+ print (f"Container { sandbox_id } already removed." )
364+ except docker .errors .APIError as e :
365+ print (f"Error during container cleanup { sandbox_id } : { e } " )
366+ console .print (f"[dim]✓ Docker sandbox { sandbox_id } terminated[/dim]" )
367+ except Exception as e :
368+ console .print (f"[yellow]⚠ Cleanup failed: { e } [/yellow]" )
369+
370+
269371_PROVIDER_TO_WORKING_DIR = {
270372 "modal" : "/workspace" ,
271373 "runloop" : "/home/user" ,
272374 "daytona" : "/home/daytona" ,
375+ "docker" : "/workspace" ,
273376}
274377
275378
@@ -278,6 +381,7 @@ def create_daytona_sandbox(
278381 "modal" : create_modal_sandbox ,
279382 "runloop" : create_runloop_sandbox ,
280383 "daytona" : create_daytona_sandbox ,
384+ "docker" : create_docker_sandbox ,
281385}
282386
283387
@@ -294,7 +398,7 @@ def create_sandbox(
294398 the appropriate provider-specific context manager.
295399
296400 Args:
297- provider: Sandbox provider ("modal", "runloop", "daytona")
401+ provider: Sandbox provider ("modal", "runloop", "daytona", "docker" )
298402 sandbox_id: Optional existing sandbox ID to reuse
299403 setup_script_path: Optional path to setup script to run after sandbox starts
300404
@@ -318,7 +422,7 @@ def get_available_sandbox_types() -> list[str]:
318422 """Get list of available sandbox provider types.
319423
320424 Returns:
321- List of sandbox type names (e.g., ["modal", "runloop", "daytona"])
425+ List of sandbox type names (e.g., ["modal", "runloop", "daytona", "docker" ])
322426 """
323427 return list (_SANDBOX_PROVIDERS .keys ())
324428
@@ -327,7 +431,7 @@ def get_default_working_dir(provider: str) -> str:
327431 """Get the default working directory for a given sandbox provider.
328432
329433 Args:
330- provider: Sandbox provider name ("modal", "runloop", "daytona")
434+ provider: Sandbox provider name ("modal", "runloop", "daytona", "docker" )
331435
332436 Returns:
333437 Default working directory path as string
0 commit comments