master/tasks.md
2026-04-03 09:49:14 +03:00

361 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План работ: 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/<chat_id>`; 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