---
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 прочитаны при правке помеченных блоков.
