From a681d6e3f7b4ef5e9c607b49290e5705f749f2d2 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Fri, 12 Jun 2026 10:42:03 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=654 --- docs/architecture/README.md | 23 ++ .../adr/adr-0040-lite-installer-canon.md | 106 ++++++ .../ORCH-104/06-adr/ADR-001-lite-installer.md | 354 ++++++++++++++++++ docs/work-items/ORCH-104/10-tech-risks.md | 43 +++ 4 files changed, 526 insertions(+) create mode 100644 docs/architecture/adr/adr-0040-lite-installer-canon.md create mode 100644 docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md create mode 100644 docs/work-items/ORCH-104/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1a50dfb..6ebce17 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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 ≈13–14 контейнеров) для заказчика без собственной инфраструктуры. Состав Plane — зеркало официального selfhost-référence v0.23.1 diff --git a/docs/architecture/adr/adr-0040-lite-installer-canon.md b/docs/architecture/adr/adr-0040-lite-installer-canon.md new file mode 100644 index 0000000..e54ea6d --- /dev/null +++ b/docs/architecture/adr/adr-0040-lite-installer-canon.md @@ -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`. diff --git a/docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md b/docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md new file mode 100644 index 0000000..d0af123 --- /dev/null +++ b/docs/work-items/ORCH-104/06-adr/ADR-001-lite-installer.md @@ -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 ` и `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 /api/instances/` + (unauth-liveness Plane CE); Gitea — `GET /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/v1/workspaces//projects/` с `X-API-Key:` → 200; +- Gitea: `GET /api/v1/user` с `Authorization: token ` → 200; +- Telegram: `GET https://api.telegram.org/bot/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=` → запись в `.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` diff --git a/docs/work-items/ORCH-104/10-tech-risks.md b/docs/work-items/ORCH-104/10-tech-risks.md new file mode 100644 index 0000000..90e187f --- /dev/null +++ b/docs/work-items/ORCH-104/10-tech-risks.md @@ -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)**: оба со средней вероятностью и снижены анти-дрейф-тестом и обязательной +живой верификацией с ручным фолбэком.