agent-swarm-workbench / arena /codebase_executor.py
Kiy-K's picture
Restore full Space app with bucket model support
31c8075 verified
Raw
History Blame Contribute Delete
6.9 kB
"""Sandboxed command executors for Codebase archives."""
from __future__ import annotations
import os
import shlex
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any, Callable
from uuid import uuid4
from deepagents.backends import LocalShellBackend
from .codebase_archive import (
CodebaseArchiveStore,
LocalCodebaseArchiveStore,
create_codebase_archive_store,
)
from .codebase_handoff import (
CodebaseHandoff,
DEFAULT_CODEBASE_HANDOFF_ROOT,
local_snapshot_workspace,
pointer_error,
)
from .sandbox_lease import SandboxLeaseManager
DEFAULT_SNAPSHOT_ROOT = DEFAULT_CODEBASE_HANDOFF_ROOT
DEFAULT_VALIDATOR_SANDBOX_ROOT = Path(tempfile.gettempdir()) / "agent-swarm-workbench-validator"
class LocalCodebaseExecutor:
"""Runs validation checks against local Codebase archive materializations."""
def __init__(self, *, root_dir: Path | None = None, timeout_seconds: int = 20) -> None:
self.root_dir = root_dir or DEFAULT_SNAPSHOT_ROOT
self.timeout_seconds = timeout_seconds
def run(self, archive_pointer: str, command: str) -> tuple[bool, str]:
workspace = local_snapshot_workspace(self.root_dir, archive_pointer)
if workspace is None:
return False, pointer_error(archive_pointer)
if not workspace.exists():
return False, f"snapshot workspace not found: {archive_pointer}"
try:
result = subprocess.run(
["/bin/sh", "-lc", command],
cwd=workspace,
capture_output=True,
text=True,
timeout=self.timeout_seconds,
env={"PATH": _executor_path()},
check=False,
)
except subprocess.TimeoutExpired as exc:
output = (exc.stdout or "") + (exc.stderr or "")
return False, f"{output}\n[Command timed out after {self.timeout_seconds}s]"
output = result.stdout
if result.stderr:
output = f"{output}[stderr] {result.stderr}"
status = "succeeded" if result.returncode == 0 else "failed"
return result.returncode == 0, f"{output}\n[Command {status} with exit code {result.returncode}]"
class CodebaseSandboxExecutor:
"""Restores Codebase archives into a sandbox backend before running checks."""
def __init__(
self,
*,
backend_factory: Callable[[], Any],
root_dir: Path | None = None,
archive_store: CodebaseArchiveStore | None = None,
timeout_seconds: int = 20,
sandbox_ttl_seconds: float | None = None,
sandbox_leases: SandboxLeaseManager | None = None,
) -> None:
self.root_dir = root_dir or DEFAULT_SNAPSHOT_ROOT
self.archive_store = archive_store or LocalCodebaseArchiveStore(root_dir=self.root_dir)
self.handoff = CodebaseHandoff(archive_store=self.archive_store, root_dir=self.root_dir)
self.backend_factory = backend_factory
self.backend = None
self.timeout_seconds = timeout_seconds
self.workspace_dir = "workspace"
self._restored: set[str] = set()
self._lease_id = f"validator-{uuid4().hex}"
self._sandbox_leases = sandbox_leases or SandboxLeaseManager(ttl_seconds=sandbox_ttl_seconds)
def run(self, archive_pointer: str, command: str) -> tuple[bool, str]:
self._ensure_backend()
self._sandbox_leases.touch(self._lease_id)
try:
self._restore(archive_pointer)
except (OSError, ValueError) as exc:
return False, f"could not restore snapshot: {exc}"
result = self.backend.execute(f"cd {shlex.quote(self.workspace_dir)} && {command}", timeout=self.timeout_seconds)
output = getattr(result, "output", "")
exit_code = getattr(result, "exit_code", None)
if exit_code is None:
return True, output
status = "succeeded" if exit_code == 0 else "failed"
return exit_code == 0, f"{output}\n[Command {status} with exit code {exit_code}]"
def close(self) -> None:
self._sandbox_leases.close(self._lease_id)
def _open_backend(self) -> None:
self.backend = self.backend_factory()
self.workspace_dir = getattr(self.backend, "_validator_workspace", "workspace")
self._restored = set()
self._sandbox_leases.start(self._lease_id, self._close_backend)
def _ensure_backend(self) -> None:
if self.backend is None:
self._open_backend()
def _close_backend(self) -> None:
if self.backend is None:
return
sandbox = getattr(self.backend, "_daytona_sandbox", None)
cleanup = None
if sandbox is not None:
cleanup = getattr(sandbox, "delete", None) or getattr(sandbox, "stop", None)
if cleanup is None:
cleanup = getattr(self.backend, "delete", None) or getattr(self.backend, "stop", None)
if callable(cleanup):
cleanup()
self.backend = None
self._restored = set()
def _restore(self, archive_pointer: str) -> None:
if archive_pointer in self._restored:
return
self.handoff.restore_to_backend(
archive_pointer,
self.backend,
workspace_dir=self.workspace_dir,
)
self._restored.add(archive_pointer)
def create_codebase_executor(
archive_store: CodebaseArchiveStore | None = None,
provider: str | None = None,
) -> CodebaseSandboxExecutor:
provider = (provider or os.getenv("VALIDATOR_SANDBOX_PROVIDER", "local")).lower()
archive_store = archive_store or create_codebase_archive_store()
if provider == "local":
return CodebaseSandboxExecutor(backend_factory=_local_validator_backend, archive_store=archive_store)
if provider == "daytona":
return CodebaseSandboxExecutor(backend_factory=_daytona_validator_backend, archive_store=archive_store)
raise ValueError(f"unsupported validator sandbox provider: {provider}")
def _executor_path() -> str:
executable_dir = str(Path(sys.executable).parent)
return os.pathsep.join([executable_dir, "/usr/local/bin", "/usr/bin", "/bin"])
def _local_validator_backend() -> LocalShellBackend:
root = DEFAULT_VALIDATOR_SANDBOX_ROOT / uuid4().hex
root.mkdir(parents=True, exist_ok=True)
backend = LocalShellBackend(root_dir=root, virtual_mode=True, inherit_env=True)
backend._validator_workspace = "workspace"
return backend
def _daytona_validator_backend() -> Any:
from daytona import Daytona
from langchain_daytona import DaytonaSandbox
client = Daytona()
sandbox = client.create()
backend = DaytonaSandbox(sandbox=sandbox)
backend._daytona_client = client
backend._daytona_sandbox = sandbox
backend._validator_workspace = "/home/daytona/workspace"
return backend