architect(ET): auto-commit from architect run_id=654
All checks were successful
CI / test (push) Successful in 56s

This commit is contained in:
2026-06-12 10:42:03 +03:00
parent be90632068
commit a681d6e3f7
4 changed files with 526 additions and 0 deletions

View File

@@ -210,6 +210,29 @@ sidecar читает только `.env.watchdog`; C-1 ORCH-100 — отдель
(docs+tests). Подробнее: [adr-0037](adr/adr-0037-lite-replication-canon.md), детально —
`docs/work-items/ORCH-102/06-adr/ADR-001-lite-setup-doc-canon.md`.
**Установщик Lite (ORCH-104 — design).** Поверх ручного канона ORCH-102 вводится интерактивный
**установщик `scripts/install_lite.py`** — «connect-only»-сородич `bootstrap_bundle.py` (тот
*поднимает* Plane/Gitea, этот *подключается* к уже существующим заказчика), автоматизирующий
happy-path `LITE_SETUP.md` §2§11. **Самодостаточный stdlib-only один файл** (примитивы эталона
реплицированы, не вынесены — `bootstrap_bundle.py` байт-в-байт; вынос общего модуля — по
rule-of-three): step-движок `check→ensure`, режимы `plan`/`apply`/`verify`, exit `0/2/1`, honest
`manual_checkpoint`. Скан предусловий хоста + управляемая установка зависимостей (точная команда
под `apt`/`dnf`, выполнение только с явным согласием при TTY; тихого root-инсталла нет, D-1);
best-effort детект существующих Plane/Gitea (`docker ps` + порт-пробы, ранжир unauth-liveness) с
ручным фолбэком; интерактивный сбор токенов/URL с **живой верификацией ДО записи** (`getpass`,
Plane `/projects/`, Gitea `/user`, Telegram `getMe`). **Каноны не форкаются:** webhook-секреты —
`gen_secrets.py`, регистрация проекта/22 статуса — `onboard_project.py`, env — рендер из
`.env.example`/`.env.watchdog.example`, стек — `docker-compose.yml`. **Граница connect-only
(нормативно):** webhook Plane — верифицируемый **manual-step** (печать инструкции пути A/Б, без
исполнения raw-SQL в чужую БД — INV-5; истинный гейт — smoke §11); логин claude CLI — manual-step
с верификацией. Гигиена секретов: `getpass`, права 600, без молчаливой перезаписи (NFR-2). Не-интерактив/CI
— fail-closed `exit 2` с именем недостающего ключа; секреты из env, не из argv. Анти-дрейф —
новый `tests/test_install_lite_script.py` (структурный + юнит + фейки) + ассерт ссылки на
установщик в `test_lite_setup_doc.py` (13 разделов сохранены). Рантайм/конвейер — байт-в-байт
(scripts+docs+tests); kill-switch не нужен (активация — явный запуск оператором). Подробнее:
[adr-0040](adr/adr-0040-lite-installer-canon.md), детально —
`docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md`.
**Type B — Bundled (ORCH-103).** Закрывает эпик ORCH-10: весь стек одним комплектом
(орк + watchdog + Gitea + Plane CE ≈1314 контейнеров) для заказчика без собственной
инфраструктуры. Состав Plane — зеркало официального selfhost-référence v0.23.1

View File

