Clawith/backend/app/services/sandbox/api/e2b_backend.py

170 lines
5.2 KiB
Python

"""E2B API-based sandbox backend."""
import time
from typing import Any
from app.services.sandbox.base import BaseSandboxBackend, ExecutionResult, SandboxCapabilities
from app.services.sandbox.config import SandboxConfig
from loguru import logger
# Lazy import e2b to make it optional
_e2b = None
def _get_e2b():
"""Lazy load e2b SDK."""
global _e2b
if _e2b is None:
try:
import e2b
_e2b = e2b
except ImportError:
raise ImportError(
"e2b package is required for E2B backend. "
"Install it with: pip install e2b"
)
return _e2b
# Language mapping for E2B
_LANGUAGE_MAP = {
"python": "python",
"bash": "bash",
"node": "node",
"javascript": "javascript",
}
class E2bBackend(BaseSandboxBackend):
"""E2B cloud-based sandbox backend.
E2B (https://e2b.dev/) provides secure, cloud-based code execution
with built-in isolation and networking.
"""
name = "e2b"
def __init__(self, config: SandboxConfig):
self.config = config
self._client = None
if not config.api_key:
raise ValueError("E2B API key is required. Set SANDBOX_API_KEY environment variable.")
@property
def client(self):
"""Get or create E2B client."""
e2b_lib = _get_e2b()
if self._client is None:
self._client = e2b_lib.AsyncSandbox
return self._client
def get_capabilities(self) -> SandboxCapabilities:
return SandboxCapabilities(
supported_languages=["python", "bash", "node", "javascript"],
max_timeout=self.config.max_timeout,
max_memory_mb=512,
network_available=True,
filesystem_available=True,
)
async def health_check(self) -> bool:
"""Check if E2B service is available."""
try:
e2b_lib = _get_e2b()
# Try to list sandboxes to verify API is accessible
await e2b_lib.AsyncSandbox.list(api_key=self.config.api_key)
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 using E2B cloud sandbox."""
start_time = time.time()
# Map language to E2B format
e2b_language = _LANGUAGE_MAP.get(language, language)
if language not in _LANGUAGE_MAP:
return ExecutionResult(
success=False,
stdout="",
stderr="",
exit_code=1,
duration_ms=int((time.time() - start_time) * 1000),
error=f"Unsupported language: {language}"
)
e2b_lib = _get_e2b()
try:
# Create sandbox and run code
async with await e2b_lib.AsyncSandbox.create(
api_key=self.config.api_key,
timeout=timeout,
) as sandbox:
# Build the command based on language
if e2b_language == "python":
cmd = "python3"
args = ["-c", code]
elif e2b_language == "bash":
cmd = "bash"
args = ["-c", code]
elif e2b_language in ("node", "javascript"):
cmd = "node"
args = ["-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}"
)
# Run the command - use string format for e2b
cmd_str = f"{cmd} {args[0]} '{args[1]}'"
result = await sandbox.commands.run(cmd_str)
duration_ms = int((time.time() - start_time) * 1000)
return ExecutionResult(
success=result.exit_code == 0,
stdout=result.stdout or "",
stderr=result.stderr or "",
exit_code=result.exit_code or 0,
duration_ms=duration_ms,
error=None
)
except Exception as e:
duration_ms = int((time.time() - start_time) * 1000)
error_msg = str(e)
logger.exception(f"[E2B] Execution error")
# Handle timeout
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"E2B execution error: {error_msg[:200]}"
)