diff --git a/AGENTS.md b/AGENTS.md index ba5bc34..c9f89d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,12 +45,12 @@ - Do not use Beads - Do not use `bd` - Use `uv` for Python commands and dependency management -- Do not create commits on your own -- Work on one task at a time - Prefer delegation for implementation -- Delegate only one task at a time -- After one task return to the user with result verification and next options -- Wait for the user before the next task commit or fix +- After implementation, run `Code-Reviewer` agent +- Pass errors to `test-engineer` agent to capture +- Delegate `Feature-Developer` agent fix the errors +- Repeat the cycle until no errors remain +- Ensure all tests pass ## Makefile - `make install` install deps with `uv` diff --git a/README.md b/README.md index 2339337..3ff3dd6 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,268 @@ -Это шаблон Python-сервиса на чистой архитектуре с заменяемым web-слоем, типизированным конфигом, явным dependency wiring и observability через порты. +# master-service -## Что это за проект +`master-service` — это control-plane сервис для sandbox-контейнеров с AI-агентом. +Он поднимает и переиспользует sandbox на чат, подключает рабочие volume, восстанавливает state после рестарта и отдает наружу минимальный HTTP API под `/api/v1`. -- Небольшой референсный сервис со слоями `domain/`, `usecase/`, `repository/` и `adapter/` -- Шаблон для сервисов на FastAPI, где FastAPI остается только во внешнем HTTP adapter -- Проект, где конфиг собирается из `config/app.yaml`, `.env` и env vars в одно дерево dataclass-конфигов -- Проект, где repository и usecase создаются один раз на старте приложения в composition root -- Проект, где логи, метрики и трейсы скрыты за интерфейсами и могут работать через `stdout`, файл или OpenTelemetry runtime +Важно: в локальном `config/app.yaml` исторически еще стоят template-имена `web-python-skelet`. +Если хочешь, чтобы `/health` и OTel service name локально тоже показывали `master-service`, переопредели: +- `APP_NAME=master-service` +- `APP_OTEL_SERVICE_NAME=master-service` -## Основные идеи +Сервис реализован на Python с Clean Architecture: +- `domain/` — сущности и доменные ошибки +- `usecase/` — сценарии приложения и порты +- `repository/` — реализации repository +- `adapter/` — HTTP, config, DI, Docker runtime и observability -- Clean Architecture и границы SOLID -- Направление зависимостей только внутрь -- Тонкие adapter-слои и явная сборка зависимостей -- Заменяемый HTTP-слой -- Observability без протекания OpenTelemetry во внутренние слои +## Что умеет сейчас + +Текущий sandbox MVP покрывает: +- `GET /api/v1/health` +- `POST /api/v1/create` с `chat_id: UUID` +- одну активную sandbox на чат +- reuse активной sandbox до истечения TTL +- cleanup просроченных sandbox в фоне +- startup reconciliation по Docker labels после рестарта сервиса +- chat mount `rw`, dependencies mount `ro`, lambda-tools mount `ro` +- логи, метрики и трейсы через порты `Logger`, `Metrics`, `Tracer` + +Пока вне scope: +- auth и access control +- p2p/WebSocket lease +- workspace/chat CRUD API +- central DB, artifacts, S3, quota и retention policy + +## Как устроен проект + +- FastAPI живет только во внешнем adapter слое +- Docker живет только во внешнем adapter слое +- конфиг собирается из `config/app.yaml`, `.env` и env vars в один dataclass tree +- repository и usecase создаются один раз на старте в `adapter/di/container.py` +- observability не протекает во внутренние слои через OpenTelemetry SDK + +## Структура + +- `domain/` — core model и domain errors +- `usecase/` — use cases и interfaces +- `repository/` — in-memory и другие repository implementations +- `adapter/config/` — typed config models и loader +- `adapter/docker/` — Docker sandbox runtime +- `adapter/observability/` — logger/metrics/tracer runtime factory +- `adapter/otel/` — OpenTelemetry adapters +- `adapter/di/` — composition root +- `adapter/http/fastapi/` — app, middleware, schemas, routers +- `adapter/sandbox/` — sandbox reconciliation logic +- `config/` — YAML config files +- `docs/` — ADR и проектные гайды ## Быстрый старт +### Требования + +- Python 3.13 +- `uv` +- локальный Docker daemon +- секреты `APP_API_TOKEN` и `APP_SIGNING_KEY` + +### Установка + ```bash make install +``` + +### Локальный запуск + +```bash APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run ``` -Приложение стартует на `http://0.0.0.0:8123` и публикует versioned API под `/api/v1`. +Это поднимет сам API, но для успешного `POST /api/v1/create` локально нужен еще рабочий sandbox runtime: + +- Docker daemon должен быть доступен по `docker.base_url` +- образ `sandbox.image` должен существовать локально +- директории `sandbox.dependencies_host_path` и `sandbox.lambda_tools_host_path` должны существовать + +В дефолтном `config/app.yaml` это значит: + +```bash +mkdir -p var/sandbox/dependencies var/sandbox/lambda-tools +docker image inspect ai-agent:latest >/dev/null +``` + +Если у тебя нет готового `ai-agent:latest`, проще начать с Docker Compose smoke path ниже. + +После старта сервис доступен на: +- `http://127.0.0.1:8123/api/v1/health` + +Проверка health: + +```bash +curl http://127.0.0.1:8123/api/v1/health +``` + +Создание или reuse sandbox: + +```bash +curl -X POST http://127.0.0.1:8123/api/v1/create \ + -H 'Content-Type: application/json' \ + -d '{"chat_id":"11111111-1111-1111-1111-111111111111"}' +``` + +Пример ответа: + +```json +{ + "session_id": "3701cfe3-e05e-48af-8385-442dcd954ca2", + "chat_id": "11111111-1111-1111-1111-111111111111", + "container_id": "64d839c6007de9396ee08ad4af4a22a59a6410ec5f4892a9277a87eb49c3ff5d", + "status": "running", + "expires_at": "2026-04-02T21:11:38.292893Z" +} +``` + +## Запуск через Docker Compose + +Для локального smoke-run есть `docker-compose.yml`. +Он поднимает: +- `app` +- `docker-engine` в режиме Docker-in-Docker +- `otel-collector` + +При этом `app` получает compose-specific config из: +- `config/docker-compose.yml` + +Запуск: + +```bash +make compose-up +``` + +Проверка: + +```bash +make compose-ps +make compose-logs +``` + +Остановка: + +```bash +make compose-down +``` + +Важно: +- в `config/docker-compose.yml` сейчас для smoke-проверки стоит `sandbox.image: nginx:1.27-alpine` +- для реального agent runtime замени `sandbox.image` на образ своего sandbox/agent контейнера +- в compose auth env vars нужны для startup config, но текущий MVP API еще не проверяет request token + +## Как конфигурировать + +### Источники конфига + +Конфиг собирается в таком порядке: +1. базовый YAML из `config/app.yaml` +2. значения из `.env` +3. process env vars поверх `.env` + +То есть env vars имеют наивысший приоритет. + +### Обязательные секреты + +Нужны всегда: +- `APP_API_TOKEN` +- `APP_SIGNING_KEY` + +Сейчас это startup config, а не активная request auth для `/api/v1/create` и `/api/v1/health`. +То есть в текущем MVP токен не нужно передавать в HTTP headers для вызова этих endpoint. + +### Основные секции YAML + +В `config/app.yaml` и `config/docker-compose.yml` есть секции: +- `app` +- `http` +- `logging` +- `metrics` +- `tracing` +- `otel` +- `docker` +- `sandbox` +- `security` + +### Полезные env overrides + +Чаще всего полезны: + +#### Общие +- `APP_NAME` +- `APP_ENV` +- `APP_HTTP_HOST` +- `APP_HTTP_PORT` + +#### Логирование и observability +- `APP_LOGGING_LEVEL` +- `APP_LOGGING_OUTPUT` +- `APP_LOGGING_FORMAT` +- `APP_LOGGING_FILE_PATH` +- `APP_METRICS_ENABLED` +- `APP_TRACING_ENABLED` +- `APP_OTEL_SERVICE_NAME` +- `APP_OTEL_LOGS_ENDPOINT` +- `APP_OTEL_METRICS_ENDPOINT` +- `APP_OTEL_TRACES_ENDPOINT` + +#### Docker runtime +- `APP_DOCKER_BASE_URL` + +#### Sandbox +- `APP_SANDBOX_IMAGE` +- `APP_SANDBOX_TTL_SECONDS` +- `APP_SANDBOX_CLEANUP_INTERVAL_SECONDS` +- `APP_SANDBOX_CHATS_ROOT` +- `APP_SANDBOX_DEPENDENCIES_HOST_PATH` +- `APP_SANDBOX_LAMBDA_TOOLS_HOST_PATH` +- `APP_SANDBOX_CHAT_MOUNT_PATH` +- `APP_SANDBOX_DEPENDENCIES_MOUNT_PATH` +- `APP_SANDBOX_LAMBDA_TOOLS_MOUNT_PATH` + +#### Security +- `APP_API_TOKEN_HEADER` +- `APP_API_TOKEN` +- `APP_SIGNING_KEY` + +### Что важно в sandbox config + +- `docker.base_url` — адрес Docker daemon +- `sandbox.image` — образ sandbox контейнера +- `sandbox.ttl_seconds` — TTL sandbox +- `sandbox.cleanup_interval_seconds` — частота cleanup loop +- `sandbox.chats_root` — корень chat directories +- `sandbox.dependencies_host_path` — host path для dependency cache +- `sandbox.lambda_tools_host_path` — host path для read-only lambda-tools +- `sandbox.chat_mount_path` — путь внутри sandbox для chat volume +- `sandbox.dependencies_mount_path` — путь внутри sandbox для dependency cache +- `sandbox.lambda_tools_mount_path` — путь внутри sandbox для lambda-tools + +## Основные команды + +- `make install` — установить зависимости через `uv` +- `make run` — локальный запуск +- `make run-otel` — запуск с OTel endpoints из env +- `make test` — `pytest` +- `make lint` — `ruff` +- `make typecheck` — `mypy` +- `make pre-commit` — lint + typecheck + test +- `make compose-build` — собрать compose images +- `make compose-up` — поднять локальный stack +- `make compose-down` — остановить stack +- `make compose-logs` — смотреть логи +- `make compose-ps` — смотреть статус сервисов ## Документация ### Гайды - [Правила проекта и ограничения для агента](AGENTS.md) -- [Кодстайл проекта для AI-агента](docs/CODESTYLE.md) +- [Кодстайл проекта](docs/CODESTYLE.md) - [Чистая архитектура, SOLID, DIP, Protocol и repository](docs/CLEAN_ARCHITECTURE_RU.md) - [Логи, метрики и трейсы в этом проекте](docs/OBSERVABILITY_RU.md) - [Как чистая архитектура реализована здесь](docs/PROJECT_GUIDE_RU.md) @@ -43,43 +275,24 @@ APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run - [003 Observability Via Interfaces](docs/003-observability-via-interfaces.md) - [004 Versioned HTTP API](docs/004-versioned-http-api.md) - [005 Early FastAPI OTel Instrumentation](docs/005-fastapi-otel-early-instrumentation.md) +- [006 MVP Docker Sandbox Orchestration](docs/006-mvp-docker-sandbox-orchestration.md) +- [007 Startup Sandbox Reconciliation](docs/007-startup-sandbox-reconciliation.md) +- [008 Sandbox Lifecycle Observability](docs/008-sandbox-lifecycle-observability.md) -## Структура проекта +## Для AI-агента -- `domain/` - core-сущности и доменные ошибки -- `usecase/` - прикладные сценарии и порты -- `repository/` - реализации repository -- `adapter/config/` - загрузка и модели типизированного конфига -- `adapter/observability/` - выбор runtime для logger, metrics и tracer -- `adapter/otel/` - OpenTelemetry adapters -- `adapter/di/` - composition root и singleton wiring -- `adapter/http/fastapi/` - HTTP-схемы, dependencies, middleware и routers -- `config/` - YAML-конфиг приложения и локального OTel collector +Если ты меняешь проект как AI-агент, сначала прочитай: -## Для ИИ +1. [AGENTS.md](AGENTS.md) +2. [docs/CODESTYLE.md](docs/CODESTYLE.md) +3. [docs/PROJECT_GUIDE_RU.md](docs/PROJECT_GUIDE_RU.md) +4. [docs/CLEAN_ARCHITECTURE_RU.md](docs/CLEAN_ARCHITECTURE_RU.md) +5. [docs/OBSERVABILITY_RU.md](docs/OBSERVABILITY_RU.md) +6. релевантные ADR в `docs/` +7. [tasks.md](tasks.md) -Если ты AI-агент и собираешься что-то менять в проекте, сначала прочитай документы в таком порядке: - -1. [Правила проекта и ограничения агента](AGENTS.md) - обязательные правила работы в этом репозитории -2. [Кодстайл проекта для AI-агента](docs/CODESTYLE.md) - границы слоев, стиль кода и правила зависимостей -3. [Как чистая архитектура реализована здесь](docs/PROJECT_GUIDE_RU.md) - практическая карта проекта и типовые сценарии изменений -4. [Чистая архитектура, SOLID, DIP, Protocol и repository](docs/CLEAN_ARCHITECTURE_RU.md) - базовые архитектурные принципы и примеры -5. [Логи, метрики и трейсы в этом проекте](docs/OBSERVABILITY_RU.md) - читать перед любыми изменениями в observability, middleware и runtime wiring -6. [ADR в `docs/`](docs/001-composition-root-and-lifetimes.md) - читать релевантные решения перед изменением архитектуры или startup wiring -7. [План задач и история работ](tasks.md) - понять, что уже сделано, что отложено и какие ограничения были зафиксированы - -Перед началом работы: - -- Определи, в каком слое будет изменение: `domain/`, `usecase/`, `repository/` или `adapter/` -- Убедись, что зависимости идут только внутрь -- Не тащи FastAPI и OpenTelemetry во внутренние слои -- Сначала изучи существующий код в нужной директории, потом вноси изменения -- Если задача затрагивает архитектурное решение, сначала сверяйся с ADR и проектными правилами - -## Запуск и команды - -- Для локального запуска нужны `APP_API_TOKEN` и `APP_SIGNING_KEY` -- `make run` запускает приложение локально -- `make run-otel` запускает приложение с локальными OTel endpoints из env vars -- `make pre-commit` запускает `ruff`, `mypy` и `pytest` -- `make compose-up` поднимает приложение и локальный LGTM stack через Docker Compose +Главные правила: +- сначала определи слой изменения +- зависимости только внутрь +- не тащи FastAPI и OpenTelemetry во внутренние слои +- архитектурные решения сверяй с ADR diff --git a/config/docker-compose.yml b/config/docker-compose.yml index a601f99..5ddb745 100644 --- a/config/docker-compose.yml +++ b/config/docker-compose.yml @@ -29,8 +29,8 @@ docker: sandbox: image: nginx:1.27-alpine - ttl_seconds: 30 - cleanup_interval_seconds: 5 + ttl_seconds: 300 + cleanup_interval_seconds: 60 chats_root: /var/lib/master-sandbox/chats dependencies_host_path: /var/lib/master-dependencies lambda_tools_host_path: /var/lib/master-lambda-tools diff --git a/tasks.md b/tasks.md index 861a726..d5009d7 100644 --- a/tasks.md +++ b/tasks.md @@ -353,7 +353,7 @@ ### M29. Финальный boundary review для sandbox observability - Субагент: `code-reviewer` -- Статус: pending +- Статус: completed - Зависимости: `M28` - Commit required: no - Scope: подтвердить, что M27-M28 закрыли remaining M26 замечания diff --git a/test/test_docker_runtime.py b/test/test_docker_runtime.py index 267d177..7f71275 100644 --- a/test/test_docker_runtime.py +++ b/test/test_docker_runtime.py @@ -14,7 +14,7 @@ from adapter.docker.runtime import DockerSandboxRuntime from adapter.observability.noop import NoopMetrics, NoopTracer from domain.error import SandboxError, SandboxStartError from domain.sandbox import SandboxSession, SandboxStatus -from usecase.interface import AttrValue, Attrs +from usecase.interface import Attrs, AttrValue CHAT_ID = UUID('123e4567-e89b-12d3-a456-426614174000') NON_CANONICAL_CHAT_ID = '123E4567E89B12D3A456426614174000'