--- work_item: ORCH-101 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-10 model_used: claude-opus-4-8 --- # 02 — ТЗ (TRZ): ORCH-101 — ORCH-10-common: расхардкод + секреты + smoke (фундамент тиража) Work Item: **ORCH-101** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **что** должно измениться и **где** (модули/контракты/артефакты), выведено из BRD и > фактического кода (аудит выполнен по репо на ветке задачи). **Как** (точные имена новых > конфиг-ключей, механика генератора секретов, форма smoke-скрипта) — решает архитектор в `06-adr/`. --- ## 1. Сводка изменения Три слоя одного фундамента тиража (эпик ORCH-10, оба типа A/B): 1. **Расхардкод:** все хост-специфичные значения, которые сегодня **обходят конфиг** (зашиты в код `src/**`, в `docker-compose.yml`, `Dockerfile`, `scripts/orchestrator-deploy-hook.sh`), переводятся на чтение из `Settings` (env `ORCH_*`) / compose-интерполяцию `${VAR:-default}` / `ARG` / env-override — **с дефолтами, равными текущим боевым значениям** (zero-regression). Нормативный реестр находок — §3.1 (результат аудита всего репо). 2. **Секреты:** механизм выпуска **нового** комплекта секретов на новом хосте (генерация webhook-секретов + чек-лист внешних токенов) + полнота `.env.example`. 3. **Smoke:** документированная воспроизводимая процедура «тестовый проект + задача → конвейер доехал» с PASS/FAIL-критериями (+ анти-регресс grep-тест против возврата хардкодов). Конвейер (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/verdict-ключи/схема БД) — не меняется. --- ## 2. Задействованные модули / пути | Путь | Действие | |------|----------| | `src/config.py` | изменить: новые `Settings`-ключи (HOME агент-процессов, git-идентичность акторов, внешний Gitea-URL уже есть — `gitea_public_url`; прочее по §3.1), дефолты = текущим значениям | | `src/plane_sync.py` | изменить: `notify_stage_change` — убрать литерал `gitea_base = "http://git.mva154.duckdns.org"` и owner `admin` из путей ссылок → `settings.gitea_public_url` (fallback `gitea_url`) + `settings.gitea_owner` | | `src/agents/launcher.py` | изменить: env агент-процесса (×2 места) — `HOME` и git author/committer (`claude-bot@mva154.local`) из конфига | | `src/self_deploy.py` | изменить: env detached-процесса — `HOME`, `deploy-finalizer@mva154.local` из конфига | | `src/post_deploy.py` | изменить: env монитора — `HOME`, `post-deploy-monitor@mva154.local` из конфига | | `src/image_freshness.py` | изменить ИЛИ обосновать: `_STAGING_PORT = 8501` (см. §3.4 А-1, инвариант ORCH-058) | | `src/qg/checks.py` | изменить ИЛИ обосновать: `SELF_HOSTING_REPO = "orchestrator"` (см. §3.4 А-2) | | `src/fs_normalize.py` | изменить: fallback-подсказку `sudo chown -R 1000:1000 /repos/_wt` строить из `settings` (uid/worktrees_dir) | | `src/projects.py` | не менять логику; `_DEFAULT_PROJECTS` остаётся документированным fallback (UUID'ы чужого Plane безвредны); фиксация в чек-листе «на новом хосте обязателен `ORCH_PROJECTS_JSON`» | | `watchdog/config.py` | проверить/синхронизировать: уже env-driven (`WATCHDOG_*`); дефолт `http://127.0.0.1:8500/metrics` согласовать с параметризацией прод-порта | | `docker-compose.yml` | изменить: интерполяция `${VAR:-default}` для всех хост-значений (§3.1 B) | | `Dockerfile` | изменить: `ARG` для uid/gid/username/home; CMD-порт — по решению архитектора (§3.4 А-3) | | `scripts/orchestrator-deploy-hook.sh` | изменить: `REPO=/home/slin/repos/orchestrator` → env-override `REPO="${REPO:-…}"` | | `scripts/` (новый) | создать: генератор секретов и/или smoke-обвязка — форма за архитектором (§3.2, §3.3) | | `.env.example` | изменить: новые ключи, секции секретов с плейсхолдерами, чек-лист «что заполнить» | | `.env.staging.example` | изменить: согласовать с новыми ключами (при необходимости) | | `tests/test_no_host_hardcodes.py` (новый) | создать: структурный анти-регресс grep-тест (FR-8) | | `tests/` (новые) | создать: тесты параметризации/секретов/smoke по `04-test-plan.yaml` | | `docs/operations/` | создать deployment-раздел тиража (предложение: `REPLICATION.md`; имя финализирует архитектор) + обновить карту env в `INFRA.md` | | `CHANGELOG.md`, `CLAUDE.md`, `README.md` | изменить: запись изменения; паспорт/обзорные доки — по фактическому объёму (правило агентов №2/№6) | --- ## 3. Функциональные требования ### FR-1 — Нормативный реестр хардкодов (результат аудита) и их устранение Привязка: BR-1, BR-2. Аудит выполнен по всему репо (grep по классам: IP/hostname, пути, порты, gid/uid, URL, идентичности). Реестр ниже **нормативен**: developer закрывает каждую строку; расхождение «нашёл ещё» — дополняет реестр в PR, а не игнорирует. #### 3.1. Реестр **A. `src/**` / `watchdog/**` — код, обходящий конфиг (блокеры AC-1):** | # | Файл:строка | Хардкод | Требование | |---|---|---|---| | A1 | `src/plane_sync.py:1064` | `gitea_base = "http://git.mva154.duckdns.org"` в `notify_stage_change` + owner `admin` в построении ссылок Branch/PR | читать `settings.gitea_public_url` (fallback `settings.gitea_url`; loopback-семантика — как в `notifications._build_plane_issue_link`) + `settings.gitea_owner`; никаких новых ключей не требуется | | A2 | `src/agents/launcher.py:594, 825` | `"HOME": "/home/slin"`; `GIT_AUTHOR/COMMITTER_EMAIL: claude-bot@mva154.local` (×2 места) | HOME и git-идентичность из конфига (новые ключи, дефолты = текущим значениям) | | A3 | `src/self_deploy.py:332–336` | `"HOME": "/home/slin"`; `deploy-finalizer@mva154.local` | то же (единый источник с A2; имя актора может остаться per-actor) | | A4 | `src/post_deploy.py:575–579` | `"HOME": "/home/slin"`; `post-deploy-monitor@mva154.local` | то же | | A5 | `src/image_freshness.py:61` | `_STAGING_PORT = 8501` (модульная константа; намеренный анти-prod-инвариант ORCH-058 AC-9) | конфигуризовать с дефолтом 8501 **с сохранением инварианта** (guard «staging-порт ≠ прод-порт») ИЛИ оставить константой с явным обоснованием в ADR — решение архитектора (§3.4 А-1) | | A6 | `src/qg/checks.py:517` | `SELF_HOSTING_REPO = "orchestrator"` (узел: на него опираются все `*_repos`-leaf'ы «empty CSV → self-hosting only») | конфиг-ключ с дефолтом `orchestrator` ИЛИ нормативная платформенная константа («репо платформы в тираже обязан называться `orchestrator`», фиксируется в deployment-доке) — решение архитектора (§3.4 А-2) | | A7 | `src/fs_normalize.py:539` | fallback-строка подсказки `sudo chown -R 1000:1000 /repos/_wt` | строить из `settings.fs_target_uid` / `settings.worktrees_dir` (соседние строки 533–535 уже так делают) | | A8 | `src/disk_watchdog.py:95` | fallback `["/repos", "/app/data"]` — зеркало дефолта `settings.disk_monitor_paths` | допустимое config-backed зеркало; не блокер. Требование: значения остаются синхронными с конфиг-дефолтом (одна точка истины желательна — на усмотрение архитектора) | | A9 | `src/projects.py:42–55` | `_DEFAULT_PROJECTS`: UUID'ы Plane текущего хоста | НЕ блокер (документированный fallback «out of the box», чужие UUID не сматчатся). Требование: чек-лист тиража обязывает задать `ORCH_PROJECTS_JSON` | | A10 | `watchdog/config.py:100,144` | дефолт `http://127.0.0.1:8500/metrics` | уже env-driven (`WATCHDOG_METRICS_URL`); дефолт легитимен; согласовать с А-3 (прод-порт), если тот станет параметром | > Паттерн `getattr(settings, "x", <литерал>)` (A7/A8 и др.) — **config-backed fallback**, не > хардкод-блокер: значение управляется конфигом. Блокер — только код, который конфиг **обходит**. **B. `docker-compose.yml` — хост-специфика (блокеры AC-6):** | # | Место | Хардкод | |---|---|---| | B1 | volumes ×3 сервисов | `/home/slin/repos:/repos`(+`:ro`), `/home/slin/.claude`, `/home/slin/.claude.json`, `/home/slin/.orchestrator-ssh:/home/slin/.ssh` | | B2 | volumes ×2 сервисов | `/usr/lib/node_modules/@anthropic-ai/claude-code:/opt/claude-code:ro`, `/usr/bin/node:/usr/bin/node:ro` | | B3 | `group_add` ×3 | `"999"` (gid docker-группы хоста) | | B4 | `user:` ×2 | `"1000:1000"` (uid:gid хоста) | | B5 | environment | `ORCH_HOST_REPOS_DIR=/home/slin/repos` ×2, `ORCH_DEPLOY_HOST_REPO_PATH=/home/slin/repos/orchestrator`, `DEPLOY_SSH_USER=slin` ×2, `ORCH_DEPLOY_SSH_USER=slin`, `DEPLOY_HOOK_SCRIPT=/home/slin/bin/enduro-deploy-hook.sh` ×2 (legacy enduro) | | B6 | `orchestrator-staging.command` | порт `8501` | Требование: compose-интерполяция `${VAR:-default}` (источник — `.env`, штатный механизм Compose); `docker compose config` без заданных переменных резолвится в текущие значения 1:1. Целевая семантика «HOME внутри контейнера согласован с маунтами `.claude`/`.ssh` и `useradd` Dockerfile» (инвариант ORCH-040) сохраняется как согласованная группа переменных. **C. `Dockerfile`:** | # | Место | Хардкод | |---|---|---| | C1 | `useradd -u 1000 -g 1000 -m -d /home/slin -s /bin/bash slin` | uid/gid/home/username | | C2 | `CMD … --port 8500` | прод-порт | Требование: C1 → `ARG` с дефолтами (1000/1000//home/slin/slin). C2 — по решению архитектора (§3.4 А-3): порт уже переопределяем через compose `command:`; допустимо оставить CMD-дефолт 8500. **D. `scripts/`:** | # | Место | Хардкод | |---|---|---| | D1 | `orchestrator-deploy-hook.sh:38` | `REPO=/home/slin/repos/orchestrator` (единственный непараметризованный путь скрипта; остальное уже env-override) | Требование: `REPO="${REPO:-/home/slin/repos/orchestrator}"` (паттерн остальных переменных скрипта). `scripts/staging_check.py` — уже env-driven (`ORCH_PLANE_API_URL`/`ORCH_GITEA_URL`/`--base-url`), изменений не требует. **E. Уже параметризовано (НЕ трогать, фиксация аудита):** дефолты `src/config.py` (`plane_api_url:8091`, `gitea_url:3000`, `repos_dir`, `host_repos_dir`, `worktrees_dir`, `runs_dir`, `db_path`, `deploy_ssh_user`, `deploy_host_repo_path`, `deploy_prod_target_port:8500`, `post_deploy_base_url`, `disk_monitor_paths`, `claude_bin`) — легитимные значения по умолчанию, управляемые env; менять их значения запрещено (BR-5). ### FR-2 — Чтение хост-значений из конфига в `src/**` Привязка: BR-1. Каждый блокер класса A (A1–A4, A7; A5/A6 — по исходу §3.4) читает значение через `settings` (существующие либо новые ключи `Settings` с env `ORCH_*`). Требования к новым ключам: - дефолт = текущее боевое значение (BR-5): `/home/slin`, `claude-bot`, `*@mva154.local`-адреса; - описательный комментарий в `config.py` по образцу существующих блоков (назначение + env-имя); - ключи отражены в `.env.example` и карте env `INFRA.md`; - точные имена ключей и группировка (например, единый `agent_home_dir` + per-actor идентичности или общий email-домен) — решение архитектора; ТЗ фиксирует контракт: **ни один из пяти акторов (агенты CLI ×2 места, self-deploy finalizer, post-deploy monitor, fs-подсказка) не содержит литералов `/home/slin` / `mva154` после изменения**. ### FR-3 — Параметризация инфра-файлов Привязка: BR-2. Реестр B/C/D закрыт; механика — compose-интерполяция / `ARG` / shell-default. Инварианты: ORCH-040 (uid 1000 + `group_add` docker-gid + HOME-согласованность маунтов — группа переменных меняется согласованно, «МИНА 1» `group_add` не удаляется), ORCH-058 (staging-сервис никогда не резолвится в прод-таргет). Поведение `docker compose config` при пустом окружении — эквивалент текущего файла (проверяется тестом TC-06). ### FR-4 — Механизм секретов нового хоста Привязка: BR-3. Состав: 1. **Инвентаризация** (фиксация аудита): генерируемые локально — `ORCH_PLANE_WEBHOOK_SECRET`, `ORCH_GITEA_WEBHOOK_SECRET`; выпускаемые внешними системами — `ORCH_PLANE_API_TOKEN`, `ORCH_PLANE_BOT_*` (7 шт., опциональны: fallback на `ORCH_PLANE_API_TOKEN`), `ORCH_GITEA_TOKEN`, `ORCH_TELEGRAM_BOT_TOKEN` (+ несекретный `ORCH_TELEGRAM_CHAT_ID`). 2. **Генерация:** webhook-секреты создаются криптослучайно (стандарт: `secrets`-модуль Python / эквивалент, длина ≥ 32 байт энтропии) на целевом хосте; форма (отдельный `scripts/gen_secrets.py`, режим onboarding-CLI или документированная команда) — решение архитектора. Требования к поведению: повторный запуск даёт другие значения; существующий `.env` никогда не перезаписывается молча (NFR-3); вывод согласован с ключами `.env.example`. 3. **Чек-лист** в deployment-доке: для каждого секрета — где выпустить (Plane UI/API, Gitea UI, BotFather), куда вписать, как проверить. Явное правило: «боевые секреты текущего хоста не копируются». 4. **Полнота `.env.example`:** каждый обязательный для старта ключ присутствует (с плейсхолдером или дефолтом), включая новые ключи FR-2/FR-3; секретные значения — только плейсхолдеры. ### FR-5 — Smoke-верификация тиража Привязка: BR-4. Документированная процедура (deployment-раздел) + опциональная скрипт-обвязка (форма — архитектор; кандидаты-кирпичи уже в репо: `scripts/onboard_project.py` `plan/apply/verify`, `scripts/staging_check.py`, `GET /health` / `/queue` / `/metrics`): 1. **Шаги:** поднять инстанс (env+секреты заполнены) → `GET /health` ок → завести тестовый проект (onboarding-CLI `verify` или sandbox-проект) → создать тестовую задачу → убедиться, что конвейер «доехал» (задача продвигается по стадиям; минимальный обязательный сигнал — стадия `analysis` отработала и артефакты `01–04` созданы; полный прогон до `done` — расширенный режим). 2. **Критерии:** каждый шаг имеет явный PASS/FAIL; итог процедуры — однозначный вердикт. 3. **Воспроизводимость:** процедура прогоняется на текущей инфре (staging-песочница 8501 + sandbox-проект) без нового железа — это и приёмочная проверка AC-3. 4. **Stateless:** процедура нигде не предполагает перенос данных/БД с боевого хоста. ### FR-6 — Анти-регресс структурный тест Привязка: BR-6. Новый `tests/test_no_host_hardcodes.py` (образец — `tests/test_agent_prompts_canon.py`): - сканирует `src/**/*.py` и `watchdog/**/*.py` на запрещённые литералы: `82.22.50.71`, `/home/slin`, `mva154`, `duckdns` (список централизован в тесте; расширяем); - судит **код**: строки-комментарии и докстринги исключаются (NFR-5; механика исключения — прагматичная построчная/AST — за developer); - `tests/**`, `docs/**`, `.env.example` — вне зоны сканирования; - допускает точечный явный allowlist (файл:литерал) с комментарием-обоснованием — пустой на момент сдачи задачи (все блокеры A закрыты). ### FR-7 — Документация Привязка: BR-7. Deployment-раздел в `docs/operations/` (предложение: `REPLICATION.md`): карта новых env-переменных (включая compose-интерполяцию), процедура секретов (FR-4), smoke-процедура (FR-5), границы «10-common vs Lite vs Bundled». Обновления: `INFRA.md` (карта env), `CHANGELOG.md`; `CLAUDE.md`/`README.md` — по фактическому объёму изменений (правило №2/№6). --- ### 3.4. Вопросы архитектору (решаются в `06-adr/`, не в ТЗ) - **А-1:** `_STAGING_PORT = 8501` (`image_freshness`) — конфиг-ключ с guard'ом «≠ прод-порт» или нормативная константа? Инвариант ORCH-058 AC-9 («никогда не прод 8500») обязан сохраниться. - **А-2:** `SELF_HOSTING_REPO = "orchestrator"` — конфиг-ключ или платформенная конвенция тиража (репо платформы всегда `orchestrator`)? На него завязаны все `*_repos`-leaf'ы. - **А-3:** CMD-порт Dockerfile (8500) — `ARG` или остаётся (порт уже переопределяем compose `command:`)? - **А-4:** форма генератора секретов (отдельный скрипт / режим `onboard_project.py` / документированная команда) и форма smoke (чистый runbook / runbook + скрипт). - **А-5:** группировка новых конфиг-ключей FR-2 (единый HOME-ключ + per-actor email'ы vs общий email-домен). --- ## 4. Изменения API Нет. Существующие эндпоинты (`/health`, `/queue`, `/metrics`) переиспользуются smoke-процедурой read-only; новые эндпоинты не вводятся. ## 5. Изменения схемы БД Нет. ## 6. Требования к новым/изменённым QG checks Нет. `QG_CHECKS`/`check_*`/machine-verdict ключи не меняются. Новый структурный тест (FR-6) — обычный pytest: попадает в существующие гейты `check_ci_green`/`check_tests_passed`/merge-gate re-test автоматически, без регистрации нового QG. ## 7. Совместимость / регресс - **Zero-regression (BR-5):** все новые параметры — с дефолтами, равными сегодняшним боевым значениям; пустой/неизменённый `.env` ⇒ байт-в-байт текущее поведение; `docker compose config` без переменных ⇒ эквивалент текущего файла. Полный `pytest tests/ -q` зелёный. - **Обратимость (NFR-2):** откат = не задавать новые переменные. Функциональный kill-switch не требуется (дефолты и есть прежнее поведение); вводить новый флаг ради флага запрещено. - **Область раската:** изменения инертны для enduro-trails (значения те же); compose/Dockerfile вступают в силу только при следующем штатном деплое через конвейер (staging 8501 → ручной Confirm Deploy) — прод-контейнер в рамках задачи не рестартуется (NFR-1). - **Инварианты соседних маркеров (правило №9):** ORCH-040 (uid/gid/HOME/«МИНА 1»), ORCH-058 (freshness → только staging), ORCH-036/059 (self-deploy фазы, Confirm Deploy), INV-4 (мерж только через PR-merge API) — сохраняются; соответствующие ADR прочитаны при правке помеченных блоков.