diff --git a/adapter/config/loader.py b/adapter/config/loader.py index f33b908..0cb4b6b 100644 --- a/adapter/config/loader.py +++ b/adapter/config/loader.py @@ -8,7 +8,6 @@ from dotenv import dotenv_values from .model import ( AppConfig, AppSectionConfig, - DockerConfig, HttpConfig, LoggingConfig, MetricsConfig, @@ -40,7 +39,6 @@ def load_config( logging_section = _section(yaml_data, 'logging') metrics_section = _section(yaml_data, 'metrics') tracing_section = _section(yaml_data, 'tracing') - docker_section = _section(yaml_data, 'docker') sandbox_section = _section(yaml_data, 'sandbox') security_section = _section(yaml_data, 'security') @@ -132,15 +130,6 @@ def load_config( enable_metrics=metrics_enabled, enable_tracing=tracing_enabled, ), - docker=DockerConfig( - base_url=_yaml_or_env_str( - docker_section, - 'base_url', - 'docker.base_url', - env_values, - 'APP_DOCKER_BASE_URL', - ) - ), sandbox=_load_sandbox_config(sandbox_section, env_values), security=SecurityConfig( token_header=_yaml_or_env_str( diff --git a/adapter/config/model.py b/adapter/config/model.py index 3a8e70d..ca18347 100644 --- a/adapter/config/model.py +++ b/adapter/config/model.py @@ -40,11 +40,6 @@ class OtelConfig: metric_export_interval: int -@dataclass(frozen=True, slots=True) -class DockerConfig: - base_url: str - - @dataclass(frozen=True, slots=True) class SandboxConfig: image: str @@ -73,6 +68,5 @@ class AppConfig: metrics: MetricsConfig tracing: TracingConfig otel: OtelConfig - docker: DockerConfig sandbox: SandboxConfig security: SecurityConfig diff --git a/adapter/di/container.py b/adapter/di/container.py index 592cf6e..c28fbfa 100644 --- a/adapter/di/container.py +++ b/adapter/di/container.py @@ -78,8 +78,8 @@ def build_container( ) observability = build_observability(app_config) + docker_client: DockerClient = docker.from_env() clock = SystemClock() - docker_client = docker.DockerClient(base_url=app_config.docker.base_url) user_repository = InMemoryUserRepository( observability.tracer, [User(id='123', email='aza@gglamer.ru', name='gglamer')] diff --git a/adapter/docker/runtime.py b/adapter/docker/runtime.py index 61fcaf6..89df9ab 100644 --- a/adapter/docker/runtime.py +++ b/adapter/docker/runtime.py @@ -1,3 +1,4 @@ +from collections.abc import Callable from datetime import datetime from pathlib import Path @@ -10,22 +11,25 @@ from domain.error import SandboxError, SandboxStartError from domain.sandbox import SandboxSession, SandboxStatus from usecase.interface import SandboxRuntime +type NowFactory = Callable[[datetime], datetime] + class DockerSandboxRuntime(SandboxRuntime): def __init__( self, config: SandboxConfig, client: DockerClient, + now: NowFactory | None = None, ) -> None: self._config = config self._client = client + self._now = _current_time if now is None else now def create( self, *, session_id: str, chat_id: str, - created_at: datetime, expires_at: datetime, ) -> SandboxSession: try: @@ -55,7 +59,7 @@ class DockerSandboxRuntime(SandboxRuntime): chat_id=chat_id, container_id=container_id, status=SandboxStatus.RUNNING, - created_at=created_at, + created_at=self._now(expires_at), expires_at=expires_at, ) @@ -124,3 +128,7 @@ class DockerSandboxRuntime(SandboxRuntime): def _host_path(self, path_value: str) -> Path: return Path(path_value).expanduser().resolve(strict=False) + + +def _current_time(expires_at: datetime) -> datetime: + return datetime.now(tz=expires_at.tzinfo) diff --git a/adapter/http/fastapi/dependencies.py b/adapter/http/fastapi/dependencies.py index 87a9224..4892459 100644 --- a/adapter/http/fastapi/dependencies.py +++ b/adapter/http/fastapi/dependencies.py @@ -1,7 +1,8 @@ +from typing import cast + from fastapi import Depends, Request from adapter.di.container import AppContainer -from usecase.sandbox import CreateSandbox from usecase.user import GetUser APP_CONTAINER_STATE = 'container' @@ -10,16 +11,10 @@ APP_CONFIG_STATE = 'config' def get_container(request: Request) -> AppContainer: container = getattr(request.app.state, APP_CONTAINER_STATE, None) - if not isinstance(container, AppContainer): + if container is None: raise RuntimeError('container unavailable') - return container + return cast(AppContainer, container) def get_get_user(container: AppContainer = Depends(get_container)) -> GetUser: return container.usecases.get_user - - -def get_create_sandbox( - container: AppContainer = Depends(get_container), -) -> CreateSandbox: - return container.usecases.create_sandbox diff --git a/adapter/http/fastapi/routers/v1/router.py b/adapter/http/fastapi/routers/v1/router.py index 1f0aff4..df3d575 100644 --- a/adapter/http/fastapi/routers/v1/router.py +++ b/adapter/http/fastapi/routers/v1/router.py @@ -1,21 +1,9 @@ from fastapi import APIRouter, Depends, HTTPException, status from adapter.di.container import AppContainer -from adapter.http.fastapi.dependencies import ( - get_container, - get_create_sandbox, - get_get_user, -) -from adapter.http.fastapi.schemas import ( - CreateSandboxRequest, - ErrorResponse, - HealthResponse, - SandboxSessionResponse, - UserResponse, -) -from domain.error import SandboxError, SandboxStartError, UserNotFoundError -from domain.sandbox import SandboxSession -from usecase.sandbox import CreateSandbox, CreateSandboxCommand +from adapter.http.fastapi.dependencies import get_container, get_get_user +from adapter.http.fastapi.schemas import ErrorResponse, HealthResponse, UserResponse +from domain.error import UserNotFoundError from usecase.user import GetUser, GetUserQuery router = APIRouter() @@ -50,42 +38,3 @@ def get_user(user_id: str, usecase: GetUser = Depends(get_get_user)) -> UserResp ) from exc return UserResponse(id=user.id, email=user.email, name=user.name) - - -@router.post( - '/create', - response_model=SandboxSessionResponse, - responses={ - status.HTTP_503_SERVICE_UNAVAILABLE: {'model': ErrorResponse}, - status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': ErrorResponse}, - }, - status_code=status.HTTP_200_OK, -) -def create_sandbox( - request: CreateSandboxRequest, - usecase: CreateSandbox = Depends(get_create_sandbox), -) -> SandboxSessionResponse: - try: - session = usecase.execute(CreateSandboxCommand(chat_id=request.chat_id)) - except SandboxStartError as exc: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail=str(exc), - ) from exc - except SandboxError as exc: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(exc), - ) from exc - - return _to_sandbox_session_response(session) - - -def _to_sandbox_session_response(session: SandboxSession) -> SandboxSessionResponse: - return SandboxSessionResponse( - session_id=session.session_id, - chat_id=session.chat_id, - container_id=session.container_id, - status=session.status.value, - expires_at=session.expires_at, - ) diff --git a/adapter/http/fastapi/schemas.py b/adapter/http/fastapi/schemas.py index 08d9056..e11c95b 100644 --- a/adapter/http/fastapi/schemas.py +++ b/adapter/http/fastapi/schemas.py @@ -1,6 +1,4 @@ -from datetime import datetime - -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel class HealthResponse(BaseModel): @@ -9,20 +7,6 @@ class HealthResponse(BaseModel): env: str -class CreateSandboxRequest(BaseModel): - model_config = ConfigDict(extra='forbid', str_strip_whitespace=True) - - chat_id: str = Field(min_length=1) - - -class SandboxSessionResponse(BaseModel): - session_id: str - chat_id: str - container_id: str - status: str - expires_at: datetime - - class UserResponse(BaseModel): id: str email: str diff --git a/config/app.yaml b/config/app.yaml index 0e729db..2de4c27 100644 --- a/config/app.yaml +++ b/config/app.yaml @@ -24,9 +24,6 @@ otel: traces_endpoint: http://localhost:4318/v1/traces metric_export_interval: 1000 -docker: - base_url: unix:///var/run/docker.sock - sandbox: image: ai-agent:latest ttl_seconds: 300 diff --git a/tasks.md b/tasks.md index 49c99a6..e8c293c 100644 --- a/tasks.md +++ b/tasks.md @@ -86,7 +86,7 @@ ### M06. HTTP endpoint `POST /api/v1/create` - Субагент: `feature-developer` -- Статус: completed +- Статус: pending - Зависимости: `M04` - Commit required: no - Scope: добавить минимальную HTTP ручку для создания или переиспользования sandbox без auth diff --git a/usecase/interface.py b/usecase/interface.py index 0c8bcaa..fcc1fe6 100644 --- a/usecase/interface.py +++ b/usecase/interface.py @@ -34,7 +34,6 @@ class SandboxRuntime(Protocol): *, session_id: str, chat_id: str, - created_at: datetime, expires_at: datetime, ) -> SandboxSession: ... diff --git a/usecase/sandbox.py b/usecase/sandbox.py index 65740ef..ae60946 100644 --- a/usecase/sandbox.py +++ b/usecase/sandbox.py @@ -53,12 +53,10 @@ class CreateSandbox: self._runtime.stop(session.container_id) self._repository.delete(session.session_id) - expires_at = now + self._ttl new_session = self._runtime.create( session_id=_new_session_id(), chat_id=command.chat_id, - created_at=now, - expires_at=expires_at, + expires_at=now + self._ttl, ) self._repository.save(new_session) self._logger.info(