@@ -0,0 +1,106 @@
---
work_item: ORCH-104
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# adr-0040: Установщик Lite-тиража — `scripts/install_lite.py` (ORCH-104, 10a)
## Статус
Proposed
## Контекст
Эпик ORCH-10 (D5 «Масштаб»), тип **A — Lite**. adr-0037 (ORCH-102) дал golden-source инструкцию
`docs/deployment/LITE_SETUP.md` — надёжный, но **ручной** маршрут из 13 разделов («голый хост →
работающий конвейер»): ~6 предусловий, ~20 ключей `.env` по 4 группам, webhook'и Plane/Gitea
(иногда raw-SQL), два Telegram-бота, claude CLI, compose, онбординг, smoke. Долго; ошибки
(опечатка секрета → 401 HMAC; неверный uid; занятый порт) всплывают поздно — на `docker compose
up`, не в момент ввода. adr-0038 (ORCH-103) для **Bundled** уже доказал каркас установщика
`scripts/bootstrap_bundle.py`.
ORCH-104 автоматизирует happy-path §2§11 одним интерактивным установщиком и добавляет раннюю
валидацию. Сквозной характер: вводится новый **операторский артефакт семьи тиража** и нормативная
**граница connect-only** (Lite *подключается* к чужой инфре, в отличие от Bundled, который её
*поднимает*) — это правило обязательно для будущих задач эпика ORCH-10. Детальный пакет решений
(D1…D15, исходы вопросов ТЗ OQ-1…OQ-7) — work-item ADR:
`docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md`.
## Решение
1. **Новый установщик `scripts/install_lite.py`** — «connect-only»-сородич `bootstrap_bundle.py`.
Имя `install_*` (не `bootstrap_*`) понятнее оператору и сигналит семантику «подключение к
готовой инфре» против «bring-up» bundle (OQ-1).
2. **Самодостаточный один файл, stdlib-only** (OQ-4): проверенные примитивы эталона
(`parse_env`/`render_env`/`manual_checkpoint`/`_write_private` 600/never-raise `_http`/
`preflight_verdict`-стиль/exit-контракт `0/2/1`) **реплицированы**, а не вынесены в общий
модуль. Так держится «один установочный файл» (BR-1) и `bootstrap_bundle.py` остаётся
байт-в-байт (нулевой риск регресса Bundled). Вынос общего `scripts/_replication_lib.py`
позже, по rule-of-three (третий тиражный скрипт).
3. **Каноны не форкаются** (как adr-0037/0038): webhook-секреты — субпроцесс `gen_secrets.py`
(никакого своего `token_hex`); регистрация проекта и 22 статуса — субпроцесс
`onboard_project.py apply/verify` (`plane_sync._PLANE_NAME_TO_KEY`); `.env`/`.env.watchdog`
рендер из `.env.example`/`.env.watchdog.example`; стек — `docker-compose.yml`
(ровно орк+watchdog, staging за профилем не поднимается).
4. **Граница connect-only — нормативно (OQ-5):** в Lite Plane/Gitea/Telegram — **чужие**
инсталляции; установщик говорит с ними только по их API и **не мутирует** их. Webhook Plane =
верифицируемый **manual-step** (печать инструкции пути A/Б, без исполнения raw-SQL в чужую БД —
INV-5); истинный гейт webhook'а — smoke (§11). Контраст с bundle, который владеет Plane-БД и
пробует INSERT.
5. **Управляемая установка зависимостей без тихого root (OQ-3):** точная команда под дистрибутив
(`apt`/`dnf`), выполнение только при TTY + явном согласии + команда из allowlist; неизвестный
дистрибутив/нет TTY/отказ → инструкция + `exit 2`. Логин claude CLI — manual-step с верификацией
(OQ-6). Не-интерактив/CI — fail-closed `exit 2` с именем недостающего ключа; секреты из env, не
из argv (OQ-7).
6. **Анти-дрейф — постоянная CI-гарантия:** новый `tests/test_install_lite_script.py` (структурный +
юнит чистых функций + фейки HTTP/процессов; без сети/docker/LLM): ссылки на кирпичи, запрет
собственной генерации секретов и захардкоженных 22 статусов, stdlib-only (AST), 0
delete/force-операций, реюз `FORBIDDEN`/`find_violations` из `tests/test_no_host_hardcodes.py`,
exit-контракт `{0,1,2}`. `LITE_SETUP.md` получает callout-указатель на установщик
(рекомендованный путь; ручной маршрут — фолбэк), **13 `## N.`-разделов сохранены**
`test_lite_setup_doc.py` остаётся зелёным + ассертит ссылку. Норматив сопровождения (NFR-5):
меняешь шаги установки → синхронизируй `LITE_SETUP.md` в том же PR.
### Что НЕ меняется
`src/**`, `docker-compose.yml`, `Dockerfile`, существующие `scripts/**` (включая
`bootstrap_bundle.py`/`gen_secrets.py`/`onboard_project.py`); `STAGE_TRANSITIONS`, состав
`QG_CHECKS`, семантика `check_*`, machine-verdict ключи, схема БД — байт-в-байт. Новый QG не
вводится (структурные тесты — в существующих гейтах). Kill-switch не нужен: активация — только явный
запуск оператором на его хосте (паттерн ORCH-009/101/102/103). Прод-контейнер в рамках задачи не
рестартуется. `07-infra-requirements.md`/`08-data-requirements.md` — N/A (топология нашего орка и
схема БД не меняются; предусловия хоста — golden source LITE_SETUP §2, не форкаются).
## Альтернативы
- **Общий модуль `_replication_lib.py` (DRY сейчас)** — отвергнуто: правка замороженного
`bootstrap_bundle.py` = риск регресса Bundled + противоречит «одному файлу»; вынос по rule-of-three.
- **Имя `bootstrap_lite.py`** — отвергнуто: `install_*` понятнее оператору и верно сигналит
connect-only.
- **Авто-драйв webhook Plane raw-SQL (как bundle)** — отвергнуто: в Lite Plane чужой; raw-SQL в
чужую БД нарушает INV-5 и часто недостижим.
- **Тихий sudo-install / сторонний TUI (rich/click)** — отвергнуто: D-1 (только consent) и NFR-6
(stdlib-only до первого `docker compose up`).
- **Замена `LITE_SETUP.md` установщиком** — отвергнуто: runbook остаётся golden source и фолбэком.
## Последствия
- Type A эпика ORCH-10 получает автоматизированный happy-path поверх ручного канона; ошибки
секретов/URL ловятся в момент ввода (живая верификация), не на `up`. Полнота/анти-форк/отсутствие
delete защищены CI.
- Цена: ~150 строк осознанного дублирования примитивов с `bootstrap_bundle.py` (rule-of-three
триггер задокументирован); webhook Plane остаётся ручным (граница connect-only), верифицируется
на smoke.
- Откат: удалить `scripts/install_lite.py` + `tests/test_install_lite_script.py`, вернуть
callout-врезку `LITE_SETUP.md` и doc-правки — состояние 1:1 (scripts+docs+tests, без миграций и
рантайм-изменений).
## Связи
adr-0037 (ORCH-102 — Lite-канон/`LITE_SETUP.md`; этот установщик автоматизирует его §2§11),
adr-0038 (ORCH-103 — `bootstrap_bundle.py`, эталон каркаса; connect-only vs bring-up),
adr-0036 (ORCH-101 — фундамент 10-common: `gen_secrets.py`, `.env.example`, расхардкод хоста),
adr-0035 (ORCH-009 — `onboard_project.py`/22 статуса, переиспользуются субпроцессом; D10 — запрет
branch protection), adr-0033 (ORCH-100 — watchdog, C-1 отдельный бот в `.env.watchdog`),
adr-0008/INV-4 (ORCH-058 — staging-порт guard; merge-актор — основание connect-only-границы).
Детально — `docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md`,
`docs/work-items/ORCH-104/10-tech-risks.md`.

View File

