"""Self-hosted sandbox backend.""" import time import httpx from app.services.sandbox.base import BaseSandboxBackend, ExecutionResult, SandboxCapabilities from app.services.sandbox.config import SandboxConfig from loguru import logger class SelfHostedBackend(BaseSandboxBackend): """Self-hosted sandbox backend. Connects to user-deployed sandbox services like aio-sandbox. Usage: - Set SANDBOX_API_URL to full endpoint URL - For aio-sandbox shell: http://localhost:8080/v1/shell/exec - For aio-sandbox jupyter: http://localhost:8080/v1/jupyter/execute Response format expected: {"success": bool, "output": str, "error": str?} """ name = "self_hosted" def __init__(self, config: SandboxConfig): self.config = config if not config.api_url: raise ValueError( "Self-hosted sandbox URL is required. " "Set SANDBOX_API_URL environment variable." ) # Normalize URL (remove trailing slash) self.api_url = config.api_url.rstrip("/") def get_capabilities(self) -> SandboxCapabilities: # Capabilities depend on the self-hosted service # We'll report conservative defaults return SandboxCapabilities( supported_languages=["python", "bash", "node", "javascript"], max_timeout=self.config.max_timeout, max_memory_mb=256, network_available=True, filesystem_available=True, ) async def health_check(self) -> bool: """Check if the self-hosted service is available.""" try: async with httpx.AsyncClient() as client: # Try /v1/sandbox first (aio-sandbox), then fall back to /health for endpoint in ["/v1/sandbox", "/health"]: check_url = self.api_url.split("/v1/")[0] + endpoint if "/v1/" in self.api_url else f"{self.api_url.rsplit('/', 1)[0]}/health" try: response = await client.get(check_url, timeout=5.0) if response.status_code == 200: return True except Exception: continue return False except Exception: return False async def execute( self, code: str, language: str, timeout: int = 30, work_dir: str | None = None, **kwargs ) -> ExecutionResult: """Execute code using the self-hosted sandbox service.""" start_time = time.time() # Build request headers = { "Content-Type": "application/json", } # Add API key if configured if self.config.api_key: headers["Authorization"] = f"Bearer {self.config.api_key}" # Build payload based on the API endpoint # For shell exec: {"cmd": "..."} # For jupyter: {"code": "..."} payload = {} # Detect endpoint type from URL and build appropriate payload url_lower = self.api_url.lower() if "shell" in url_lower: # aio-sandbox shell: wrap code as command if language == "python": cmd = f"python3 -c {repr(code)}" elif language == "bash": cmd = code elif language == "node": cmd = f"node -e {repr(code)}" else: cmd = code payload = {"cmd": cmd} elif "jupyter" in url_lower: # aio-sandbox jupyter payload = {"code": code} else: # Generic format payload = { "code": code, "language": language, "timeout": timeout, } # Add any additional kwargs payload.update(kwargs) try: async with httpx.AsyncClient() as client: # Use URL directly without appending /execute response = await client.post( self.api_url, json=payload, headers=headers, timeout=float(timeout + 10) # Add buffer for network ) duration_ms = int((time.time() - start_time) * 1000) if response.status_code != 200: return ExecutionResult( success=False, stdout="", stderr="", exit_code=response.status_code, duration_ms=duration_ms, error=f"Sandbox service error: HTTP {response.status_code} - {response.text[:200]}" ) result = response.json() # Parse response - support multiple formats: # Generic: {"success": true, "stdout": "...", "stderr": "...", "exit_code": 0} # aio-sandbox shell: {"success": true, "data": {"output": "..."}} # aio-sandbox jupyter: {"output": "...", "status": "ok"} # Try to extract output output = "" stderr = "" success = True error_msg = None exit_code = 0 # aio-sandbox shell format if "data" in result and isinstance(result.get("data"), dict): output = result["data"].get("output", "") # aio-sandbox jupyter format elif "output" in result and "status" in result: output = result.get("output", "") if result.get("status") != "ok": success = False error_msg = result.get("error", result.get("output", "")) # Generic format else: output = result.get("stdout") or result.get("output") or "" stderr = result.get("stderr") or "" success = result.get("success", True) exit_code = result.get("exit_code", 0 if success else 1) error_msg = result.get("error") return ExecutionResult( success=success, stdout=output[:10000], stderr=stderr[:5000], exit_code=exit_code, duration_ms=result.get("duration_ms", duration_ms), error=error_msg ) except httpx.TimeoutException: duration_ms = int((time.time() - start_time) * 1000) return ExecutionResult( success=False, stdout="", stderr="", exit_code=124, duration_ms=duration_ms, error=f"Code execution timed out after {timeout}s" ) except Exception as e: duration_ms = int((time.time() - start_time) * 1000) logger.exception(f"[SelfHosted] Execution error") return ExecutionResult( success=False, stdout="", stderr="", exit_code=1, duration_ms=duration_ms, error=f"Self-hosted sandbox error: {str(e)[:200]}" )