from datetime import datetime from pathlib import Path from uuid import UUID from docker import DockerClient from docker.errors import DockerException, NotFound from docker.types import Mount from adapter.config.model import SandboxConfig from domain.error import SandboxError, SandboxStartError from domain.sandbox import SandboxSession, SandboxStatus from usecase.interface import SandboxRuntime class DockerSandboxRuntime(SandboxRuntime): def __init__( self, config: SandboxConfig, client: DockerClient, ) -> None: self._config = config self._client = client def create( self, *, session_id: str, chat_id: str, created_at: datetime, expires_at: datetime, ) -> SandboxSession: normalized_chat_id = chat_id try: normalized_chat_id = _canonical_chat_id(chat_id) chat_path = self._chat_path(normalized_chat_id) dependencies_path = self._readonly_host_path( self._config.dependencies_host_path ) lambda_tools_path = self._readonly_host_path( self._config.lambda_tools_host_path ) chat_path.mkdir(parents=True, exist_ok=True) container = self._client.containers.run( self._config.image, detach=True, labels=self._labels(session_id, normalized_chat_id, expires_at), mounts=self._mounts(chat_path, dependencies_path, lambda_tools_path), ) except (DockerException, OSError, ValueError) as exc: raise SandboxStartError(normalized_chat_id) from exc container_id = str(getattr(container, 'id', '')).strip() if not container_id: raise SandboxStartError(normalized_chat_id) return SandboxSession( session_id=session_id, chat_id=normalized_chat_id, container_id=container_id, status=SandboxStatus.RUNNING, created_at=created_at, expires_at=expires_at, ) def stop(self, container_id: str) -> None: try: container = self._client.containers.get(container_id) container.stop() except NotFound: return except DockerException as exc: raise SandboxError('sandbox_stop_failed') from exc def _labels( self, session_id: str, chat_id: str, expires_at: datetime, ) -> dict[str, str]: return { 'session_id': session_id, 'chat_id': chat_id, 'expires_at': expires_at.isoformat(), } def _mounts( self, chat_path: Path, dependencies_path: Path, lambda_tools_path: Path, ) -> list[Mount]: return [ Mount( target=self._config.chat_mount_path, source=str(chat_path), type='bind', ), Mount( target=self._config.dependencies_mount_path, source=str(dependencies_path), type='bind', read_only=True, ), Mount( target=self._config.lambda_tools_mount_path, source=str(lambda_tools_path), type='bind', read_only=True, ), ] def _chat_path(self, chat_id: str) -> Path: if not chat_id.strip(): raise ValueError('invalid chat path') chats_root = self._host_path(self._config.chats_root) chat_path = (chats_root / chat_id).resolve(strict=False) if not chat_path.is_relative_to(chats_root): raise ValueError('invalid chat path') return chat_path def _readonly_host_path(self, path_value: str) -> Path: host_path = self._host_path(path_value) if not host_path.exists(): raise ValueError('invalid host path') return host_path def _host_path(self, path_value: str) -> Path: return Path(path_value).expanduser().resolve(strict=False) def _canonical_chat_id(chat_id: str) -> str: return str(UUID(str(chat_id).strip()))