@@ -0,0 +1,354 @@
---
work_item: ORCH-104
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# ADR-001: Установщик Lite-тиража `scripts/install_lite.py` — структура, границы, переиспользование канонов
Work Item: **ORCH-104** — Установочный скрипт для Lite (Type A эпика ORCH-10, поверх ORCH-102)
Стадия: **architecture**
Сквозная регистрация: **`docs/architecture/adr/adr-0040-lite-installer-canon.md`** (решение
кросс-каттинговое — вводит новый операторский артефакт в семью тиража и нормативную границу
«connect-only vs bring-up»; продолжает серию adr-0036 → 0037 → 0038).
## Статус
Proposed
## Контекст
ORCH-102 дал золотой источник Lite-тиража — ручной runbook `docs/deployment/LITE_SETUP.md` из 13
разделов («голый хост → работающий конвейер»). Это надёжный, но **ручной** маршрут: оператор сам
проверяет ~6 предусловий хоста, копирует `.env.example`, гоняет `gen_secrets.py`, заполняет ~20
ключей `.env` по 4 группам, настраивает webhook Plane (иногда прямым SQL в Postgres, §5.4),
выпускает токен/webhook Gitea, ставит claude CLI, заводит **два** Telegram-бота, поднимает compose,
регистрирует проект `onboard_project.py`, гоняет smoke. Долго; легко ошибиться; ошибка всплывает
поздно (на `docker compose up`, а не в момент ввода).
ORCH-104 автоматизирует **happy-path §2§11** одним интерактивным установщиком и добавляет
**раннюю валидацию** (живая проверка каждого секрета/URL ДО записи). Это **scripts + docs + tests**
изменение: рантайм/конвейер (`src/**`, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, machine-verdict
ключи, схема БД) — **не трогаются** (NFR-1/INV-1). Каноны не форкаются: секреты —
`gen_secrets.py`, регистрация проекта — `onboard_project.py`, env — из `.env.example` /
`.env.watchdog.example`, стек — из `docker-compose.yml`.
**Сверено по коду/репо (факты, на которых стоит решение):**
- `scripts/bootstrap_bundle.py` (ORCH-103, ~970 строк) — ближайший прецедент: step-движок
`check→ensure`, режимы `plan`/`apply`/`verify`, exit `0/2/1`, чистые `parse_env`/`render_env`/
`preflight_verdict`/`collect_facts`, honest `manual_checkpoint` (TTY-aware, verify-loop),
`_write_private` (chmod 600, маскировка), `_http` (never-raise GET), stdlib-only, **0
delete-операций**. Subprocess-кирпичи `gen_secrets.py --write <tmp>` и `onboard_project.py
apply/verify --json` (парс `instructions[]` на префикс `ORCH_PROJECTS_JSON=`).
- `scripts/gen_secrets.py` (ORCH-101) — единственный легитимный источник webhook-секретов
(`secrets.token_hex(32)`); `--write [PATH]` отказывает на существующем файле (exit 2), перезапись
только `--force`; stdlib-only.
- `scripts/onboard_project.py` (ORCH-009) — `plan`/`apply`/`verify` (`choices`), exit `0/1/2`
(`Report.exit_code`), `--json` → отчёт с `instructions[]`, обязательные
`--name/--repo/--prefix/--stack/--test-cmd/--prod-port/--staging-port/--webhook-url`. **22 статуса
с точными именами** — только этот кирпич (`plane_sync._PLANE_NAME_TO_KEY`).
- `tests/test_no_host_hardcodes.py` экспортирует `FORBIDDEN` + `find_violations(source, forbidden)`
(tokenize-исключение комментариев/докстрингов) — переиспользуемы по импорту (single source of
truth; так уже делает `test_lite_setup_doc.py`).
- `tests/test_lite_setup_doc.py` — структурный анти-дрейф: `SECTIONS` (13 `## N.`-заголовков **в
порядке**), список обязательных кирпичей, key-sync `.env.watchdog.example`, сверка «22 статуса»
импортом `plane_sync._PLANE_NAME_TO_KEY`, fenced-скан `FORBIDDEN` + секрет-эвристика.
- `LITE_SETUP.md` §5.4 — webhook Plane CE **не в `/api/v1`**: путь A (UI) / путь Б (raw SQL
`INSERT INTO webhooks … docker exec plane-db psql`). §7.2 — claude CLI: интерактивный логин
Anthropic. §6.4 — branch protection на `main` **не включать** (INV-4, ложные HOLD merge-актора).
ТЗ (`02-trz.md` §12) оставило архитектору 7 открытых вопросов (OQ-1…OQ-7). Они разрешены ниже как
решения D1…D11; D12…D15 — структурные решения (анти-дрейф, doc-sync, границы scope).
## Решение
### Сводка
Новый **самодостаточный** stdlib-only установщик `scripts/install_lite.py` — «connect-only»-сородич
`bootstrap_bundle.py`: тот *поднимает* Plane/Gitea, этот *подключается* к уже существующим. Тот же
проверенный каркас (step-движок `check→ensure`, режимы `plan`/`apply`/`verify`, exit `0/2/1`,
honest `manual_checkpoint`, `_write_private` 600, never-raise `_http`), реплицированный в один файл
ради «одного установочного файла» (BR-1) и нулевого риска регресса Bundled-пути. Каноны
(`gen_secrets.py`, `onboard_project.py`, `.env.example`, `docker-compose.yml`) переиспользуются
субпроцессами/рендером, не форкаются. Ключевая граница Lite: установщик говорит **только** с
локальным хостом и собственными Plane/Gitea/Telegram заказчика по их API — он **не** мутирует
чужую инфраструктуру (webhook Plane = верифицируемый manual-step, не raw-SQL).
### D1 — Имя и идентичность: `scripts/install_lite.py` (OQ-1)
Файл — `scripts/install_lite.py` (рекомендация анализа принята). `install_*` понятнее конечному
**оператору**, чем `bootstrap_*`, и точнее отражает суть: Lite **устанавливает/подключает** орк к
готовой инфре, тогда как `bootstrap_bundle.py` **бутстрапит** весь стек (включая Plane/Gitea).
Несимметрия имён — намеренный сигнал разной семантики (D9). Привязка: BR-1, AC-1.
### D2 — Самодостаточный один файл, не общий модуль (OQ-4)
`install_lite.py` **реплицирует** небольшие проверенные примитивы `bootstrap_bundle.py`
(`parse_env`/`render_env`/`manual_checkpoint`/`_http`/`_write_private`/`collect_facts`-стиль/
`preflight_verdict`-стиль/exit-контракт) **внутри себя**, а не импортирует общий модуль.
**Почему так, а не DRY-модуль:**
- **«Один установочный файл» (BR-1)** — оператор копирует/запускает один артефакт; скрытый граф
импортов противоречит UX-цели.
- **`bootstrap_bundle.py` остаётся байт-в-байт (TRZ §2).** Вынос общего кода потребовал бы
*править* существующий кирпич → перегон его анти-дрейф-тестов (`test_bootstrap_script.py`) и
риск регресса **Bundled**-пути. Это scope-creep и риск ради чужой задачи.
- **stdlib-only (NFR-6/INV-7)** соблюдается тривиально в обоих вариантах — DRY его не решает.
- Цена — ~150 строк дублирования примитивов; они малы, генеричны и стабильны (низкий риск дрейфа,
т.к. два скрипта автоматизируют **разные** runbook'и).
- **Триггер будущего выноса (rule of three):** появится **третий** тиражный скрипт с теми же
примитивами → вот тогда выносить `scripts/_replication_lib.py` отдельной задачей (с правкой обоих
существующих под её анти-дрейф). До тех пор — дублирование осознанное и документированное.
Привязка: BR-6, NFR-6, INV-2/INV-7, AC-7/AC-10.
### D3 — Step-движок и режимы: канон `bootstrap_bundle.py` (FR-1)
Каркас 1:1 с эталоном:
- **Режимы** (argparse, позиционный `mode`, `choices=("plan","apply","verify")`, дефолт `plan`):
`plan` — ноль мутаций (печать плана + read-only preflight); `apply` — полный прогон step-движком;
`verify` — read-only пост-проверка (health-контракты + `onboard_project.py verify`).
- **Exit-контракт:** `0` успех; `2` — manual-step / незавершённое предусловие / нет TTY; `1`
ошибка. Никаких иных кодов (AC-1 FAIL).
- **Step-движок `check→ensure`:** упорядоченный `APPLY_STEPS = ((name, fn), …)`, каждая
`fn(ctx) -> str` возвращает `"ok"`/`"skipped"`/`"manual-step"` либо бросает `ManualStop`(→2)/
`InstallError`(→1) (ловятся в `main`). Идемпотентность — встроенная inline-проверка «уже
сделано» в каждом шаге (повтор = каскад `skipped`, «resume» = повторный запуск). Результаты —
`ctx["results"]`, печатаются в итоговой сводке.
- **Шаги Lite (порядок):** `preflight` (D4) → `detect_infra` (D5) → `collect_verify` (D6) →
`secrets` (D7) → `env` (D7) → `up` (D8) → `onboard` (D8) → `plane_webhook` (D9, manual) →
`health` (D12).
Привязка: BR-1, BR-7, FR-1, AC-1, AC-8.
### D4 — Preflight-скан и управляемая установка зависимостей (FR-2/FR-3, OQ-3)
**Скан (FR-2):** read-only `collect_facts` (образец эталона) — наличие `docker` / `docker compose`
v2 / `git` / `python3` / `node` / `claude` (+ читаемость кред); свободность прод-порта
`ORCH_DEPLOY_PROD_TARGET_PORT` (дефолт 8500; при self-hosting-вилке staging
`ORCH_STAGING_PORT`=8501); `uid`/`gid`/`docker-gid` и владелец каталога репозиториев; наличие
ssh-каталога. Чистая `preflight_verdict(facts) -> (blockers, warnings)` + человекочитаемый список
«есть/нет». `plan`/preflight печатает его и при блокерах отдаёт `exit 2`.
**Управляемая установка (FR-3, D-1 владельца):** в Lite реалистично **все** недостающие
зависимости (docker, node, `@anthropic-ai/claude-code` через npm, git) — системного/root-уровня.
Поэтому **тира «тихого авто-инсталла» нет вообще** — это самый безопасный разрез D-1:
- чистая `install_command(distro, dep) -> str | None` (карта: debian/ubuntu→`apt`, rhel/fedora/
centos→`dnf`; дистрибутив — из `/etc/os-release` `ID`/`ID_LIKE` + `shutil.which`) печатает
**точную команду**;
- выполнение — **только** при (a) наличии TTY, (b) явном `y/N`=yes, (c) команда из фиксированного
allowlist (никакого произвольного exec);
- отказ / нет TTY / неизвестный дистрибутив (`install_command``None`) → печать инструкции +
`exit 2`. **Ни одной молчаливой root-мутации** (AC-3 FAIL).
Привязка: BR-2/BR-3, FR-2/FR-3, AC-2/AC-3, NFR-8.
### D5 — Детект существующих Plane/Gitea + выбор (FR-4, OQ-2)
Best-effort обнаружение (never-raise, NFR-3); его роль — лишь **пред-заполнить догадку**,
авторитет — живая верификация D6 перед записью (митигейшн R-1).
- **Кандидаты из двух источников:** (1) `docker ps` (имена/образы) — Plane: образ/имя
`makeplane/plane-*`, `plane-{web,space,admin,api,proxy}`, `proxy`; Gitea: образ `gitea/gitea`,
имя `gitea*`. (2) Слушающие localhost-порты (stdlib socket): Plane — 80/443/8080; Gitea — 3000.
- **Проба живости (ранжир уверенности, без токена):** Plane — `GET <base>/api/instances/`
(unauth-liveness Plane CE); Gitea — `GET <base>/api/v1/version`. Кандидат с успешной пробой
ранжируется выше; дедуп по нормализованному base-URL.
- **Чистая `rank_candidates(docker_facts, port_probes) -> [Candidate(url, source, confidence)]`**
(юнит-тестируема без сети).
- **Поведение по числу (FR-4):** 0 → запрос ручного URL (фолбэк); 1 → предложить с подтверждением;
≥2 → **нумерованный список** + выбор (`input()` индекс), вне диапазона → ручной ввод. Выбор
наполняет `ORCH_PLANE_*` / `ORCH_GITEA_*` и проходит D6.
- **never-raise:** сбой любой пробы → кандидат отброшен, детект продолжается; тотальный сбой →
пустой список → ручной ввод (AC-4 не должен ронять скрипт).
Привязка: BR-5, FR-4, AC-4, NFR-3.
### D6 — Интерактивный сбор + живая верификация ДО записи (FR-5)
Honest-checkpoint контракт (как `manual_checkpoint` эталона): для каждого требуемого секрета/URL —
печать «откуда взять» (ссылка на LITE_SETUP §5§8), скрытый ввод секрета (`getpass`),
**верификация онлайн ДО записи**:
- Plane: `GET <api>/api/v1/workspaces/<slug>/projects/` с `X-API-Key:<token>` → 200;
- Gitea: `GET <url>/api/v1/user` с `Authorization: token <token>` → 200;
- Telegram: `GET https://api.telegram.org/bot<token>/getMe``{"ok":true}`.
Провал → повтор (до N) либо `exit 2` с подсказкой; значение в `.env` **не пишется** (AC-5 FAIL —
неверный секрет записан / верификация после записи / секрет виден на экране). Авто-детектируемое
(uid/gid/docker-gid/порты/пути/node/claude/выбранные URL) **пред-заполняется** — оператор только
подтверждает. Привязка: BR-4, FR-5, AC-5, NFR-2.
### D7 — Секрет-гигиена и рендер `.env`/`.env.watchdog` (FR-6/FR-7, NFR-2)
- **Webhook-секреты — строго кирпич** `gen_secrets.py` субпроцессом во временный файл, парс
`parse_env`. **Никакого собственного `secrets.token_hex`/`token_urlsafe` в установщике**
(анти-форк, проверяется AST-сканом теста, AC-7). Уже присутствуют и валидны → skip (не
перетирать без `--force`).
- **Рендер** — идемпотентный `render_env(example_text, overrides)` из `.env.example` /
`.env.watchdog.example`: существующие ключи-override обновляются на месте (комментарии канона
сохранены), неизвестные — управляемым блоком в конец. Watchdog-ключи (`WATCHDOG_TG_*`) —
**только** в `.env.watchdog` (файл-носитель, LITE_SETUP §4.3; в `.env` они инертны).
- **Гигиена:** запись через `_write_private` (chmod **600**); значения секретов в stdout/лог
**не попадают** (лог печатает только имена ключей/пути); молчаливой перезаписи нет.
Привязка: BR-6, FR-6/FR-7, AC-6, NFR-2, INV-4.
### D8 — Подъём стека + регистрация проекта кирпичом (FR-8/FR-9)
- **Up:** `docker compose up -d --build` ровно `orchestrator` + `orchestrator-watchdog` (staging НЕ
поднимается — за профилем); ожидание готовности поллингом `GET /health` (таймаут).
- **Onboarding — строго кирпич** `onboard_project.py`: сбор параметров проекта (флаги или
интерактивно), `apply` затем `verify` субпроцессом (`--json`); парс `instructions[]` на
`ORCH_PROJECTS_JSON=<merged>` → запись в `.env` через `render_env`; manual-пункты отчёта (exit 2)
пробрасываются оператору. **Никакого собственного создания статусов/лейблов/репо/webhook**
(22 статуса — только кирпич, AC-7). После записи реестра — управляемый рестарт
`up -d --force-recreate orchestrator` в тихом окне (как LITE_SETUP §10).
Привязка: BR-1/BR-6, FR-8/FR-9, AC-7/AC-8.
### D9 — Webhook Plane = всегда верифицируемый manual-step (OQ-5) — граница connect-only
**Ключевое отличие Lite от Bundled.** `bootstrap_bundle.py::step_plane_webhook` *владеет*
Plane-Postgres (сам его поднял) и пробует прямой `INSERT` с manual-фолбэком. В Lite Plane — **чужая
инсталляция заказчика** (D-2): установщик может быть не на одном хосте с Plane, не иметь
docker-доступа к её БД, и raw-SQL в чужую БД — ровно та кросс-граничная мутация, что запрещает
INV-5. Поэтому webhook Plane в Lite — **всегда honest manual-step, никогда авто-SQL**:
- инструкция пути A (UI Plane: URL `…/webhook/plane`, secret `ORCH_PLANE_WEBHOOK_SECRET`, события
Issue + Issue Comment);
- путь Б (raw-SQL из LITE_SETUP §5.4) — **печатается как инструкция** для Plane CE без UI, но
установщиком **не исполняется**;
- **верификация** — фактическую запись webhook в Plane CE через `/api/v1` прочитать нельзя →
истинный гейт webhook'а = **smoke** (D12 / §11): реальная задача, переведённая в «To Analyse»,
даёт analyst-job в `/queue`. `manual_checkpoint` фиксирует факт «оператор настроил» и сообщает,
что финальная проверка — на smoke.
Привязка: BR-8, OQ-5, AC-5/AC-11, INV-3/INV-5.
### D10 — Логин claude CLI = manual-step с верификацией (OQ-6)
Интерактивный логин — поток Anthropic, **не автоматизируется** (подтверждено). Установщик:
детект `claude` (`shutil.which`/`claude --version`); отсутствует → consent-gated install (D4,
`npm install -g @anthropic-ai/claude-code`); логин → `manual_checkpoint`: инструкция «выполнить
`claude` на хосте под deploy-user», **верификация** = `claude --version` OK **и** читаемость
кред-файла под run-uid (`~/.claude/.credentials.json`, как LITE_SETUP §7.2). Привязка: OQ-6, AC-3,
NFR-3.
### D11 — Не-интерактивный / CI-контур (OQ-7)
- **Флаги:** `mode` (plan|apply|verify); `--force` (перевыпуск секретов → проброс `gen_secrets.py
--force`); проброс параметров проекта в `onboard_project.py`
(`--name/--repo/--prefix/--stack/--test-cmd/--prod-port/--staging-port/--description/
--webhook-url`); `--non-interactive`.
- **Секреты — из окружения, не из argv** (анти-ps-leak, NFR-2): в не-интерактиве токены берутся из
одноимённых `ORCH_*` env-переменных / существующего `.env`, не из флагов.
- **Fail-closed:** `--non-interactive` **или** `not sys.stdin.isatty()` → любой обязательный,
но отсутствующий/непрошедший верификацию вход → `exit 2` с **точным именем** недостающего ключа,
без prompt'а (зеркало `manual_checkpoint` эталона). Скрипт пригоден для повторного/CI-прогона
(идемпотентный `apply`).
Привязка: BR-7, FR-1, AC-8, NFR-2.
### D12 — Health-гейт и итоговая сводка (FR-10)
После `apply` (и в `verify`): `GET /health` → 200; `GET /queue` / `GET /metrics` → валидный JSON.
Итоговая сводка по шагам (`ok`/`skipped`/`manual-step`) + общий вердикт PASS/FAIL; любой FAIL шага →
`exit 1` с диагностикой (хвост `docker logs orchestrator --tail` / снапшот `/queue`). `verify`
(read-only) повторяет health-контракты + `onboard_project.py verify`. Привязка: BR-7, FR-10, AC-11.
### D13 — Анти-дрейф тесты (NFR-7)
Новый **`tests/test_install_lite_script.py`** — детерминированный (без сети/docker/subprocess/LLM;
HTTP/процессы — через инъекцию фейков):
- **Структурные:** ссылки на кирпичи (`gen_secrets.py`, `onboard_project.py`) присутствуют; **нет
собственной генерации секретов** (запрет `secrets.token_hex`/`token_urlsafe`/`token_bytes` в
файле — делегирование кирпичу, AC-7); **нет захардкоженных 22 имён статусов** (AC-7);
**stdlib-only** (AST-скан import'ов из разрешённого набора; нет `from src …`/`import src …`,
AC-10/INV-7); **нет delete/force-операций** (`rm -rf`, `docker … rm`, `down -v`, `git push
--force`, удаление веток, AC-10/INV-3); **FORBIDDEN host-литералы** — импорт `FORBIDDEN` +
`find_violations` из `test_no_host_hardcodes.py` (single source of truth, не копия, AC-10);
exit-контракт `{0,1,2}`.
- **Юнит (чистые функции):** `preflight_verdict` (блокеры/варнинги), `install_command(distro,dep)`,
`rank_candidates` (ранжир/дедуп), `parse_env`/`render_env` (round-trip, сохранение комментариев,
watchdog-ключи в свой файл), маскирование-хелпер.
- **С фейками:** verify-before-write (битый токен отклонён и не записан), идемпотентный `apply`
(второй прогон → `skipped`, без дублей), `plan` zero-mutation.
Обновление **`tests/test_lite_setup_doc.py`**: добавить ассерт «LITE_SETUP.md ссылается на
`install_lite.py` как рекомендованный путь»; **все существующие проверки (13 разделов, кирпичи,
key-sync, 22 статуса, FORBIDDEN) остаются зелёными** и не ослабляются. Привязка: BR-9, AC-9/AC-10/
AC-12, NFR-7.
### D14 — Doc-sync и норматив сопровождения (FR-11, BR-9)
- **`LITE_SETUP.md`:** указатель на установщик — **callout-врезка** (`>`-блок) в §1 (или сразу
перед §2), НЕ новый нумерованный раздел: «Рекомендованный путь — `python3 scripts/install_lite.py`
(автоматизирует §2§11); ручной маршрут ниже — референс/фолбэк для траблшутинга». **13 `## N.`
заголовков сохранены байт-в-байт** (на них ключуется `test_lite_setup_doc.py::SECTIONS`).
- **Обновить в том же PR (golden source, правило агентов №2):** `CLAUDE.md` (раздел тиража),
`docs/architecture/README.md` (Type A — Lite ← делается этой стадией, D15), `docs/overview/`
(витрина Lite-install/использования — затронута, см. ORCH-105; финальные правки — на стадии
development), `CHANGELOG.md`.
- **NFR-5 норматив:** меняешь шаги установки → синхронизируй `LITE_SETUP.md` в том же PR (держит
анти-дрейф `test_lite_setup_doc.py`). Зеркало норматива ORCH-102/103.
Привязка: BR-9, FR-11, AC-12.
### D15 — Границы scope, сквозная регистрация, отсутствие kill-switch
- **`07-infra-requirements.md` — N/A:** топология **нашего** оркестратора не меняется (установщик
вне рантайма, исполняется на хосте заказчика). Предусловия хост-контура — золотой источник
LITE_SETUP §2; форкать их в 07 запрещено каноном «link-first, не дублировать».
- **`08-data-requirements.md` — N/A:** изменений схемы БД нет (TRZ §5; БД создаёт сам орк пустой
при первом старте, stateless §12).
- **Сквозной ADR `adr-0040`** регистрирует Lite-установщик как операторский артефакт семьи тиража и
нормативную границу connect-only (D9). Детальный пакет (D1…D15) — в этом work-item ADR.
- **Kill-switch не нужен:** активация — только явный запуск оператором на его хосте (паттерн
ORCH-009/101/102/103). Рантайм байт-в-байт (INV-1).
## Альтернативы
- **Общий модуль `scripts/_replication_lib.py` (DRY сейчас)** — отвергнуто: потребовал бы править
замороженный `bootstrap_bundle.py` (риск регресса Bundled, TRZ §2) и противоречит «одному
установочному файлу» (BR-1). Вынос — позже, по rule-of-three (D2).
- **Имя `bootstrap_lite.py` (симметрия с bundle)** — отвергнуто: `install_*` понятнее оператору и
верно сигналит «connect-only» против «bring-up» bundle (D1).
- **Авто-драйв webhook Plane raw-SQL (как bundle `step_plane_webhook`)** — отвергнуто: в Lite Plane
чужой; raw-SQL в чужую БД нарушает INV-5 и может быть недостижим (нет docker-доступа). Только
manual-step + smoke-верификация (D9).
- **Тихий авто-install системных пакетов под sudo** — отвергнуто D-1: только consent + точная
команда из allowlist; неизвестный дистрибутив/нет TTY → инструктировать + exit 2 (D4).
- **Полноценный TUI-мастер / сторонние зависимости (rich/click)** — отвергнуто: NFR-6 stdlib-only,
работа на голом `python3` до первого `docker compose up`; `argparse`+`input`/`getpass` достаточно.
- **Замена `LITE_SETUP.md` установщиком** — отвергнуто: runbook остаётся золотым источником и
фолбэком (дополняем, не выкидываем; D14).
## Последствия
- **+** Оператор проходит §2§11 одним файлом; ошибки секретов/URL ловятся **в момент ввода**
(живая верификация), а не на `docker compose up`.
- **+** Идемпотентность/повтор/CI: `plan`/`apply`/`verify`, exit `0/2/1`, fail-closed без TTY.
- **+** Каноны не форкаются → нулевой дрейф статусов/секретов/env; `bootstrap_bundle.py` и
Bundled-путь не тронуты.
- **+** Self-hosting-безопасно: только scripts/docs/tests; говорит лишь с локальным хостом и
инфрой заказчика; 0 delete-операций; наш прод не затронут (исполняется на чужом хосте).
- **** ~150 строк дублирования примитивов с `bootstrap_bundle.py` (осознанно; rule-of-three
триггер задокументирован — D2).
- **** Эвристика детекта Plane/Gitea ложно-(положительна/отрицательна) на нестандартных
именах/портах → митигируется ручным фолбэком и живой верификацией (D5/D6, R-1).
- **** Webhook Plane остаётся ручным шагом (граница connect-only) → его корректность
подтверждается только на smoke (D9); это честно сообщается оператору.
- **Откат:** удаление `scripts/install_lite.py` + `tests/test_install_lite_script.py` + revert
callout-врезки `LITE_SETUP.md` и doc-правок. Состояние 1:1, ни миграций, ни рантайм-изменений.
## Ссылки
- BRD: `docs/work-items/ORCH-104/01-brd.md`
- TRZ: `docs/work-items/ORCH-104/02-trz.md` (OQ-1…OQ-7 → D1…D11)
- Acceptance: `docs/work-items/ORCH-104/03-acceptance-criteria.md` (AC-1…AC-12)
- Tech-risks: `docs/work-items/ORCH-104/10-tech-risks.md`
- Сквозной ADR: `docs/architecture/adr/adr-0040-lite-installer-canon.md`
- Предшественники: adr-0036 (фундамент 10-common), adr-0037 (Lite-канон/`LITE_SETUP.md`),
adr-0038 (Bundled/`bootstrap_bundle.py`), adr-0035 (онбординг/`onboard_project.py`)
- Сверено по коду: `scripts/bootstrap_bundle.py`, `scripts/gen_secrets.py`,
`scripts/onboard_project.py`, `tests/test_no_host_hardcodes.py`, `tests/test_lite_setup_doc.py`,
`docs/deployment/LITE_SETUP.md`, `.env.example`, `.env.watchdog.example`, `docker-compose.yml`

View File

@@ -0,0 +1,43 @@
---
work_item: ORCH-104
stage: architecture
author_agent: architect
status: proposed
created_at: 2026-06-12
model_used: claude-opus-4-8
---
# 10 — Технические риски: ORCH-104 — Установочный скрипт для Lite
Work Item: **ORCH-104** · Repo: **orchestrator** · Стадия: architecture
> Информационный (гейтом не парсится). Перечисляет риски реализации `scripts/install_lite.py` и их
> митигейшн. Опирается на BRD §8 (R-1…R-5) и архитектурные решения ADR-001 (D1…D15).
## Реестр рисков
| ID | Риск | Вер. | Влия. | Митигейшн |
|----|------|------|-------|-----------|
| TR-1 | Эвристика детекта Plane/Gitea ложно-положительна/отрицательна (нестандартные имена контейнеров/порты, reverse-proxy) → подставлен не тот URL. | Сред. | Сред. | D5: детект лишь **пред-заполняет догадку**; авторитет — живая верификация D6 (Plane `/projects/`, Gitea `/user`) ДО записи; ручной ввод URL — всегда доступный фолбэк; never-raise (NFR-3). Неверный URL не пройдёт верификацию → не запишется. |
| TR-2 | Утечка секрета в stdout/лог или перезапись существующего `.env` без согласия. | Низ. | **Выс.** | D7/NFR-2/INV-4: ввод секретов `getpass`; лог печатает только имена ключей/пути; запись `_write_private` (chmod 600); webhook-секреты — кирпич `gen_secrets.py` (отказ перезаписи без `--force`); idem-skip валидных значений. Покрыто AC-6 + AST-скан теста. |
| TR-3 | Управляемый авто-install выполняет небезопасную/неверную для дистрибутива root-команду. | Низ. | **Выс.** | D4/D-1: тихого авто-инсталла нет; выполнение только при TTY + явном `y/N` + команда из фиксированного allowlist; неизвестный дистрибутив (`install_command→None`)/нет TTY/отказ → инструкция + `exit 2`. Покрыто AC-3. |
| TR-4 | Установщик мутирует **чужую** инфраструктуру (raw-SQL в Plane-БД заказчика, правка `main`, force-push). | Низ. | **Выс.** | D9/INV-3/INV-5: webhook Plane = manual-step (печать инструкции, не исполнение SQL); 0 delete/force-операций (AST-скан AC-10); говорит только с локальным хостом и API заказчика; никогда не трогает `main`/прод. |
| TR-5 | Дрейф `install_lite.py``LITE_SETUP.md` при будущих правках (шаги разойдутся). | Сред. | Сред. | D14/BR-9/NFR-5: норматив «меняешь шаги → обнови LITE_SETUP.md в том же PR»; анти-дрейф `test_lite_setup_doc.py` (ссылка на установщик + 13 разделов/кирпичи/key-sync зелёные). Reviewer-ось обзорных доков (ORCH-079/011). |
| TR-6 | Дублирование примитивов с `bootstrap_bundle.py` (D2) расходится со временем (поведенческий дрейф `parse_env`/`render_env`/`manual_checkpoint`). | Низ. | Низ. | D2: примитивы малы, генеричны, стабильны; скрипты автоматизируют разные runbook'и; юнит-тесты round-trip `parse_env`/`render_env` в `test_install_lite_script.py`; rule-of-three триггер выноса в общий модуль задокументирован. |
| TR-7 | Установщик случайно импортирует платформу (`src/**`) или сторонний пакет → ломает stdlib-only/работу до `docker compose up`. | Низ. | Сред. | D2/D13/NFR-6/INV-7: AST-скан import'ов из разрешённого stdlib-набора, запрет `from src …`/`import src …`; работа на голом `python3` подтверждается тестом без сети/docker. |
| TR-8 | Webhook Plane настроен оператором неверно (опечатка секрета/URL/событий) — установщик не может прочитать его в Plane CE → рапортует успех при неработающем webhook. | Сред. | Сред. | D9: истинный гейт webhook'а**smoke** (D12/§11): задача в «To Analyse» → analyst-job в `/queue`; `manual_checkpoint` честно сообщает, что финальная проверка — на smoke; траблшутинг 401/HMAC — LITE_SETUP §13.1. |
| TR-9 | Не-интерактивный/CI-прогон без TTY молча зависает на `input()`/`getpass` или берёт неполные данные. | Низ. | Сред. | D11: `--non-interactive`/`not isatty()` → fail-closed `exit 2` с точным именем недостающего ключа; секреты из env, не из argv (анти-ps-leak); зеркало контракта `manual_checkpoint` эталона. |
| TR-10 | `apply` не идемпотентен: повтор создаёт дубль проекта/webhook, перевыпускает секреты или падает на сделанном шаге. | Низ. | Сред. | D3/D8: step-движок `check→ensure` (inline «уже сделано» → `skipped`); онбординг — идемпотентный кирпич `onboard_project.py`; секреты — idem-skip. Покрыто AC-8 (два последовательных `apply` с фейками). |
## Сводный вывод
Доминирующий класс — **гигиена секретов и кросс-граничные мутации** (TR-2/TR-3/TR-4): высокое
влияние при низкой вероятности, полностью снято архитектурой (getpass + 600 + кирпич-секреты;
consent-only allowlist-install; connect-only-граница без raw-SQL/delete/force). Остаточный риск для
**прод-конвейера self-hosting — нулевой**: установщик вне рантайма (`src/**`/`STAGE_TRANSITIONS`/
`QG_CHECKS`/схема БД байт-в-байт, INV-1), исполняется на хосте заказчика и не касается ни нашего
прода, ни `main`, ни общей БД. Эскалация `arch:major-change` **не требуется** (аддитивный
scripts+docs+tests артефакт, обратимый удалением файла); возврат в анализ не нужен — все 7 OQ ТЗ
разрешимы без нарушения принципов. Ведущие реализационные риски — **дрейф док↔скрипт (TR-5)** и
**ложный детект (TR-1)**: оба со средней вероятностью и снижены анти-дрейф-тестом и обязательной
живой верификацией с ручным фолбэком.