master/adapter/docker/runtime.py
Azamat e629e34c4d ref #10: [fix] enforce UUID chat ids
Normalize chat ids to a single UUID form so locks, repository keys, and mount paths cannot diverge through path-like aliases.
2026-04-02 22:35:50 +03:00

134 lines
4.1 KiB
Python

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()))