"""Codebase handoff between Swarm workspaces, archives, and Validator sandboxes.""" from __future__ import annotations import re import tempfile from dataclasses import dataclass from pathlib import Path from typing import Any from .codebase_archive import ( CodebaseArchiveStore, LocalCodebaseArchiveStore, archive_from_directory, archive_from_downloads, supports_archive_pointer, ) DEFAULT_CODEBASE_HANDOFF_ROOT = Path(tempfile.gettempdir()) / "agent-swarm-workbench-codebases" LOCAL_SNAPSHOT_SCHEME = "local-snapshot://" CODEBASE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$") @dataclass class CodebaseHandoff: """Moves a Codebase across Workspace, archive, and Validator sandbox seams.""" archive_store: CodebaseArchiveStore | None = None root_dir: Path | None = None def __post_init__(self) -> None: root = self.root_dir or DEFAULT_CODEBASE_HANDOFF_ROOT if self.archive_store is None: self.archive_store = LocalCodebaseArchiveStore(root_dir=root) def snapshot_local_workspace(self, source_root: Path, codebase_id: str) -> str: _validate_codebase_id(codebase_id) return self.archive_store.save(archive_from_directory(codebase_id, source_root / "workspace")) def snapshot_backend_workspace( self, backend: Any, codebase_id: str, *, workspace_dir: str = "/workspace", ) -> str: _validate_codebase_id(codebase_id) workspace_dir = normalize_workspace_dir(workspace_dir) glob_result = backend.glob(f"{workspace_dir}/**") if getattr(glob_result, "error", None): raise RuntimeError("could not list workspace files") file_paths = [ _entry_path(match) for match in getattr(glob_result, "matches", []) if not _entry_is_dir(match) ] if not file_paths: raise FileNotFoundError("workspace has no files") archive_files: list[tuple[str, bytes]] = [] for item in backend.download_files(file_paths): if getattr(item, "error", None): raise RuntimeError(f"could not download workspace file: {getattr(item, 'path', '')}") source_path = str(getattr(item, "path", "")) relative = workspace_relative_path(source_path, workspace_dir=workspace_dir) archive_files.append((relative.as_posix(), getattr(item, "content", b"") or b"")) return self.archive_store.save(archive_from_downloads(codebase_id, archive_files)) def restore_to_backend( self, archive_pointer: str, backend: Any, *, workspace_dir: str, ) -> int: if not supports_archive_pointer(archive_pointer): raise ValueError(pointer_error(archive_pointer)) workspace = (self.root_dir or DEFAULT_CODEBASE_HANDOFF_ROOT) / restore_id(archive_pointer) / "workspace" self.archive_store.materialize(archive_pointer, workspace) files = [ (sandbox_workspace_path(workspace_dir, path.relative_to(workspace).as_posix()), path.read_bytes()) for path in workspace.rglob("*") if path.is_file() ] results = backend.upload_files(files) for result in results: if getattr(result, "error", None): raise RuntimeError( f"could not upload file: {getattr(result, 'path', '')}: {getattr(result, 'error', '')}" ) return len(files) def local_snapshot_workspace(root_dir: Path, archive_pointer: str) -> Path | None: snapshot_id = local_snapshot_id(archive_pointer) if snapshot_id is None: return None return root_dir / snapshot_id / "workspace" def local_snapshot_id(archive_pointer: str) -> str | None: if not archive_pointer.startswith(LOCAL_SNAPSHOT_SCHEME): return None snapshot_id = archive_pointer.removeprefix(LOCAL_SNAPSHOT_SCHEME) if not CODEBASE_ID_PATTERN.fullmatch(snapshot_id): return None return snapshot_id def restore_id(archive_pointer: str) -> str: if "://" not in archive_pointer: raise ValueError("unsupported archive pointer") snapshot_id = archive_pointer.split("://", 1)[1] _validate_codebase_id(snapshot_id) return snapshot_id def pointer_error(archive_pointer: str) -> str: if archive_pointer.startswith(LOCAL_SNAPSHOT_SCHEME): return "invalid snapshot id" return "unsupported archive pointer" def normalize_workspace_dir(workspace_dir: str) -> str: return workspace_dir.rstrip("/") or "/workspace" def workspace_relative_path(path: str, *, workspace_dir: str = "/workspace") -> Path: prefix = f"{normalize_workspace_dir(workspace_dir)}/" if not path.startswith(prefix): raise ValueError(f"path outside workspace: {path}") relative = path.removeprefix(prefix) if not relative or ".." in Path(relative).parts: raise ValueError(f"unsafe workspace path: {path}") return Path(relative) def sandbox_workspace_path(workspace_dir: str, relative_path: str) -> str: return f"{normalize_workspace_dir(workspace_dir)}/{relative_path}" def _validate_codebase_id(codebase_id: str) -> None: if not CODEBASE_ID_PATTERN.fullmatch(codebase_id): raise ValueError("invalid codebase id") def _entry_path(entry: Any) -> str: return str(entry.get("path", "") if isinstance(entry, dict) else getattr(entry, "path", "")) def _entry_is_dir(entry: Any) -> bool: return bool(entry.get("is_dir") if isinstance(entry, dict) else getattr(entry, "is_dir", False))