# План работ: master-service MVP sandbox ## Контекст - Источники требований: `docs/master.md`, `meetings/meeting_1_document.md`, `README.md`, `docs/*` - Базовый template уже готов: typed config, DI container, observability, FastAPI adapter и versioned API под `/api/v1` - Текущая цель: минимальное управление Docker sandbox без auth - MVP API: `POST /api/v1/create` - Sandbox policy: TTL `300` секунд, одна активная sandbox на `chat_id`, повторный `create` переиспользует активную сессию - Volume policy: chat volume `rw`, dependencies volume `ro`, lambda-tools volume `ro` - Host paths вычисляются из typed config, а HTTP request передает только `chat_id` - Cleanup выполняется периодическим in-process loop внутри master-service ## Вне текущего scope - auth и access control - p2p lease и WebSocket transport - workspace/chat CRUD API - chat files, artifacts, S3, quota и retention policy - центральная БД и multi-node orchestration ## Правила выполнения - Выполняем по одной задаче - Коммиты не делаем - Если по ходу нужна смена архитектуры, останавливаемся и согласуем решение - `domain/` и `usecase/` не импортируют Docker, FastAPI, OpenTelemetry, env или YAML - Inner layers работают только через минимальные domain сущности и usecase порты ## Очередь задач ### M01. ADR и минимальный sandbox scaffolding - Исполнитель: `primary-agent` - Статус: completed - Зависимости: нет - Commit required: no - Scope: зафиксировать MVP-решение в ADR и создать минимальные сущности, ошибки и usecase-контракты для sandbox orchestration - Файлы: `docs/006-mvp-docker-sandbox-orchestration.md`, `domain/sandbox.py`, `domain/error.py`, `usecase/interface.py`, `usecase/sandbox.py` - Критерии приемки: в `domain/` есть минимальная `SandboxSession` и sandbox-ошибки; в `usecase/` есть порты `SandboxSessionRepository`, `SandboxRuntime` и `Clock`; созданы скелеты `CreateSandbox` и `CleanupExpiredSandboxes`; ADR занимает 10-20 строк ### M02. Typed config для sandbox runtime - Субагент: `feature-developer` - Статус: completed - Зависимости: `M01` - Commit required: no - Scope: расширить typed-config слоем `sandbox` с настройками image, TTL, cleanup interval, host paths и container mount paths - Файлы: `adapter/config/model.py`, `adapter/config/loader.py`, `config/app.yaml` - Решение: chat host path строится как путь под общим `sandbox.chats_root/`; request не передает host path напрямую - Критерии приемки: конфиг собирается в typed dataclass-дерево; дефолтный TTL равен `300`; есть отдельные настройки для `chats_root`, `dependencies_host_path`, `lambda_tools_host_path`, `chat_mount_path`, `dependencies_mount_path`, `lambda_tools_mount_path`, `cleanup_interval_seconds`; inner layers не читают env ### M03. Docker runtime adapter для sandbox lifecycle - Субагент: `feature-developer` - Статус: completed - Зависимости: `M01`, `M02` - Commit required: no - Scope: реализовать outer adapter над Docker для создания и остановки sandbox контейнера с нужными labels и mount policy - Файлы: `adapter/docker/runtime.py`, `adapter/docker/__init__.py` - Ограничения: все Docker-детали остаются в `adapter/`; runtime не должен протекать во внутренние слои - Критерии приемки: runtime умеет создать sandbox container по входным параметрам usecase; chat volume монтируется как `rw`; dependency и lambda-tools volumes монтируются как `ro`; контейнер получает labels с `session_id`, `chat_id` и `expires_at`; runtime переводит ошибки Docker в понятные исключения адаптера ### M04. In-memory session repository и usecase `CreateSandbox` - Субагент: `feature-developer` - Статус: completed - Зависимости: `M01`, `M02`, `M03` - Commit required: no - Scope: реализовать in-memory registry активных sandbox-сессий и usecase создания sandbox с логикой reuse по `chat_id` - Файлы: `repository/sandbox_session.py`, `usecase/sandbox.py`, `adapter/di/container.py` - Решение: если по `chat_id` есть неистекшая сессия, usecase возвращает ее без нового container start; если сессия истекла, usecase инициирует stop старой sandbox и создает новую - Критерии приемки: одна активная sandbox на `chat_id`; TTL-логика использует порт `Clock`; usecase не импортирует Docker; container wiring остается singleton-based ### M05. Cleanup expired sandboxes и lifecycle wiring - Субагент: `feature-developer` - Статус: completed - Зависимости: `M04` - Commit required: no - Scope: реализовать usecase cleanup просроченных sandbox и подключить периодический cleanup loop в FastAPI lifecycle - Файлы: `usecase/sandbox.py`, `adapter/di/container.py`, `adapter/http/fastapi/app.py`, при необходимости `adapter/http/fastapi/dependencies.py` - Ограничения: не ломать ADR про раннее OTel instrumentation; lifecycle loop должен стартовать и останавливаться один раз - Критерии приемки: cleanup находит истекшие сессии, останавливает sandbox через runtime и удаляет их из registry; интервал cleanup берется из конфига; shutdown корректно завершает фоновую задачу ### M06. HTTP endpoint `POST /api/v1/create` - Субагент: `feature-developer` - Статус: completed - Зависимости: `M04` - Commit required: no - Scope: добавить минимальную HTTP ручку для создания или переиспользования sandbox без auth - Файлы: `adapter/http/fastapi/schemas.py`, `adapter/http/fastapi/dependencies.py`, `adapter/http/fastapi/routers/v1/router.py`, при необходимости `adapter/di/container.py` - Request: `{ "chat_id": "..." }` - Response: `session_id`, `chat_id`, `container_id`, `status`, `expires_at` - Критерии приемки: router остается тонким; handler только переводит HTTP input в команду usecase и маппит ошибки в HTTP; endpoint живет под `/api/v1/create`; auth не добавляется ### M07. Тесты для create, reuse, TTL и mount policy - Субагент: `test-engineer` - Статус: completed - Зависимости: `M03`, `M04`, `M05`, `M06` - Commit required: no - Scope: покрыть тестами ключевое поведение MVP без запуска реального production Docker stack - Файлы: `test/*` - Критерии приемки: есть unit-тесты для `CreateSandbox` и `CleanupExpiredSandboxes` с fake clock; есть HTTP smoke-тест для `POST /api/v1/create`; есть adapter-level тест с mock Docker client на mount policy `chat=rw`, `deps=ro`, `tools=ro`; тесты не тащат FastAPI или Docker в inner-layer тесты ### M08. Архитектурный и boundary review по MVP sandbox - Субагент: `code-reviewer` - Статус: completed - Зависимости: `M07` - Commit required: no - Scope: проверить соблюдение clean architecture, dependency direction и соответствие MVP-ограничениям - Файлы: весь измененный код - Критерии приемки: Docker остается только во внешнем adapter; FastAPI не протекает в `domain/` и `usecase/`; TTL и mount policy читаются как явные, тестируемые правила; замечания сформулированы как точечные правки или подтверждение готовности ## Follow-up после M08 review ### M09. Сериализация lifecycle sandbox по `chat_id` - Субагент: `feature-developer` - Статус: completed - Зависимости: `M08` - Commit required: no - Scope: убрать гонки между параллельными `create` и cleanup для одного `chat_id` - Файлы: `usecase/interface.py`, `usecase/sandbox.py`, `repository/sandbox_lock.py` или другой outer-layer lock implementation, `adapter/di/container.py` - Решение: ввести явный usecase-port для process-local lock по `chat_id`; outer-layer реализация держит per-chat lock registry; `CreateSandbox` и `CleanupExpiredSandboxes` выполняют мутации сессии под этим lock - Критерии приемки: для одного `chat_id` не поднимаются два sandbox при concurrent create; create-vs-cleanup не оставляет orphan container; locking не протекает в HTTP и Docker adapter как бизнес-логика ### M10. Устойчивый cleanup и вынос blocking cleanup из event loop - Субагент: `feature-developer` - Статус: completed - Зависимости: `M09` - Commit required: no - Scope: сделать cleanup устойчивым к частичным ошибкам и не блокировать FastAPI event loop синхронным Docker stop - Файлы: `usecase/sandbox.py`, `adapter/http/fastapi/app.py` - Решение: `CleanupExpiredSandboxes` обрабатывает stop/delete по каждой сессии отдельно и продолжает batch; HTTP cleanup loop выносит blocking cleanup work в thread через adapter-layer orchestration - Критерии приемки: ошибка на одной expired session не мешает чистить остальные; background cleanup loop не умирает после ошибки; blocking cleanup больше не выполняется прямо в event loop ### M11. Удаление не-MVP user surface из приложения - Субагент: `feature-developer` - Статус: completed - Зависимости: `M08` - Commit required: no - Scope: убрать из runtime app неотносящиеся к MVP user endpoint и seed user wiring - Файлы: `adapter/http/fastapi/routers/v1/router.py`, `adapter/http/fastapi/dependencies.py`, `adapter/http/fastapi/schemas.py`, `adapter/di/container.py` - Решение: оставить в MVP только `health` и sandbox API; примерный user code может остаться в репозитории как template, но не должен быть подключен в runtime app - Критерии приемки: `GET /api/v1/users/{user_id}` больше не опубликован; container не создает seeded user repository/usecase для runtime app; незапрошенная user-surface area исчезает ### M12. Регрессионные тесты на race conditions и cleanup resilience - Субагент: `test-engineer` - Статус: completed - Зависимости: `M09`, `M10`, `M11` - Commit required: no - Scope: добавить тесты на новые гарантии после review fixes - Файлы: `test/*` - Критерии приемки: есть тест на duplicate create для одного `chat_id`; есть тест на create-vs-cleanup race или эквивалентную сериализацию; есть тест, что cleanup продолжает batch после stop failure; HTTP smoke/regression тесты обновлены под удаление user endpoint ### M13. Повторный boundary review после fix-pass - Субагент: `code-reviewer` - Статус: completed - Зависимости: `M12` - Commit required: no - Scope: проверить, что must-fix и should-fix замечания из `M08` закрыты без нарушения clean architecture - Файлы: весь измененный код после `M09`-`M12` - Критерии приемки: нет гонки на one-sandbox-per-chat; cleanup не блокирует event loop и не валится на первом stop failure; runtime app не публикует лишний user API; замечания сведены к minor или отсутствуют ### M14. Починка mypy-типизации тестов после sandbox MVP - Субагент: `test-engineer` - Статус: completed - Зависимости: `M07` - Commit required: no - Scope: устранить текущие ошибки `make pre-commit` в test-suite без изменения production behavior - Файлы: `test/test_docker_runtime.py`, `test/test_create_http.py`, при необходимости общие test helpers в `test/*` - Ошибки: несовместимый fake Docker client для `DockerSandboxRuntime`, неточная типизация `run_calls` и ASGI message payload, использование `object` вместо типизированных test doubles для `AppRepositories`, `AppUsecases`, `AppContainer` - Решение: сделать test doubles типизированными через совместимые fake classes или локальные protocols; убрать `object` и неиндексируемые `dict[str, object]` там, где mypy не может вывести типы - Критерии приемки: `uv run mypy .` проходит; `make pre-commit` доходит как минимум до pytest stage; production code не меняется или меняется только при явной необходимости для testability ## Follow-up после M13 review ### M15. Канонизация и валидация `chat_id` - Субагент: `feature-developer` - Статус: completed - Зависимости: `M13` - Commit required: no - Scope: сделать `chat_id` строго UUID и убрать path alias/whole-root mount риск через неканоничные значения - Файлы: `adapter/http/fastapi/schemas.py`, `adapter/docker/runtime.py`, при необходимости `usecase/sandbox.py` и тесты в `test/*` - Решение: принять `chat_id` как UUID на HTTP boundary, использовать его каноничную строковую форму дальше в usecase/repository/path construction и не принимать произвольные path-like строки - Критерии приемки: не-UUID значения отклоняются на HTTP boundary с `400/422`; UUID используется как единое каноничное значение для lock key, repository key и filesystem path; появляются регрессионные тесты на invalid `chat_id` ### M16. Lifecycle reconciliation на startup/shutdown - Субагент: `feature-developer` - Статус: completed - Зависимости: `M13` - Commit required: no - Scope: устранить restart-gap между in-memory registry и уже запущенными Docker containers - Файлы: `adapter/docker/runtime.py`, `adapter/di/container.py`, `adapter/http/fastapi/app.py`, при необходимости новые outer-layer helper files и тесты в `test/*` - Решение: основная стратегия — reconciliation по Docker labels на startup, чтобы после restart master-service продолжал видеть уже запущенные sandbox и не поднимал дубликаты; graceful shutdown cleanup остается опциональным дополнением - Критерии приемки: после restart master-service может восстановить/синхронизировать state по Docker labels без потери работающих agent containers; one-sandbox-per-chat не нарушается из-за пустого in-memory registry; lifecycle policy явно зафиксирована и покрыта тестами ### M17. Управление жизненным циклом per-chat locks - Субагент: `feature-developer` - Статус: completed - Зависимости: `M13` - Commit required: no - Scope: ограничить неограниченный рост registry locks по числу когда-либо увиденных `chat_id` - Файлы: `repository/sandbox_lock.py`, при необходимости тесты в `test/*` - Решение: добавить eviction/ref-count/weakref policy во внешнем lock registry без нарушения сериализации lifecycle для активного `chat_id` - Критерии приемки: registry locks не растет бесконечно без причины; сериализация для активных чатов сохраняется; поведение покрыто тестами ### M18. Перевести sandbox ids на UUID types - Субагент: `feature-developer` - Статус: completed - Зависимости: `M15` - Commit required: no - Scope: сделать `chat_id` и `session_id` типом `UUID` внутри sandbox scope, оставив `container_id` строкой как внешний Docker identifier - Файлы: `domain/sandbox.py`, `usecase/interface.py`, `usecase/sandbox.py`, `repository/sandbox_session.py`, `adapter/http/fastapi/*`, `adapter/docker/runtime.py`, `adapter/di/container.py`, `test/*` - Решение: HTTP boundary принимает/возвращает UUID, usecase и repository работают с UUID objects, Docker labels продолжают сериализоваться в строки через `str(uuid)` - Критерии приемки: внутри sandbox flow `chat_id` и `session_id` больше не строки; `container_id` остается `str`; pydantic корректно сериализует UUID в response; `make pre-commit` проходит ## Follow-up после issue #11 observability ### M19. ADR и observability contracts для sandbox lifecycle - Исполнитель: `primary-agent` - Статус: completed - Зависимости: `M18` - Commit required: yes - Commit message: `add sandbox observability contracts` - Scope: зафиксировать sandbox lifecycle observability policy в ADR-lite и подготовить минимальные контракты для traces и current-state metrics без нарушения clean architecture - Файлы: `docs/008-sandbox-lifecycle-observability.md`, `usecase/interface.py`, `repository/sandbox_session.py`, `adapter/otel/metrics.py`, `adapter/observability/noop.py` - Решение: добавить в `Metrics` порт операцию `set(...)` для gauge-like current-state сигналов; добавить в `SandboxSessionRepository` `count_active()` как источник truth для `sandbox.active.count` - Критерии приемки: ADR занимает 10-20 строк; inner layers по-прежнему знают только порты `Logger`/`Metrics`/`Tracer`; current-state метрика активных sandbox выражается без OTel imports во внутреннем слое ### M20. Трейсы и метрики в sandbox usecases - Субагент: `feature-developer` - Статус: completed - Зависимости: `M19` - Commit required: yes - Commit message: `instrument sandbox usecases` - Scope: добавить spans и ключевые lifecycle metrics в `CreateSandbox` и `CleanupExpiredSandboxes` - Файлы: `usecase/sandbox.py`, `adapter/di/container.py`, при необходимости тесты в `test/*` - Решение: usecase получает `Metrics` и `Tracer` через конструктор; `CreateSandbox` и `CleanupExpiredSandboxes` публикуют `sandbox.create.total`, `sandbox.cleanup.total`, `sandbox.cleanup.error.total` и обновляют `sandbox.active.count` после мутаций registry - Критерии приемки: есть spans `usecase.create_sandbox` и `usecase.cleanup_expired_sandboxes`; span attrs и metric attrs включают ключевые lifecycle identifiers/result fields; reuse/replace/cleanup paths наблюдаемы без OTel imports в usecase ### M21. Трейсы и runtime metrics в Docker adapter и reconciliation - Субагент: `feature-developer` - Статус: completed - Зависимости: `M19` - Commit required: yes - Commit message: `instrument sandbox docker runtime` - Scope: добавить observability в `DockerSandboxRuntime` и reconciliation path для Docker operations и current-state sync - Файлы: `adapter/docker/runtime.py`, `adapter/sandbox/reconciliation.py`, `adapter/di/container.py`, при необходимости тесты в `test/*` - Решение: `DockerSandboxRuntime` получает `Metrics` и `Tracer`; create/stop/list публикуют duration histograms `sandbox.runtime.create.duration_ms`, `sandbox.runtime.stop.duration_ms`, `sandbox.runtime.list_active.duration_ms`, error counter `sandbox.runtime.error.total` и span attrs по chat/session/container; reconciliation обновляет `sandbox.active.count` по registry snapshot - Критерии приемки: Docker adapter остается во внешнем слое; ошибки Docker операций отражаются в spans и metrics; после startup reconciliation current-state метрика активных sandbox синхронизирована с registry ### M22. Тесты на sandbox observability - Субагент: `test-engineer` - Статус: completed - Зависимости: `M20`, `M21` - Commit required: yes - Commit message: `add sandbox observability tests` - Scope: покрыть regression tests новую observability policy без реального OTel backend - Файлы: `test/test_sandbox_usecase.py`, `test/test_docker_runtime.py`, при необходимости новые focused tests в `test/*` - Решение: использовать типизированные fake metrics/tracer implementations и проверить names/attrs ключевых spans и metrics на create/reuse/replace/cleanup/runtime paths - Критерии приемки: тесты подтверждают spans и metrics на usecase и adapter paths; constructor wiring обновлен без mypy regressions; `make typecheck` и релевантный `pytest` проходят ### M23. Boundary review для sandbox observability - Субагент: `code-reviewer` - Статус: in_progress - Зависимости: `M22` - Commit required: no - Scope: проверить, что observability изменения закрывают issue #11 и FR-034 без нарушения clean architecture - Файлы: весь измененный код после `M19`-`M22` - Критерии приемки: inner layers не импортируют OTel; Docker-specific tracing остается в `adapter/docker/`; current-state и duration metrics достаточно покрывают sandbox lifecycle; замечания сведены к minor или отсутствуют ## Follow-up после M23 boundary review ### M24. Исправить replace trace identity в CreateSandbox - Субагент: `feature-developer` - Статус: completed - Зависимости: `M23` - Commit required: yes - Commit message: `fix sandbox replace trace identity` - Scope: устранить смешение old/new sandbox identifiers в replace path usecase tracing - Файлы: `usecase/sandbox.py`, при необходимости точечные тесты в `test/*` - Решение: сохранять старые и новые sandbox identifiers в отдельных span attrs или child spans так, чтобы replace success и replace failure оставались однозначно трассируемыми - Критерии приемки: replace path не перетирает previous/new identifiers; при replace failure span остается консистентным и отражает обе стороны lifecycle ### M25. Добрать failure-path observability regression tests - Субагент: `test-engineer` - Статус: completed - Зависимости: `M24` - Commit required: yes - Commit message: `add sandbox observability failure tests` - Scope: покрыть tests для replace-failure trace, cleanup error metrics/spans и Docker stop observability - Файлы: `test/test_sandbox_usecase.py`, `test/test_docker_runtime.py`, при необходимости другие focused tests в `test/*` - Решение: использовать presence-based assertions и проверять ключевые span/metric contracts без brittle exact-order checks - Критерии приемки: есть тест на replace failure tracing; есть тест на `sandbox.cleanup.error.total`; есть тесты на Docker stop observability для success/error/not_found или эквивалентного набора outcome paths ### M26. Повторный boundary review для sandbox observability - Субагент: `code-reviewer` - Статус: in_progress - Зависимости: `M25` - Commit required: no - Scope: подтвердить, что follow-up fixes закрыли M23 замечания без новых boundary нарушений - Файлы: весь измененный код после `M24`-`M25` - Критерии приемки: нет замечаний по replace tracing identity и missing failure-path observability coverage; clean architecture по-прежнему соблюдена ## Follow-up после M26 boundary review ### M27. Компенсация save failure после runtime.create - Субагент: `feature-developer` - Статус: completed - Зависимости: `M26` - Commit required: yes - Commit message: `fix sandbox create rollback gap` - Scope: не оставлять untracked running container и неконсистентный `sandbox.active.count` при падении `repository.save()` после успешного `runtime.create()` - Файлы: `usecase/sandbox.py`, при необходимости точечные тесты в `test/*` - Решение: сделать create/replace path registry-safe через rollback или другой явный compensation path без нарушения clean architecture - Критерии приемки: save failure не оставляет новый container в runtime без registry state; `sandbox.active.count` отражает финальное committed state; replace и fresh-create failure paths консистентны ### M28. Регрессии на rollback и startup failure observability - Субагент: `test-engineer` - Статус: completed - Зависимости: `M27` - Commit required: yes - Commit message: `add sandbox rollback regression tests` - Scope: покрыть tests для save-failure rollback и startup observability failure paths - Файлы: `test/test_sandbox_usecase.py`, `test/test_docker_runtime.py`, `test/test_create_http.py`, при необходимости другие focused tests в `test/*` - Решение: добавить tests на fresh-create/replace save failure compensation, `list_active` failure observability и reconciliation failure span/metric expectations где применимо - Критерии приемки: rollback path покрыт; list/reconciliation failure observability не регрессирует; tests остаются presence-based и стабильными ### M29. Финальный boundary review для sandbox observability - Субагент: `code-reviewer` - Статус: completed - Зависимости: `M28` - Commit required: no - Scope: подтвердить, что M27-M28 закрыли remaining M26 замечания - Файлы: весь измененный код после `M27`-`M28` - Критерии приемки: нет замечаний по rollback gap и startup failure observability coverage; sandbox observability slice приемлем as-is