"""Local docker-based sandbox backend.""" import time from pathlib import Path from app.services.sandbox.base import BaseSandboxBackend, ExecutionResult, SandboxCapabilities from app.services.sandbox.config import SandboxConfig from loguru import logger # Lazy import docker to make it optional _docker = None def _get_docker(): """Lazy load docker SDK.""" global _docker if _docker is None: try: import docker _docker = docker except ImportError: raise ImportError( "docker package is required for docker backend. " "Install it with: pip install docker" ) return _docker # Language to docker image mapping _DOCKER_IMAGES = { "python": "python:3.11-slim", "bash": "bash:5.2", "node": "node:18-slim", } # Docker run command mapping _DOCKER_COMMANDS = { "python": ["python3", "-c"], "bash": ["bash", "-c"], "node": ["node", "-e"], } class DockerBackend(BaseSandboxBackend): """Docker-based sandbox backend. This backend executes code inside Docker containers for better isolation. It requires the docker SDK to be installed and docker daemon to be running. """ name = "docker" def __init__(self, config: SandboxConfig): self.config = config self._client = None @property def client(self): """Lazy load docker client.""" if self._client is None: docker_lib = _get_docker() self._client = docker_lib.from_env() return self._client def get_capabilities(self) -> SandboxCapabilities: return SandboxCapabilities( supported_languages=["python", "bash", "node"], max_timeout=self.config.max_timeout, max_memory_mb=256, network_available=self.config.allow_network, filesystem_available=True, ) async def health_check(self) -> bool: """Check if docker is available and running.""" try: self.client.ping() return True except Exception: return False async def execute( self, code: str, language: str, timeout: int = 30, work_dir: str | None = None, **kwargs ) -> ExecutionResult: """Execute code inside a docker container.""" start_time = time.time() # Validate language if language not in _DOCKER_IMAGES: return ExecutionResult( success=False, stdout="", stderr="", exit_code=1, duration_ms=int((time.time() - start_time) * 1000), error=f"Unsupported language: {language}. Use: {', '.join(_DOCKER_IMAGES.keys())}" ) # Get image and command image = _DOCKER_IMAGES[language] # Prepare environment env = { "HOME": "/root", "PYTHONDONTWRITEBYTECODE": "1", } # Build docker run command if language == "python": cmd = ["python3", "-c", code] elif language == "bash": cmd = ["bash", "-c", code] elif language == "node": cmd = ["node", "-e", code] else: return ExecutionResult( success=False, stdout="", stderr="", exit_code=1, duration_ms=int((time.time() - start_time) * 1000), error=f"Unsupported language: {language}" ) # Resource limits cpu_limit = self.config.cpu_limit memory_limit = self.config.memory_limit # Network config network = None if not self.config.allow_network else "bridge" try: # Pull image if needed try: self.client.images.get(image) except Exception: # Image not found, pull it self.client.images.pull(image) # Run container container = self.client.containers.run( image, cmd, detach=False, mem_limit=memory_limit, cpu_period=100000, # Docker default cpu_quota=int(float(cpu_limit) * 100000), network_mode=network, environment=env, remove=True, stdout=True, stderr=True, ) # Wait for container with timeout result = container.wait(timeout=timeout) # Get output stdout = container.logs(stdout=True, stderr=False).decode("utf-8", errors="replace")[:10000] stderr = container.logs(stdout=False, stderr=True).decode("utf-8", errors="replace")[:5000] duration_ms = int((time.time() - start_time) * 1000) exit_code = result.get("StatusCode", 1) return ExecutionResult( success=exit_code == 0, stdout=stdout, stderr=stderr, exit_code=exit_code, duration_ms=duration_ms, error=None if exit_code == 0 else f"Exit code: {exit_code}" ) except Exception as e: duration_ms = int((time.time() - start_time) * 1000) error_msg = str(e) logger.exception(f"[Docker] Execution error") # Handle timeout specifically if "timeout" in error_msg.lower(): return ExecutionResult( success=False, stdout="", stderr="", exit_code=124, duration_ms=duration_ms, error=f"Code execution timed out after {timeout}s" ) return ExecutionResult( success=False, stdout="", stderr="", exit_code=1, duration_ms=duration_ms, error=f"Docker execution error: {error_msg[:200]}" )