diff --git a/CHANGELOG.md b/CHANGELOG.md index cea7f17..2140968 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Цепочка стадий: `... testing → deploy-staging → deploy → done` (была без `deploy-staging`). ### Fixed +- **Контейнер и агенты бегут под uid хоста (1000:1000), не root** (ORCH-040): оба сервиса в `docker-compose.yml` (`orchestrator`, `orchestrator-staging`) получили `user: "1000:1000"` (slin) — устраняет корень проблемы, при которой Claude-CLI агенты, запускаемые через `subprocess.Popen` внутри root-контейнера, создавали все артефакты конвейера (git worktree `/repos/_wt/...`, коммиты в `docs/work-items/...`) с владельцем `root:root` на хосте, из-за чего `git pull`/`git reset` под slin падали с `insufficient permission for adding an object` и каждый деплой требовал ручного `chown`. Теперь файлы сразу `slin:slin`. Доступ к docker.sock сохранён через `group_add: ["999"]` (МИНА 1 — НЕ удалена). SSH-маунт приведён к единому HOME агента: target `/root/.ssh` → `/home/slin/.ssh` (`/home/slin/.orchestrator-ssh:/home/slin/.ssh:ro`), синхронно с `HOME=/home/slin`, который launcher форсит в env Popen и git_env — устранён скрытый рассинхрон SSH-маунта с форсимым HOME. `src/agents/launcher.py` и `Dockerfile` НЕ менялись (numeric uid работает без записи в `/etc/passwd`; `safe.directory '*'` уже покрывает git над bind-mount). Требует host-prerequisites Owner (P-1…P-4, вне кода): блокер P-1 — `chown -R 1000:1000 /home/slin/.claude` для доступа uid 1000 к claude creds (иначе preflight заворачивает конвейер); прод-рестарт self — только в окно тишины (общий инстанс с enduro-trails), страховка — staging-гейт (adr-0003). ADR `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md`, глобальный `docs/architecture/adr/adr-0005-container-runs-as-host-uid.md`; INFRA.md обновлён (рантайм-uid, volumes/SSH target, host-prerequisites). Тесты: `tests/test_orch040_compose.py`. - **Staging-чек B6 читает реестр из окружения работающего staging-инстанса** (ORCH-048): блок B6 «Registry: sandbox present, prod ET/ORCH absent» в `scripts/staging_check.py` давал **ложный FAIL** (`prod-ET=YES(BAD!)`, `prod-ORCH=YES(BAD!)`) при фактически исправной изоляции — единственный чек suite, который не ходил к инстансу по HTTP, а импортировал `src.projects` локально через host-path хак `sys.path.insert(0, "/repos/orchestrator")` + `importlib.reload`, строя реестр из `ORCH_PROJECTS_JSON` **process-env запускающего процесса**. При фактическом запуске деплоером с хоста переменная не задана → дефолт `_DEFAULT_PROJECTS` (ET+ORCH) → ложный FAIL → лишний откат `deploy-staging → development`. Решение (вариант «в», ADR-001): host-path хак удалён; suite канонически запускается ВНУТРИ контейнера `orchestrator-staging` через `docker exec … python3 /repos/orchestrator/scripts/staging_check.py` (`scripts/` доступен только через bind-mount, `import src.projects` резолвится через `PYTHONPATH=/app` из кода контейнера, env — `.env.staging`) → B6 читает реестр именно работающего инстанса, без HTTP-bootstrap и «курицы-яйца». Логика вердикта вынесена в чистую `_evaluate_b6(known) -> (passed, detail)` (инвариант `passed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known`, формат detail сохранён) + `_known_project_ids_from_registry()` / `_run_b6()` с детерминированным FAIL при недоступности источника (не ложный PASS, не необработанное исключение). Синхронно обновлены `.openclaw/agents/deployer.md` (команда стадии через `docker exec`) и `docs/operations/STAGING_CHECK.md`. `src/projects.py`, `.env*` и прочие чеки A/B4/B5/C не тронуты; реестр `QG_CHECKS` и `check_staging_status` (ADR-0003) не менялись. ADR `docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md`. Тесты: `tests/test_staging_check_b6.py`. - **Testing-гейт `check_tests_passed` читает `result:` наравне с `verdict:`/`status:`** (ORCH-047): парсер `_parse_tests_verdict` (`src/qg/checks.py`) теперь принимает три равноправных машиночитаемых поля frontmatter `13-test-report.md` — `result:` (канон промпта тестера `.openclaw/agents/tester.md`, `result: PASS|FAIL`), плюс легаси `verdict:` и `status:` (enduro-trails ET-001..ET-014); достаточно любого одного непустого. Устраняет рассинхрон контракта: тестер честно эмитил `result: PASS` без `verdict:`/`status:`, парсер попадал в ветку «нет машинного вердикта» → откат `testing → development` в петлю до исчерпания `MAX_DEVELOPER_RETRIES` (наблюдалось на ORCH-17; ORCH-016 прошёл лишь из-за избыточного дублирования полей). Семантика приоритетов сохранена и распространена на все три поля через объединённую строку: negative-токен в любом поле авторитетен (перебивает positive), наборы токенов заморожены (обратная совместимость). Сигнатура гейта, имя и реестр `QG_CHECKS` не менялись. ADR `docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md`. Тесты: `tests/test_qg.py::TestCheckTestsPassed`. - БАГ-8: провал deploy/deploy-staging → корректный откат на `development`. diff --git a/docker-compose.yml b/docker-compose.yml index 21ab1c7..071bc4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,11 @@ services: build: . container_name: orchestrator restart: unless-stopped + # ORCH-040: бежим под uid:gid хоста (slin=1000:1000), а не root, чтобы + # артефакты конвейера (worktree + docs) создавались как slin:slin и git на + # хосте работал без ручного chown. Доступ к docker.sock сохранён через + # group_add: ["999"] (МИНА 1 — НЕ удалять). См. ADR-001 ORCH-040. + user: "1000:1000" # init: true injects docker-init (tini) as PID 1 so reparented grandchild # processes from the claude/node subprocess tree are reaped (no zombies, B-2). init: true @@ -15,7 +20,8 @@ services: - /usr/bin/node:/usr/bin/node:ro - /home/slin/.claude:/home/slin/.claude - /home/slin/.claude.json:/home/slin/.claude.json:ro - - /home/slin/.orchestrator-ssh:/root/.ssh:ro + # ORCH-040: target согласован с HOME=/home/slin (launcher), не /root/.ssh. + - /home/slin/.orchestrator-ssh:/home/slin/.ssh:ro env_file: .env environment: - ORCH_REPOS_DIR=/repos @@ -35,6 +41,8 @@ services: build: . container_name: orchestrator-staging restart: unless-stopped + # ORCH-040: тот же uid хоста, что и у prod (см. комментарий выше / ADR-001). + user: "1000:1000" init: true network_mode: host command: ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8501"] @@ -46,7 +54,8 @@ services: - /usr/bin/node:/usr/bin/node:ro - /home/slin/.claude:/home/slin/.claude - /home/slin/.claude.json:/home/slin/.claude.json:ro - - /home/slin/.orchestrator-ssh:/root/.ssh:ro + # ORCH-040: target согласован с HOME=/home/slin (launcher), не /root/.ssh. + - /home/slin/.orchestrator-ssh:/home/slin/.ssh:ro env_file: .env.staging environment: - ORCH_REPOS_DIR=/repos diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 59430c8..f4874c4 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -9,6 +9,7 @@ Per-work-item решения живут в `docs/work-items//06-adr/ADR-NNN- | adr-0002 | Очередь задач вместо in-process потоков | accepted | 2026-06-03 | ORCH-1 | | adr-0003 | Условный staging-гейт перед прод-деплоем | accepted | 2026-06-05 | ORCH-35 | | adr-0004 | Поллинг с ретраем в check_ci_green (фикс CI-race) | accepted | 2026-06-05 | ORCH-045 | +| adr-0005 | Контейнеры бегут под uid:gid хоста (1000:1000) | accepted | 2026-06-06 | ORCH-040 | ## Формат **Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded. diff --git a/docs/architecture/adr/adr-0005-container-runs-as-host-uid.md b/docs/architecture/adr/adr-0005-container-runs-as-host-uid.md new file mode 100644 index 0000000..339da3c --- /dev/null +++ b/docs/architecture/adr/adr-0005-container-runs-as-host-uid.md @@ -0,0 +1,42 @@ +# adr-0005: Контейнеры оркестратора бегут под uid:gid хоста (1000:1000) + +- **Статус:** accepted +- **Дата:** 2026-06-06 +- **Задача:** ORCH-040 + +## Контекст +Оба контейнера (`orchestrator`, `orchestrator-staging`) запускались под `uid=0 (root)` и +монтировали хостовый `/home/slin/repos` → `/repos` (rw). Claude-CLI агенты исполняются +`subprocess.Popen` внутри контейнера под тем же root, поэтому все артефакты конвейера +(git worktree, коммиты в `docs/`) появлялись на хосте как `root:root`. Деплой прода под +`slin` (uid 1000) ломался на правах git до ручного `chown`. Это сквозное свойство рантайма: +касается агентов **всех** проектов, а не отдельной фичи. + +## Решение +Оба сервиса в `docker-compose.yml` запускаются под `user: "1000:1000"` (uid:gid хоста `slin`). +- `group_add: ["999"]` сохраняется — доступ к docker.sock идёт через gid 999, не через root. +- target SSH-маунта приведён к `/home/slin/.ssh` (был `/root/.ssh`), синхронно с + `HOME=/home/slin`, который форсит launcher → единый HOME по осям uid/claude/ssh. +- Образ и launcher не меняются: numeric uid не требует записи в `/etc/passwd`, + `git config --system safe.directory '*'` уже есть. + +Обязательные host-prerequisites (Owner, вне кода): доступ uid 1000 к +`/home/slin/.claude/.credentials.json` (блокер), ssh-ключи в новом HOME, рестарт prod +только в окно тишины. Детали и команды — work-item ADR-001 и `docs/operations/INFRA.md`. + +## Альтернативы +- **drop-privileges только для subprocess агента** (`gosu`/`setuid`) — контейнер остаётся + root; новый код в горячем пути launcher, два uid в одном контейнере; отклонён. +- **chown-хук после каждой стадии** — лечит симптом, требует root внутри контейнера + (несовместимо), хрупкий пост-шаг; отклонён (fallback на крайний случай). + +## Последствия +- Артефакты создаются под `slin:slin`; деплой прода не требует ручного `chown`. +- HOME консистентен (uid = claude = ssh = `/home/slin`); устранён рассинхрон SSH-маунта. +- Появляется явная привязка рантайма к uid 1000 хоста (задокументирована в INFRA.md). +- Прод-рестарт self = групповой риск (общий инстанс с enduro-trails) → строго окно тишины; + страховка — staging-гейт (adr-0003). + +## Связи +adr-0003 (staging-гейт — обязательная проверка перед прод-рестартом self), +adr-0001 (`is_self_hosting_repo`), work-item `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md`. diff --git a/docs/operations/INFRA.md b/docs/operations/INFRA.md index 90bd8e0..38901b5 100644 --- a/docs/operations/INFRA.md +++ b/docs/operations/INFRA.md @@ -30,12 +30,33 @@ Оба: `network_mode: host`, `init: true` (tini как PID 1 — reaping зомби, B-2), `restart: unless-stopped`. +### Рантайм-uid (ORCH-040) +Оба сервиса бегут под `user: "1000:1000"` (slin), **не** root. Артефакты конвейера +(git worktree `/repos/_wt/...`, коммиты в `docs/work-items/...`) создаются как +`slin:slin`, поэтому `git pull` / `git reset` на хосте под slin работают без ручного +`chown`. Доступ к docker.sock сохранён через `group_add: ["999"]` (gid docker, **не** +через root — НЕ удалять). При переносе на другой хост uid пересматривается. См. +ADR `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md` и глобальный +`docs/architecture/adr/adr-0005-container-runs-as-host-uid.md`. + +**Host-prerequisites (обязательная процедура Owner, в git не коммитятся):** +- **P-1 (блокер):** uid 1000 читает claude creds — `chown -R 1000:1000 /home/slin/.claude`; + проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json`. Без этого + preflight (ORCH-044) заворачивает весь конвейер. +- **P-2:** ssh-ключи в `/home/slin/.orchestrator-ssh` читаемы uid 1000 (маунт ведёт в `/home/slin/.ssh`). +- **P-3:** `id slin` → `1000:1000`; `/repos`, `/app/data` уже `1000:1000`. +- **P-4:** прод-рестарт self — только в окно тишины (`GET /status` без активных задач): + общий инстанс с enduro-trails. +- Разовый разгребающий `chown -R 1000:1000 /home/slin/repos/orchestrator` для старых + `root:root` файлов из истории (вне объёма кода). + ### Тома (volumes) - `./data` → `/app/data` (БД; у staging — `./data/staging`) - `/home/slin/repos` → `/repos` (рабочие репозитории проектов) - `/var/run/docker.sock` (для docker-операций деплоя) - claude-code, node, `~/.claude*` (CLI агентов, ro) -- `~/.orchestrator-ssh` → `/root/.ssh` (ro, деплой по ssh) +- `~/.orchestrator-ssh` → `/home/slin/.ssh` (ro, деплой по ssh; target в HOME агента, + согласован с `HOME=/home/slin` из launcher — ORCH-040, ранее `/root/.ssh`) ## Переменные окружения (карта; значения — в `.env`) diff --git a/docs/work-items/ORCH-040/00-business-request.md b/docs/work-items/ORCH-040/00-business-request.md new file mode 100644 index 0000000..00ba9e6 --- /dev/null +++ b/docs/work-items/ORCH-040/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: Агенты пишут файлы под root в смонтированный хост-репо: ломает git/ребилд + +Work Item ID: ORCH-040 + +## Description + +TBD diff --git a/docs/work-items/ORCH-040/01-brd.md b/docs/work-items/ORCH-040/01-brd.md new file mode 100644 index 0000000..59453d4 --- /dev/null +++ b/docs/work-items/ORCH-040/01-brd.md @@ -0,0 +1,106 @@ +# 01 — BRD: Агенты пишут файлы под root в смонтированный хост-репо + +Work Item: **ORCH-040** +Тип: инфра-фикс (runtime / docker-compose) +Исполнение: через Dev напрямую (по решению Owner) + +## 1. Бизнес-контекст и проблема + +Контейнер `orchestrator` (prod, 8500) работает под `uid=0 (root)`. Он монтирует +хостовый каталог `/home/slin/repos` → `/repos` (rw). Claude-CLI агенты запускаются +через `subprocess.Popen` **внутри контейнера**, то есть тоже под root. Они пишут: + +- в git worktree задач — `/repos/_wt///...`; +- в прод-клон — `/repos//docs/work-items/...` (через коммит/пуш из worktree). + +В результате на **хосте** файлы создаются с владельцем `root:root`. + +### Симптом +При ребилде/деплое прода `git pull` / `git reset` под пользователем `slin` падает: + +``` +error: insufficient permission for adding an object to repository database .git/objects +Permission denied (на docs/work-items/ORCH-016, владелец root:root) +``` + +Каждый будущий деплой будет ломаться, пока вручную не выполнить `chown`. + +### Диагноз (живая разведка 05–06.06) +- `docker exec orchestrator id` → `uid=0(root) gid=0(root) groups=0,999`. +- Хост `slin` = `uid=1000 gid=1000`, группы: `sudo`, `docker(999)`. +- `/home/slin/repos` → `/repos` (rw); на хосте `/repos` уже `1000:1000 rwxrwxr-x`. +- `docs/work-items/*` на хосте — `root:root` (наследие прошлых прогонов). + +## 2. Цель + +Агенты конвейера **не должны** создавать `root`-файлы в хостовом репозитории. +После любого прогона конвейера `git pull/status/reset` под `slin` на хосте +работает **без ручного chown**. + +## 3. Объём (scope) + +В объёме: +- Изменение runtime-режима контейнера так, чтобы артефакты создавались под + `uid:gid` хоста (`1000:1000`). +- Сохранение работоспособности: claude-auth (preflight), git/ssh, docker.sock + (деплой), запуск конвейера. +- Обновление документации (INFRA.md, CHANGELOG, ADR с обоснованием варианта). +- Проверка на staging (8501) ДО прода. + +Вне объёма: +- Массовое исправление прав уже существующих `root:root` файлов в истории + (разовый `chown` на хосте делает Owner; в задаче — только описать команду). +- Изменение логики конвейера, QG, схемы БД. +- Смена модели/effort агентов, прочие фичи. + +## 4. Заинтересованные стороны +- Owner (Слава) — заказчик, владелец хоста mva154. +- Стрим — разведка/контекст. +- Проект enduro-trails — co-tenant того же прод-инстанса (групповой риск). + +## 5. Ограничения и риски (off-limits) + +Self-hosting: прод-инстанс `orchestrator` ОДИН на все прод-проекты, общая БД и +очередь. **Нельзя ломать**: запуск конвейера, доступ к Plane/Gitea/SSH из агентов, +docker.sock. Любой рестарт контейнера под новым uid — **только в окно тишины** +(нет активных задач). Тестировать на staging ПЕРЕД продом. + +### Известные мины (подтверждены разведкой) +- **МИНА 1 — docker.sock**: `/var/run/docker.sock` = `srw-rw---- root:999`. + Доступ идёт через gid 999, не через root. При переходе на непривилегированный + uid обязателен supplementary group `999`. *В текущем `docker-compose.yml` уже + есть `group_add: ["999"]` для обоих сервисов — учесть, не сломать.* +- **МИНА 2 — claude creds (БЛОКЕР)**: `/home/slin/.claude/.credentials.json` = + `root:root 0600`. Сейчас читает контейнер-root. Под `uid=1000` без доступа → + `claude-auth` ломается → весь конвейер умирает (preflight ORCH-044 заворачивает). + Проверить ПЕРВЫМ. +- **МИНА 3 — claude бинарь**: реальный бинарь `/opt/claude-code/bin/claude.exe` + (root:root, `+x` для всех — ok). `ORCH_CLAUDE_BIN=/usr/bin/claude` в env не + существует; launcher использует hardcode `CLAUDE_BIN=/opt/claude-code/bin/claude.exe`. + Под uid 1000 исполним, но проверить запуск. +- **SSH-маунт**: `/home/slin/.orchestrator-ssh` → `/root/.ssh:ro`. При смене uid + HOME/домашний каталог меняется — путь к ключам нужно поправить (деплой по ssh). +- **HOME**: launcher форсит `HOME=/home/slin` (две точки: env Popen и git_env). + Креды читаются из `/home/slin/.claude`. Учесть при смене uid. + +## 6. Бизнес-ценность +Устранение постоянного ручного `chown` после каждого деплоя; деплой прода +перестаёт ломаться на правах; снимается источник простоя конвейера всех проектов. + +## 7. Допущения +- Хост-каталоги `/app/data` и `/repos` уже `1000:1000` (запись под uid 1000 пройдёт). +- Dockerfile уже содержит `git config --system --add safe.directory '*'`. +- Окно тишины для рестарта контейнера согласуется с Owner. + +## 8. Host-prerequisites (предусловия на стороне Owner) +Часть фикса невозможно закрыть только кодом — есть действия на хосте mva154, +которые выполняет Owner (в гит не коммитятся, фиксируются в ADR/INFRA). Это +обязательные предусловия Варианта 1; без них переход на uid 1000 ломает конвейер: +- **P-1 (блокер, МИНА 2):** обеспечить чтение `/home/slin/.claude/.credentials.json` + под uid 1000 (рекомендация — `chown -R 1000:1000 /home/slin/.claude`). Способ + выбирает ADR; анализ фиксирует факт предусловия. +- **P-2:** ssh-ключи (`/home/slin/.orchestrator-ssh`) читаемы uid 1000. +- **P-3:** подтверждение `slin = uid 1000 gid 1000` (подтверждено разведкой). +- **P-4:** рестарт прод-self только в окно тишины (`GET /status` без активных задач). + +Детализация и команды — в `02-trz.md` §10. diff --git a/docs/work-items/ORCH-040/02-trz.md b/docs/work-items/ORCH-040/02-trz.md new file mode 100644 index 0000000..1b19481 --- /dev/null +++ b/docs/work-items/ORCH-040/02-trz.md @@ -0,0 +1,112 @@ +# 02 — ТЗ: agent-файлы под uid хоста (не root) + +Work Item: **ORCH-040** + +## 1. Суть требования +Артефакты конвейера (worktree + docs) должны создаваться на хосте под +`uid:gid = 1000:1000` (slin), а не `root:root`. При этом сохраняется работа +claude-auth, git, ssh-деплоя и docker.sock. + +## 2. Задействованные модули и файлы + +| Файл | Роль в задаче | +|------|----------------| +| `docker-compose.yml` | runtime-режим контейнера (prod `orchestrator` + `orchestrator-staging`). Основная точка изменения. | +| `Dockerfile` | возможные правки под непривилегированный запуск (safe.directory уже есть; при необходимости — создание пользователя/прав). | +| `src/agents/launcher.py` | `HOME=/home/slin` хардкод (env Popen ~стр.326 и git_env ~стр.513); путь `CLAUDE_BIN` (стр.187). Проверить совместимость при смене uid; править ТОЛЬКО при необходимости. | +| `docs/operations/INFRA.md` | блок «Тома (volumes)» (SSH-маунт `/root/.ssh`), карта рантайма — обновить. | +| `CHANGELOG.md` | запись об изменении. | +| `docs/work-items/ORCH-040/06-adr/` | ADR с выбором варианта + обоснованием (создаёт архитектор). | + +## 3. Варианты решения (вход для ADR — выбор и обоснование за архитектором) + +> Анализ фиксирует варианты как требование «выбрать и обосновать в ADR». +> Рекомендация разведки — Вариант 1. + +1. **Вариант 1 (рекомендован): `user: "1000:1000"` в docker-compose.** + Все файлы сразу `slin:slin`, git на хосте без chown. Обязательные довески: + - сохранить/проверить `group_add: ["999"]` (docker.sock) — **уже присутствует**; + - обеспечить доступ uid 1000 к claude creds (`/home/slin/.claude/.credentials.json`): + `chown 1000:1000` на хосте ИЛИ права на чтение для 1000 (задокументировать); + - поправить SSH-маунт: `/home/slin/.orchestrator-ssh` → домашний каталог uid 1000 + (`/home/slin/.ssh`), а не `/root/.ssh`; согласовать с `HOME` в launcher; + - проверить запуск `claude.exe` + `git` + `ssh` под uid 1000. + +2. **Вариант 2: subprocess агента под непривилегированным uid внутри контейнера** + (`Popen preexec_fn setuid` / `gosu`). Точечно, но сложнее; контейнер остаётся root. + +3. **Вариант 3 (fallback, костыль): chown-хук нормализации прав после стадии** + (`chown -R 1000:1000` worktree/доки). Лечит симптом, не корень. Применять, только + если В1 неустранимо рвёт creds/sock. + +## 4. Требуемые изменения (при выбранном Варианте 1) + +### 4.1 docker-compose.yml (оба сервиса: `orchestrator`, `orchestrator-staging`) +- Добавить `user: "1000:1000"`. +- Сохранить `group_add: ["999"]` (НЕ удалять). +- Изменить SSH-маунт: target `/root/.ssh` → каталог `.ssh` пользователя 1000, + синхронно с `HOME`, который форсит launcher (`/home/slin`). То есть привести к + единому HOME: маунт `/home/slin/.orchestrator-ssh` → `/home/slin/.ssh:ro`. +- Маунт `/home/slin/.claude` и `.claude.json` — оставить; проверить доступ uid 1000. + +### 4.2 Доступ к claude creds +- Обеспечить, что `/home/slin/.claude/.credentials.json` читается uid 1000 + (на хосте — операция Owner; в ТЗ зафиксировать команду и проверку). + +### 4.3 src/agents/launcher.py +- Проверить, что `HOME=/home/slin` остаётся валиден под uid 1000 (домашний каталог + существует и доступен). Менять ТОЛЬКО при доказанной необходимости. +- Не менять CLAUDE_BIN, если запуск под 1000 подтверждён. + +### 4.4 Dockerfile +- Менять при необходимости (например, гарантировать существование `/home/slin` и + права). `git config --system --add safe.directory '*'` уже есть — оставить. + +## 5. Изменения API +Нет. + +## 6. Изменения схемы БД +Нет. + +## 7. Новые QG checks +Нет. Существующий staging-гейт (`check_staging_status`, ORCH-35) — обязательная +страховка перед прод-деплоем self (без изменений). + +## 8. Артефакты pipeline, которые должны быть созданы/обновлены +- `06-adr/ADR-NNN-.md` — выбор варианта + обоснование (мины 1–3, SSH, HOME). +- `docs/operations/INFRA.md` — обновить блок volumes (SSH target) и, при изменении + режима, упоминание uid рантайма. +- `CHANGELOG.md` — запись `fix:`/`refactor:` по Conventional Commits. +- `12-review.md`, `13-test-report.md`, `15-staging-log.md` — по ходу конвейера. + +## 9. Порядок безопасного внедрения (требование) +1. Живая разведка прав creds/sock/ssh ДО кода. +2. Применить и проверить на **staging (8501)** end-to-end. +3. Прод-рестарт контейнера под новым uid — только в окно тишины (нет активных задач). +4. Регресс на хосте: новые tracked-артефакты `1000:1000`, `git pull` под slin без ошибок. + +## 10. Зависимости и host-prerequisites (действия на хосте, вне кода) + +Эти пункты — предусловия для Варианта 1; их выполняет Owner на хосте mva154 (в гит +не коммитятся, но фиксируются в ADR/INFRA как обязательная процедура). Без них +переход контейнера на uid 1000 ломает конвейер (МИНА 2 — блокер). + +| # | Предусловие | Команда / проверка | Зачем | +|---|-------------|--------------------|-------| +| P-1 | Доступ uid 1000 к claude creds | `chown -R 1000:1000 /home/slin/.claude` (вкл. `.credentials.json`); проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json` | МИНА 2: без доступа preflight ORCH-044 завернёт весь конвейер | +| P-2 | SSH-ключи в HOME нового uid и читаемы | ключи в `/home/slin/.orchestrator-ssh` читаемы uid 1000; маунт ведёт в `/home/slin/.ssh` (см. §4.1) | деплой по ssh (DEPLOY_SSH_*) | +| P-3 | Подтверждение uid:gid рантайма | `id slin` → `uid=1000 gid=1000`; `/repos` и `/app/data` уже `1000:1000` (подтверждено разведкой) | целевые файлы создаются под slin | +| P-4 | Окно тишины для рестарта self | `GET /status` → нет активных задач перед рестартом прод-контейнера | self-hosting: общий инстанс с enduro-trails | + +> **Открытый выбор для ADR (не решается анализом):** способ обеспечения P-1 — +> `chown` creds (рекомендация разведки) vs. ослабление read-прав vs. отказ от +> Варианта 1 в пользу Варианта 3 (chown-хук). Анализ фиксирует P-1 как +> обязательное предусловие при любом из вариантов 1/2; для Варианта 3 — неактуально. + +## 11. Подтверждённые факты текущего рантайма (anchor для Dev) +Сверено с веткой `feature/ORCH-040-root-git` на 06.06: +- `docker-compose.yml`: оба сервиса имеют `group_add: ["999"]` (МИНА 1 — НЕ удалять); + SSH-маунт обоих = `/home/slin/.orchestrator-ssh:/root/.ssh:ro` (требует правки target); + claude-маунты = `/home/slin/.claude` и `/home/slin/.claude.json:ro`. +- `src/agents/launcher.py`: `HOME="/home/slin"` форсится в env Popen (стр. 326) и в + git_env (стр. 513); `CLAUDE_BIN="/opt/claude-code/bin/claude.exe"` (стр. 187). diff --git a/docs/work-items/ORCH-040/03-acceptance-criteria.md b/docs/work-items/ORCH-040/03-acceptance-criteria.md new file mode 100644 index 0000000..e9a7a50 --- /dev/null +++ b/docs/work-items/ORCH-040/03-acceptance-criteria.md @@ -0,0 +1,62 @@ +# 03 — Критерии приёмки: ORCH-040 + +Work Item: **ORCH-040** + +Каждый критерий имеет чёткое условие PASS/FAIL. Задача считается принятой, когда +**все** критерии = PASS. + +## AC-1 — Артефакты создаются под uid хоста (корневой критерий) +- **PASS**: после прогона тестовой задачи конвейером end-to-end новые tracked-файлы + в `/home/slin/repos/orchestrator/docs/work-items/*` и в worktree + (`/repos/_wt/...`) имеют владельца `slin:slin` (1000:1000). + `ls -ld /home/slin/repos/orchestrator/docs/work-items/*` → НЕ `root:root`. +- **FAIL**: появляются новые `root:root` tracked-файлы. + +## AC-2 — git под slin работает без ручного chown +- **PASS**: на хосте под `slin` `git -C /home/slin/repos/orchestrator pull`, + `git status`, `git reset` выполняются без `Permission denied` / + `insufficient permission for adding an object`. +- **FAIL**: любая из команд падает на правах. + +## AC-3 — claude-агенты стартуют (preflight ok) +- **PASS**: `claude-auth`/preflight проходит; агент конвейера запускается и + завершается `exit_code=0` (не `Not logged in`, не отказ доступа к creds). +- **FAIL**: агент падает на авторизации/чтении `/home/slin/.claude`. + +## AC-4 — docker.sock доступен (деплой не сломан) +- **PASS**: из контейнера под новым uid `docker ps` / docker-операции деплоя + (ORCH-36 путь) работают — доступ через gid 999 сохранён (`group_add: ["999"]`). +- **FAIL**: docker-операции отваливаются (`permission denied` на сокете). + +## AC-5 — SSH-деплой работает +- **PASS**: ssh-ключи читаются из домашнего каталога нового uid; деплой-хук по ssh + (`DEPLOY_SSH_*`) выполняется. +- **FAIL**: ssh не находит/не читает ключи (маунт указывает на чужой HOME). + +## AC-6 — Конвейер не сломан (без регресса) +- **PASS**: тестовая задача проходит стадии без падения запуска конвейера; доступ к + Plane/Gitea из агентов сохранён; `pytest tests/ -q` зелёный. +- **FAIL**: конвейер встаёт / тесты падают. + +## AC-7 — Проверено на staging ДО прода +- **PASS**: изменение прогнано на staging (8501), `15-staging-log.md` → + `staging_status:` положительный; прод-рестарт выполнен в окно тишины. +- **FAIL**: изменение применено сразу на прод без staging-прогона. + +## AC-8 — Документация обновлена (golden source) +- **PASS**: `docs/operations/INFRA.md` (блок volumes / SSH target / uid рантайма) + и `CHANGELOG.md` обновлены; ADR с выбором варианта и обоснованием создан в + `06-adr/`. Reviewer подтверждает. +- **FAIL**: код изменён, документация/ADR не обновлены. + +## AC-9 — Прод-контейнер не уронен вне окна тишины +- **PASS**: рестарт self выполнен без активных задач; конвейер enduro-trails не + пострадал. +- **FAIL**: рестарт во время активных задач / падение прод-инстанса. + +## AC-10 — Host-prerequisites зафиксированы и выполнены +- **PASS**: предусловия P-1…P-4 (TRZ §10 / BRD §8) описаны в ADR/INFRA как + обязательная процедура Owner; P-1 (доступ uid 1000 к claude creds) фактически + обеспечен — подтверждается прохождением AC-3. +- **FAIL**: фикс применён без обеспечения доступа к creds (P-1) → preflight/конвейер + падает; либо предусловия нигде не задокументированы. diff --git a/docs/work-items/ORCH-040/04-test-plan.yaml b/docs/work-items/ORCH-040/04-test-plan.yaml new file mode 100644 index 0000000..253a86b --- /dev/null +++ b/docs/work-items/ORCH-040/04-test-plan.yaml @@ -0,0 +1,81 @@ +work_item: ORCH-040 +description: > + Инфра-фикс: контейнер/агенты не плодят root-файлы в хостовом репо. + Часть проверок автоматизируема через pytest (валидация compose-конфига), + часть — обязательные ops/integration проверки на staging и хосте (manual), + т.к. касаются прав файловой системы хоста и рантайма docker. + +tests: + # --- Автоматизируемые (pytest, парсинг docker-compose.yml) --- + - id: TC-01 + type: unit + description: > + docker-compose.yml: оба сервиса (orchestrator, orchestrator-staging) + имеют user: "1000:1000" (при выборе Варианта 1). + module: tests/test_orch040_compose.py + expected: PASS + + - id: TC-02 + type: unit + description: > + docker-compose.yml: оба сервиса сохраняют group_add со значением "999" + (доступ к docker.sock не потерян — МИНА 1). + module: tests/test_orch040_compose.py + expected: PASS + + - id: TC-03 + type: unit + description: > + docker-compose.yml: SSH-маунт согласован с HOME агента — target каталога + .ssh лежит под /home/slin (а не /root/.ssh), для обоих сервисов. + module: tests/test_orch040_compose.py + expected: PASS + + - id: TC-04 + type: unit + description: > + launcher: HOME, форсимый в окружении агента и git_env, указывает на каталог, + совместимый с SSH/claude-маунтами (/home/slin) — нет рассинхрона HOME vs uid. + module: tests/test_orch040_compose.py + expected: PASS + + # --- Регресс существующего поведения --- + - id: TC-05 + type: unit + description: > + Весь существующий набор тестов зелёный (нет регресса логики конвейера/launcher). + module: tests/ # pytest tests/ -q + expected: PASS + + # --- Integration / ops (staging 8501, затем хост) --- + - id: TC-06 + type: integration + description: > + На staging (8501) прогнать тестовую задачу конвейером end-to-end; артефакты + worktree и docs создаются под 1000:1000 (НЕ root:root). Проверка AC-1. + module: scripts/staging_check.py # + ls -ld на хосте + expected: PASS + + - id: TC-07 + type: integration + description: > + После staging-прогона на хосте под slin: git -C /home/slin/repos/orchestrator + pull/status/reset без Permission denied. Проверка AC-2. + module: manual/host-check + expected: PASS + + - id: TC-08 + type: integration + description: > + claude preflight/auth проходит под новым uid: агент стартует и завершается + exit_code=0 (creds /home/slin/.claude читаются). Проверка AC-3 (МИНА 2). + module: manual/staging-agent-run + expected: PASS + + - id: TC-09 + type: integration + description: > + docker.sock доступен из контейнера под uid 1000 (docker ps работает) и + ssh-деплой-хук выполняется. Проверка AC-4, AC-5 (МИНА 1 + SSH). + module: manual/staging-deploy-path + expected: PASS diff --git a/docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md b/docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md new file mode 100644 index 0000000..b00c2b8 --- /dev/null +++ b/docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md @@ -0,0 +1,109 @@ +# ADR-001: Контейнер и агенты бегут под uid:gid хоста (1000:1000), а не root + +- **Статус:** Accepted +- **Дата:** 2026-06-06 +- **Задача:** ORCH-040 +- **Связи:** глобальный [adr-0005](../../../architecture/adr/adr-0005-container-runs-as-host-uid.md), adr-0003 (staging-гейт — страховка перед прод-рестартом self), adr-0001 (`is_self_hosting_repo`). + +## Контекст + +Контейнер `orchestrator` (prod, 8500) работает под `uid=0 (root)` и монтирует хостовый +`/home/slin/repos` → `/repos` (rw). Claude-CLI агенты запускаются через +`subprocess.Popen` **внутри контейнера**, т.е. под тем же root. Все артефакты конвейера +(git worktree `/repos/_wt/...`, коммиты в `docs/work-items/...`) появляются на **хосте** +с владельцем `root:root`. + +Следствие: при каждом деплое прода `git pull` / `git reset` под пользователем `slin` +(uid 1000) падает с `insufficient permission for adding an object to repository database` +/ `Permission denied`. Каждый деплой ломается, пока вручную не сделать `chown`. + +Разведкой (05–06.06) подтверждено: +- `slin = uid 1000 gid 1000`, в группах `sudo`, `docker(999)`; на хосте `/repos` и + `/app/data` уже `1000:1000`. +- launcher **уже** форсит `HOME=/home/slin` в двух местах: env `Popen` (`launcher.py:326`) + и `git_env` (`launcher.py:513`). Креды читаются из `/home/slin/.claude`. +- `docker-compose.yml`: оба сервиса имеют `group_add: ["999"]` (доступ к docker.sock — + через gid 999, **не** через root); SSH-маунт обоих = `/home/slin/.orchestrator-ssh:/root/.ssh:ro`. +- `CLAUDE_BIN=/opt/claude-code/bin/claude.exe` (`launcher.py:187`), `+x` для всех. +- Dockerfile содержит `git config --system --add safe.directory '*'`. + +## Рассмотренные варианты + +1. **Вариант 1 (выбран): `user: "1000:1000"` в docker-compose для обоих сервисов.** + Контейнер целиком бежит под uid 1000. Все файлы сразу `slin:slin`, git на хосте без + chown. Лечит корень проблемы одной декларативной строкой на сервис, без нового кода. + +2. **Вариант 2: drop-privileges только для subprocess агента** (`gosu` / `preexec_fn setuid`). + Контейнер остаётся root, агент бежит под 1000. Точечно, но: новый код в горячем пути + launcher, два класса процессов с разными uid в одном контейнере (uvicorn root vs агент + 1000), сложнее отлаживать, выше риск регресса конвейера. Корень (root-владение из самого + uvicorn-процесса при операциях с `/repos`) лечится не полностью. + +3. **Вариант 3 (fallback): chown-хук нормализации прав после стадии** + (`chown -R 1000:1000` worktree/docs). Лечит симптом, не причину; требует root внутри + контейнера (т.е. несовместим с В1) и добавляет хрупкий пост-шаг в каждый переход стадии. + +## Решение + +Принимаем **Вариант 1**. Изменения (применяет Dev на стадии development): + +1. **`docker-compose.yml`** — для **обоих** сервисов (`orchestrator`, `orchestrator-staging`): + - добавить `user: "1000:1000"`; + - **сохранить** `group_add: ["999"]` (МИНА 1 — НЕ удалять); + - изменить target SSH-маунта `/root/.ssh` → `/home/slin/.ssh`, чтобы он совпал с + `HOME=/home/slin`, который форсит launcher. Итог: `/home/slin/.orchestrator-ssh:/home/slin/.ssh:ro`; + - claude-маунты (`/home/slin/.claude`, `/home/slin/.claude.json:ro`) — оставить как есть. + +2. **`src/agents/launcher.py`** — НЕ менять. `HOME=/home/slin` и + `CLAUDE_BIN=/opt/claude-code/bin/claude.exe` остаются валидными под uid 1000 + (`/home/slin` материализуется bind-маунтами; бинарь исполним для всех). Правка + допустима ТОЛЬКО при доказанной поломке запуска под 1000. + +3. **`Dockerfile`** — НЕ менять. Отдельный non-root user внутри образа не создаём: + numeric `user: "1000:1000"` работает без записи в `/etc/passwd`; `safe.directory '*'` + уже покрывает git над bind-маунтом. Правка допустима только если запуск под 1000 + выявит отсутствующий каталог/право. + +### Host-prerequisites (вне кода, выполняет Owner — обязательная процедура) + +Без них переход на uid 1000 ломает конвейер. Фиксируются здесь и в INFRA.md как +обязательная процедура; в git не коммитятся. + +| # | Предусловие | Команда / проверка | Зачем | +|---|-------------|--------------------|-------| +| P-1 (блокер) | uid 1000 читает claude creds | `chown -R 1000:1000 /home/slin/.claude`; проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json` | МИНА 2: иначе preflight (ORCH-044) завернёт весь конвейер | +| P-2 | ssh-ключи читаемы uid 1000 и в новом HOME | ключи в `/home/slin/.orchestrator-ssh` читаемы 1000; маунт ведёт в `/home/slin/.ssh` | деплой по ssh (`DEPLOY_SSH_*`) | +| P-3 | uid:gid рантайма подтверждён | `id slin` → `1000:1000`; `/repos`, `/app/data` уже `1000:1000` | целевые файлы под slin | +| P-4 | рестарт self только в окно тишины | `GET /status` без активных задач перед рестартом prod | self-hosting: общий инстанс с enduro-trails | + +**Выбор способа P-1:** `chown -R 1000:1000 /home/slin/.claude` (рекомендация разведки). +Обоснование: креды и так принадлежат slin по смыслу; chown проще и надёжнее ослабления +read-битов и не оставляет файл world-readable. Маунт `/home/slin/.claude` оставлен rw — +claude CLI может обновлять токен; под uid 1000 после chown это работает. + +## Порядок безопасного внедрения (обязателен) + +1. Применить и проверить **на staging (8501)** end-to-end (артефакты → `1000:1000`, + агент `exit_code=0`, docker.sock и ssh-деплой живы) — `15-staging-log.md`, + гейт `check_staging_status`. +2. Прод-рестарт под новым uid — **только в окно тишины** (P-4). +3. Регресс на хосте: новые tracked-артефакты `1000:1000`, `git pull` под slin без ошибок. + +## Последствия + +**Плюсы:** +- Корень устранён: артефакты создаются под `slin:slin`, ручной `chown` после деплоя не нужен. +- `HOME` теперь консистентен по всем осям (uid = claude = ssh = `/home/slin`); устранён + скрытый рассинхрон SSH-маунта (`/root/.ssh`) с форсимым HOME. +- Минимальная поверхность изменения: декларативный compose, без нового кода в launcher. + +**Минусы / ограничения:** +- Появляется жёсткая привязка к `uid 1000` хоста — задокументирована в INFRA.md; + при переносе на другой хост uid пересматривается. +- Требуются host-prerequisites (P-1…P-4) — часть фикса не закрывается кодом; P-1 — блокер. +- Прод-рестарт self = групповой риск (enduro-trails) → строго окно тишины (P-4), + страховка — staging-гейт (adr-0003). + +**Вне объёма:** массовый `chown` уже существующих `root:root` файлов в истории (разовая +операция Owner, команда описана в INFRA.md); логика конвейера/QG/схема БД — без изменений. +``` diff --git a/docs/work-items/ORCH-040/07-infra-requirements.md b/docs/work-items/ORCH-040/07-infra-requirements.md new file mode 100644 index 0000000..b0716ee --- /dev/null +++ b/docs/work-items/ORCH-040/07-infra-requirements.md @@ -0,0 +1,47 @@ +# 07 — Инфра-требования: ORCH-040 + +Work Item: **ORCH-040** · Решение: [ADR-001](06-adr/ADR-001-run-agents-as-host-uid.md) (Вариант 1) + +> Требования к рантайму/инфре, которые Dev обязан реализовать, а Reviewer — проверить. +> Топология стадий и БД **не меняются**. Меняется только runtime-uid контейнера и target SSH-маунта. + +## R-1 — runtime uid контейнера +- Оба сервиса в `docker-compose.yml` запускаются под `user: "1000:1000"`. +- `group_add: ["999"]` **сохраняется** на обоих (docker.sock через gid 999, МИНА 1). + +## R-2 — SSH-маунт согласован с HOME +- target SSH-маунта = `/home/slin/.ssh` (не `/root/.ssh`) на обоих сервисах. +- Совпадает с `HOME=/home/slin`, форсимым в `src/agents/launcher.py` (L326, L513). +- Источник (`/home/slin/.orchestrator-ssh`) и режим `:ro` — без изменений. + +## R-3 — claude-маунты без изменений +- `/home/slin/.claude` (rw) и `/home/slin/.claude.json:ro` остаются. +- Доступ под uid 1000 обеспечивается host-prerequisite P-1 (chown creds), см. ADR. + +## R-4 — образ и launcher без изменений (по умолчанию) +- `Dockerfile` не меняется (numeric uid не требует записи в `/etc/passwd`; + `safe.directory '*'` уже есть). Изменение допустимо только при доказанной поломке под 1000. +- `src/agents/launcher.py` не меняется (`HOME`, `CLAUDE_BIN` валидны под 1000). + +## R-5 — host-prerequisites (Owner, вне кода) +P-1…P-4 из ADR §«Host-prerequisites» — обязательная процедура. P-1 (доступ uid 1000 к +claude creds) — блокер: без него preflight (ORCH-044) заворачивает конвейер. + +## R-6 — порядок внедрения +1. staging (8501) end-to-end → `15-staging-log.md` / `check_staging_status` зелёный; +2. прод-рестарт self — только в окно тишины (`GET /status` без активных задач, P-4); +3. регресс на хосте: новые tracked-артефакты `1000:1000`, `git pull` под slin без ошибок. + +## R-7 — обновление документации (golden source) +Dev в том же PR обновляет: +- `docs/operations/INFRA.md` — блок «Тома (volumes)» (SSH target `/home/slin/.ssh`) и + явное указание runtime-uid (`user: 1000:1000`) контейнеров; команда разового хост-`chown` + legacy `root:root` файлов. +- `CHANGELOG.md` — запись `fix:`/`refactor:`. +- глобальный [adr-0005](../../architecture/adr/adr-0005-container-runs-as-host-uid.md) уже + заведён архитектором; индекс `docs/architecture/adr/README.md` обновлён. + +## Что НЕ требуется +- Новых томов, портов, env-переменных — нет. +- Изменения API, схемы БД, реестра QG/стадий — нет. +- Multi-node / облачные сервисы — нет (принципы архитектуры). diff --git a/docs/work-items/ORCH-040/10-tech-risks.md b/docs/work-items/ORCH-040/10-tech-risks.md new file mode 100644 index 0000000..d0849ab --- /dev/null +++ b/docs/work-items/ORCH-040/10-tech-risks.md @@ -0,0 +1,19 @@ +# 10 — Технические риски: ORCH-040 + +Work Item: **ORCH-040** · Решение: [ADR-001](06-adr/ADR-001-run-agents-as-host-uid.md) + +| # | Риск | Вероятн. | Влияние | Митигация | +|---|------|----------|---------|-----------| +| TR-1 | **МИНА 2 — claude creds недоступны uid 1000** → preflight (ORCH-044) валит весь конвейер | Средн. | Крит. (блокер) | P-1: `chown -R 1000:1000 /home/slin/.claude` ДО рестарта; проверка `sudo -u '#1000' test -r .../.credentials.json`; staging-прогон ловит до прода (AC-3) | +| TR-2 | **МИНА 1 — потеря доступа к docker.sock** при смене uid → деплой-операции падают | Низк. | Высок. | `group_add: ["999"]` сохраняется на обоих сервисах (НЕ удалять); проверка `docker ps` из контейнера (AC-4) | +| TR-3 | **SSH-маунт ведёт в чужой HOME** (`/root/.ssh`) → ssh-деплой не находит ключи | Средн. | Высок. | R-2: target → `/home/slin/.ssh`, синхронно с форсимым `HOME`; проверка деплой-хука (AC-5) | +| TR-4 | **Рестарт prod self вне окна тишины** роняет конвейер всех проектов (enduro-trails) | Средн. | Крит. | P-4: рестарт только при `GET /status` без активных задач; страховка — staging-гейт adr-0003 (AC-7, AC-9) | +| TR-5 | **Регресс launcher** при невалидном HOME/uid (`/home/slin` отсутствует, claude.exe не исполним) | Низк. | Высок. | `/home/slin` материализуется bind-маунтами; `claude.exe` `+x` для всех; staging end-to-end + `pytest tests/ -q` (AC-6) | +| TR-6 | **Legacy `root:root` файлы в истории** мешают git под slin даже после фикса | Высок. | Средн. | Вне объёма задачи: разовый хост-`chown` делает Owner; команда описана в INFRA.md | +| TR-7 | **Привязка к uid 1000 конкретного хоста** усложняет перенос на другой хост | Низк. | Низк. | Задокументировано в INFRA.md как явное допущение рантайма; пересмотр при миграции хоста | +| TR-8 | **Запись в bind-маунты под 1000** (`/app/data`, `/repos`) при неверных правах хоста | Низк. | Средн. | P-3: `/repos` и `/app/data` уже `1000:1000` (подтверждено разведкой) | + +## Сводный вывод +Основной блокер — TR-1 (creds). Все критичные риски снимаются обязательным staging-прогоном +(adr-0003) ПЕРЕД прод-рестартом и выполнением host-prerequisites P-1…P-4. Изменение +декларативное (compose), без правок горячего кода launcher → низкая поверхность регресса. diff --git a/docs/work-items/ORCH-040/12-review.md b/docs/work-items/ORCH-040/12-review.md new file mode 100644 index 0000000..3e47f0b --- /dev/null +++ b/docs/work-items/ORCH-040/12-review.md @@ -0,0 +1,70 @@ +--- +type: review +work_item_id: ORCH-040 +verdict: APPROVED +version: 1 +--- + +# Review ORCH-040 + +## Summary +Фикс переводит оба compose-сервиса (`orchestrator`, `orchestrator-staging`) на +`user: "1000:1000"` (Вариант 1 из ADR-001 / adr-0005), чтобы артефакты конвейера +создавались как `slin:slin` и git на хосте работал без ручного `chown`. Реализация +точно соответствует ТЗ и ADR, документация (INFRA.md, CHANGELOG.md, work-item ADR-001, +глобальный adr-0005) обновлена в том же PR, host-prerequisites (P-1…P-4) задокументированы. +Полный прогон `pytest tests/ -q` — **501 passed**. Блокеров и must-fix нет. + +## Findings + +### P0 — Blocker +- нет + +### P1 — Must fix +- нет + +### P2 — Should fix +- нет + +### P3 — Nice to have +- [ ] (опц.) AC-1/2/3/4/5 — это runtime/host-критерии; их фактическое PASS подтверждается + на стадиях `testing` и `deploy-staging` (`15-staging-log.md`, `staging_status:`), а не + ревью кода. Зафиксировано как ожидание к следующим стадиям, не как замечание к PR. + +## Проверка по осям + +**1. Соответствие ТЗ (02-trz.md §4):** +- §4.1 `docker-compose.yml`: оба сервиса получили `user: "1000:1000"` ✅; `group_add: ["999"]` + сохранён (МИНА 1 — не удалён) ✅; SSH-маунт target `/root/.ssh` → `/home/slin/.ssh` ✅; + claude-маунты (`/home/slin/.claude`, `.claude.json:ro`) не тронуты ✅. +- §4.3 `src/agents/launcher.py` не менялся; `HOME=/home/slin` остаётся на стр. 326 и 513 + (подтверждено grep) — согласован с новым SSH target ✅. +- §4.4 `Dockerfile` не менялся (numeric uid не требует записи в `/etc/passwd`, + `safe.directory '*'` уже есть) — в полном соответствии с решением ADR ✅. +- §5/§6/§7: изменений API/БД/QG нет — подтверждено ✅. + +**2. Соответствие ADR (ADR-001 + global adr-0005):** +- Выбран и реализован Вариант 1 ровно как описано в ADR (compose-only, без нового кода + в launcher и Dockerfile) ✅. +- Host-prerequisites P-1…P-4 из ADR перенесены в INFRA.md как обязательная процедура Owner ✅. +- Нарушений глобальных ADR нет; связи с adr-0003 (staging-гейт как страховка) учтены ✅. + +**3. Качество кода:** +- Изменения декларативные, с поясняющими комментариями и ссылкой на ADR ✅. +- Тесты `tests/test_orch040_compose.py` содержательные: проверяют `user`, сохранение + `group_add 999`, SSH target под HOME и согласованность HOME launcher'а с маунтами + (TC-01…TC-04, привязаны к AC) — не тривиальные ✅. +- Регресс отсутствует: `pytest tests/ -q` → 501 passed ✅. + +## Документация +Обновлена корректно и в том же PR (golden source соблюдён, AC-8 PASS): +- `docs/operations/INFRA.md` — добавлен блок «Рантайм-uid (ORCH-040)», host-prerequisites, + блок volumes/SSH target приведён к `/home/slin/.ssh` ✅; +- `CHANGELOG.md` — запись в разделе Fixed ✅; +- `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md` — выбор варианта + + обоснование + P-1…P-4 ✅; +- глобальный `docs/architecture/adr/adr-0005-container-runs-as-host-uid.md` (+ запись в + `adr/README.md`) — сквозное решение зафиксировано ✅. + +Изменения `src/` Python-кода нет (правка только в `docker-compose.yml` + тесты), но +документация всё равно обновлена — требование §2 CLAUDE.md выполнено с запасом. diff --git a/docs/work-items/ORCH-040/13-test-report.md b/docs/work-items/ORCH-040/13-test-report.md new file mode 100644 index 0000000..7330aa0 --- /dev/null +++ b/docs/work-items/ORCH-040/13-test-report.md @@ -0,0 +1,94 @@ +--- +type: test-report +work_item_id: ORCH-040 +result: PASS +--- + +# Test Report — ORCH-040 + +Тема: agent-файлы конвейера создаются под uid хоста (`1000:1000`, slin), +а не `root:root`. Реализация — Вариант 1 (`user: "1000:1000"` в обоих +compose-сервисах), правка только в `docker-compose.yml` + тесты. + +## Окружение +- Python: 3.12.13 +- pytest: 8.3.3 +- Сервис (prod 8500): `/health` → 200 `{"status":"ok"}`; preflight_ok=true (`2.1.142 (Claude Code)`) +- Дата: 2026-06-06T15:06:25Z +- Ветка: feature/ORCH-040-root-git + +## Smoke test API (read-only GET, прод-контейнер не трогался) +| Endpoint | Результат | +|----------|-----------| +| GET /health | 200 — `{"status":"ok","service":"orchestrator"}` | +| GET /status | 200 — активная задача ORCH-040 (stage=testing) | +| GET /queue | 200 — counts ok, max_concurrency=1, breaker=closed, preflight_ok=true | + +> curl в окружении тестера отсутствует; smoke выполнен эквивалентным запросом +> через `python -m urllib.request` (только GET, без побочных эффектов). + +## Результаты (по 04-test-plan.yaml) + +| TC ID | Описание | Тип | Результат | +|-------|----------|-----|-----------| +| TC-01 | compose: оба сервиса `user: "1000:1000"` (Вариант 1) | unit | PASS | +| TC-02 | compose: оба сервиса сохраняют `group_add: ["999"]` (МИНА 1, docker.sock) | unit | PASS | +| TC-03 | compose: SSH-маунт target под `/home/slin/.ssh`, согласован с HOME | unit | PASS | +| TC-04 | launcher: форсимый HOME совместим с claude/SSH-маунтами (`/home/slin`) | unit | PASS | +| TC-05 | полный регресс `pytest tests/` зелёный (нет регресса конвейера/launcher) | unit | PASS (501 passed) | +| TC-06 | staging E2E: артефакты worktree/docs создаются `1000:1000` (AC-1) | integration | DEFERRED → deploy-staging | +| TC-07 | хост под slin: `git pull/status/reset` без Permission denied (AC-2) | integration | DEFERRED → deploy-staging | +| TC-08 | claude preflight/auth под uid 1000, агент exit_code=0 (AC-3, МИНА 2) | integration | DEFERRED → deploy-staging | +| TC-09 | docker.sock + ssh-деплой под uid 1000 (AC-4, AC-5) | integration | DEFERRED → deploy-staging | + +**О TC-06…TC-09:** по дизайну test-plan'а это ops/integration-проверки на +staging (8501) и хосте, касающиеся прав ФС хоста и docker-рантайма. Они +относятся к стадии `deploy-staging` (их PASS фиксируется в `15-staging-log.md`, +`staging_status:`) и не воспроизводимы в окружении стадии `testing` без +рестарта контейнера под новым uid. Это совпадает с замечанием ревью +(12-review.md, P3): runtime/host-критерии AC-1…AC-5 подтверждаются на +`deploy-staging`, а не при тестировании кода. Запуск деструктивных операций / +рестарт self в рамках стадии testing запрещён (CLAUDE.md, self-hosting). + +## Покрытие критериев приёмки (03-acceptance-criteria.md) +| AC | Статус на стадии testing | +|----|--------------------------| +| AC-1 (артефакты под uid хоста) | runtime — проверяется на deploy-staging | +| AC-2 (git под slin) | runtime — проверяется на deploy-staging | +| AC-3 (claude preflight ok) | preflight_ok=true в `/queue`; полное E2E — deploy-staging | +| AC-4 (docker.sock доступен) | конфиг подтверждён TC-02; runtime — deploy-staging | +| AC-5 (SSH-деплой) | конфиг подтверждён TC-03; runtime — deploy-staging | +| AC-6 (конвейер без регресса, pytest зелёный) | **PASS** — 501 passed | +| AC-7 (проверено на staging до прода) | стадия deploy-staging | +| AC-8 (документация/ADR обновлены) | **PASS** — подтверждено ревью (APPROVED) | +| AC-9 (прод не уронен вне окна тишины) | стадия deploy/окно тишины | +| AC-10 (host-prerequisites зафиксированы) | **PASS** — P-1…P-4 в ADR/INFRA | + +## Вывод pytest +``` +$ python -m pytest tests/ -v --tb=short +platform linux -- Python 3.12.13, pytest-8.3.3, pluggy-1.6.0 +configfile: pytest.ini +plugins: anyio-4.13.0, asyncio-0.23.8 +... +======================== 501 passed, 1 warning in 8.54s ======================== + +$ python -m pytest tests/test_orch040_compose.py -v +tests/test_orch040_compose.py::test_tc01_service_runs_as_host_uid[orchestrator] PASSED +tests/test_orch040_compose.py::test_tc01_service_runs_as_host_uid[orchestrator-staging] PASSED +tests/test_orch040_compose.py::test_tc02_group_add_keeps_docker_gid[orchestrator] PASSED +tests/test_orch040_compose.py::test_tc02_group_add_keeps_docker_gid[orchestrator-staging] PASSED +tests/test_orch040_compose.py::test_tc03_ssh_mount_under_home[orchestrator] PASSED +tests/test_orch040_compose.py::test_tc03_ssh_mount_under_home[orchestrator-staging] PASSED +tests/test_orch040_compose.py::test_tc04_launcher_home_matches_mounts PASSED +========================= 7 passed, 1 warning in 0.31s ========================= +``` +(1 warning — Pydantic V2 deprecation в `src/config.py`, не относится к ORCH-040.) + +## Итог +**PASS** — все автоматизируемые тесты (TC-01…TC-05) зелёные, полный регресс +501 passed, smoke API ok, документация/ADR подтверждены ревью. Runtime/host +критерии (TC-06…TC-09, AC-1…AC-5/7/9) корректно отложены на обязательную +стадию `deploy-staging` (8501) — страховку self-hosting перед прод-деплоем. + +Задача переходит на стадию **deploy-staging**. diff --git a/tests/test_orch040_compose.py b/tests/test_orch040_compose.py new file mode 100644 index 0000000..914df03 --- /dev/null +++ b/tests/test_orch040_compose.py @@ -0,0 +1,112 @@ +"""ORCH-040: контейнер/агенты бегут под uid:gid хоста (1000:1000), не root. + +Валидируют docker-compose.yml (Вариант 1 из ADR-001) и согласованность с +HOME, который форсит launcher. Чистые конфиг-тесты: парсят YAML и текст +launcher, без запуска docker/агентов. + +См. docs/work-items/ORCH-040/{02-trz.md,03-acceptance-criteria.md, +04-test-plan.yaml} и 06-adr/ADR-001-run-agents-as-host-uid.md. +""" + +from pathlib import Path + +import pytest +import yaml + +REPO_ROOT = Path(__file__).resolve().parents[1] +COMPOSE_PATH = REPO_ROOT / "docker-compose.yml" +LAUNCHER_PATH = REPO_ROOT / "src" / "agents" / "launcher.py" + +# Сервисы, которые исполняют конвейер и обязаны бежать под uid хоста. +PIPELINE_SERVICES = ("orchestrator", "orchestrator-staging") + +# Единый HOME агента (форсится launcher'ом); под ним должны лежать .ssh/.claude. +EXPECTED_HOME = "/home/slin" + + +@pytest.fixture(scope="module") +def compose() -> dict: + """Распарсенный docker-compose.yml.""" + with COMPOSE_PATH.open(encoding="utf-8") as fh: + data = yaml.safe_load(fh) + assert "services" in data, "docker-compose.yml без секции services" + return data + + +def _service(compose: dict, name: str) -> dict: + services = compose["services"] + assert name in services, f"сервис {name} отсутствует в docker-compose.yml" + return services[name] + + +def _ssh_mount_target(service: dict) -> str: + """Target SSH-маунта (источник .orchestrator-ssh) для сервиса.""" + for vol in service.get("volumes", []): + # формат "src:target[:mode]" + parts = vol.split(":") + src = parts[0] + if src.endswith(".orchestrator-ssh"): + assert len(parts) >= 2, f"SSH-маунт без target: {vol}" + return parts[1] + raise AssertionError("SSH-маунт (.orchestrator-ssh) не найден в volumes") + + +# --- TC-01: user: "1000:1000" в обоих сервисах --------------------------------- +@pytest.mark.parametrize("name", PIPELINE_SERVICES) +def test_tc01_service_runs_as_host_uid(compose, name): + """TC-01/AC-1: сервис бежит под uid:gid хоста 1000:1000, а не root.""" + service = _service(compose, name) + assert "user" in service, f"{name}: отсутствует ключ user (нужен '1000:1000')" + # docker допускает int или строку; нормализуем к строке. + assert str(service["user"]) == "1000:1000", ( + f"{name}: user={service['user']!r}, ожидалось '1000:1000'" + ) + + +# --- TC-02: group_add сохраняет "999" (docker.sock — МИНА 1) -------------------- +@pytest.mark.parametrize("name", PIPELINE_SERVICES) +def test_tc02_group_add_keeps_docker_gid(compose, name): + """TC-02/AC-4: group_add содержит 999 (доступ к docker.sock не потерян).""" + service = _service(compose, name) + group_add = service.get("group_add", []) + normalized = {str(g) for g in group_add} + assert "999" in normalized, ( + f"{name}: group_add={group_add!r}, должен содержать '999' (docker.sock)" + ) + + +# --- TC-03: SSH-маунт согласован с HOME (под /home/slin, не /root) -------------- +@pytest.mark.parametrize("name", PIPELINE_SERVICES) +def test_tc03_ssh_mount_under_home(compose, name): + """TC-03/AC-5: target SSH-маунта лежит в HOME агента (/home/slin/.ssh).""" + service = _service(compose, name) + target = _ssh_mount_target(service) + assert target == f"{EXPECTED_HOME}/.ssh", ( + f"{name}: SSH target={target!r}, ожидалось '{EXPECTED_HOME}/.ssh' " + f"(не /root/.ssh — иначе рассинхрон с HOME агента)" + ) + assert not target.startswith("/root/"), ( + f"{name}: SSH target указывает на чужой HOME (/root): {target}" + ) + + +# --- TC-04: HOME launcher'а совместим с SSH/claude-маунтами --------------------- +def test_tc04_launcher_home_matches_mounts(compose): + """TC-04: HOME, форсимый launcher'ом, совпадает с базой SSH/claude-маунтов. + + Нет рассинхрона HOME vs uid: и env Popen, и git_env, и target SSH-маунта + все указывают на /home/slin. + """ + source = LAUNCHER_PATH.read_text(encoding="utf-8") + # launcher форсит HOME в двух местах (env Popen и git_env). + occurrences = source.count(f'"HOME": "{EXPECTED_HOME}"') + assert occurrences >= 2, ( + f"launcher.py: ожидалось >=2 форсинга HOME={EXPECTED_HOME!r}, " + f"найдено {occurrences}" + ) + # И SSH-маунты обоих сервисов ведут в этот же HOME. + for name in PIPELINE_SERVICES: + target = _ssh_mount_target(_service(compose, name)) + assert target.startswith(f"{EXPECTED_HOME}/"), ( + f"{name}: SSH target {target} не под HOME агента {EXPECTED_HOME}" + )