diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1a50dfb..0de0aab 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -245,6 +245,28 @@ Type B). Анти-дрейф — `tests/test_bundle_compose.py` / `test_bundled_ [adr-0038](adr/adr-0038-bundled-replication-canon.md), детально — `docs/work-items/ORCH-103/06-adr/ADR-001-bundled-stack-compose-and-bootstrap.md`. +**Интерактивный installer Lite (ORCH-104 — design).** Lite получает инструмент, симметричный +Bundled-bootstrap'у: **`scripts/setup_lite.py`** — python stdlib-only wizard, автоматизирующий +маршрут LITE_SETUP §2–§12 (скан предусловий с офером доустановки per-package consent'ом; +discovery docker-инсталляций Plane/Gitea строго по image-префиксам с выбором пользователя; +интерактивный сбор ключей §4.2 с немедленной верификацией каждого значения и секрет-гигиеной; +автодетект хост-параметров; сборка `.env`/`.env.watchdog` от канонов + `gen_secrets.py`; +webhook Plane CE: UI-путь — рекомендация, SQL-путь — офер под предусловиями с обязательной +пост-верификацией; машинная охрана C-1/§6.4/ORCH-058; подъём ровно орк+watchdog; +onboarding строго кирпичом plan→согласие→apply→verify; итог PASS/FAIL/MANUAL, exit 0/2/1). +Режимы — семейные `plan`/`apply`/`verify`, но **дефолт — `apply`-wizard** (бизнес-цель «одна +команда»); безопасность дефолта — структурно, не режимом: фаза 0 ≡ plan (read-only), ранний +guard чужого `.env` (**маркер managed-файла**: без маркера → отказ exit 2, с маркером → +resume-ensure), per-action consent на каждую мутацию, non-TTY без явного `--yes` → exit 2 +(headless — env-prefill каноническими именами ключей, не answers-file). Step-движок +check→ensure без state-файла (resume = повторный запуск); no-delete; канон не форкается — +LITE_SETUP получает подраздел §1.1 «Быстрый путь» (пиннинг «13 разделов» цел), норматив +сопровождения расширен: «меняешь шаги тиража → обнови док **и скрипт** в том же PR». +Анти-дрейф — `tests/test_setup_lite_script.py` (зеркала delete/status-needle-наборов, unit +чистых функций) + аддитивный тест в `test_lite_setup_doc.py`. Рантайм/конвейер — байт-в-байт; +kill-switch не нужен. Подробнее: [adr-0040](adr/adr-0040-lite-interactive-installer.md), +детально — `docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`. + ## Витрина системы `docs/overview/` (ORCH-011 — design) Единая точка входа «бизнес + тех» для трёх аудиторий (заказчик / менеджер / разработчик) — diff --git a/docs/architecture/adr/adr-0040-lite-interactive-installer.md b/docs/architecture/adr/adr-0040-lite-interactive-installer.md new file mode 100644 index 0000000..d2218a9 --- /dev/null +++ b/docs/architecture/adr/adr-0040-lite-interactive-installer.md @@ -0,0 +1,121 @@ +--- +work_item: ORCH-104 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-11 +model_used: claude-opus-4-8 +--- + +# adr-0040: Интерактивный installer Lite-тиража — `scripts/setup_lite.py` (ORCH-104) + +## Статус +Proposed + +## Контекст + +Эпик ORCH-10 закрыт по обоим типам, но асимметрично: Type B — Bundled имеет **инструмент** +(`scripts/bootstrap_bundle.py`, ORCH-103/adr-0038), Type A — Lite — только **документ** +(`docs/deployment/LITE_SETUP.md`, ORCH-102/adr-0037: 13 разделов, ~30+ ручных команд, ~20 +обязательных ключей). Порог входа Lite высок: оператор вручную собирает значения, которые +машина определяет сама; ошибки конфигурации проявляются поздно (на smoke, не в момент ввода). +Бизнес-запрос Владельца: один установочный файл — выполняет установку, запрашивает данные в +момент установки, сканирует систему с офером доустановки, при нескольких инсталляциях +Plane/Gitea даёт выбрать. + +Сквозной характер: серия канонов тиража (adr-0035 onboarding → adr-0036 10-common → +adr-0037 Lite-док → adr-0038 Bundled) дополняется слоем «Lite-инструмент»; вводятся нормативы, +обязательные для будущих операторских CLI платформы и любого агента, меняющего шаги тиража. +Детальный пакет решений (D1…D12, исходы OQ-1…OQ-6 ТЗ) — work-item ADR: +`docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`. + +## Решение + +1. **Новый операторский CLI `scripts/setup_lite.py`** (имя зеркалит LITE_SETUP.md): python + stdlib-only wizard, автоматизирующий маршрут LITE_SETUP §2–§12 — скан предусловий хоста с + офером доустановки (per-package consent, закрытый набор менеджеров apt-get/dnf/yum/zypper, + re-check после установки, иначе MANUAL), discovery docker-инсталляций Plane/Gitea (строго + image-префиксы `makeplane/*`/`gitea/gitea*`; 0 → ручной ввод + подсказка про Bundled, 1 → + префилл, ≥2 → выбор; «ввести вручную» всегда; never-block), интерактивный сбор ключей §4.2 + с немедленной верификацией каждого значения фактическим вызовом (Plane/Gitea API, Telegram + getMe; лимит 3 попытки → MANUAL), автодетект хост-параметров, сборка `.env`/`.env.watchdog` + от канонов-example (webhook-секреты — строго кирпич `gen_secrets.py`), подъём ровно + орк+watchdog + health, регистрация проекта строго кирпичом `onboard_project.py` + (plan→согласие→apply→verify), итоговый отчёт PASS/FAIL/MANUAL + exit `0/2/1`. +2. **Wizard-контракт consent-gated мутаций (норматив для интерактивных операторских CLI):** + режимы — семейные `plan`/`apply`/`verify`, но **дефолт — `apply`** (бизнес-цель «одна + команда»); отступление от plan-default семейства безопасно по построению и допускается + ТОЛЬКО при четырёх структурных гарантиях: (а) фаза 0 apply ≡ plan (read-only скан + печать + плана + явный вопрос «продолжить?»), (б) ранний guard чужих конфигов до первого вопроса, + (в) per-action consent на каждую мутацию с печатью точной команды (отказ → честный MANUAL), + (г) non-TTY без явного `--yes` → немедленный exit 2 (зависание/мутации исключены). + Инструменты без per-action consent (bootstrap_bundle) обязаны сохранять plan-default. +3. **Маркер managed-файла** — норматив примирения идемпотентного resume с защитой чужих + конфигов: первая строка собранного скриптом `.env*` — фиксированный комментарий + `# managed by scripts/setup_lite.py (ORCH-104)`; существующий файл без маркера → отказ + exit 2 (без `--force`), с маркером → resume-ensure (дозаполнение без перетирания значений). +4. **Headless-канал — env-prefill + явный `--yes`** (не answers-file: секреты на диске вне + `.env` запрещены): значения берутся из переменных окружения процесса с каноническими + именами ключей `.env.example`; верификация обязательна и в headless; отсутствие + обязательного значения → exit 2, не молчаливый дефолт. +5. **Webhook Plane CE (Lite-инсталляция заказчика):** Path A (UI) — рекомендация + (manual-checkpoint с верификацией либо честный MANUAL «проверка — smoke §11»); Path Б + (SQL INSERT в Postgres Plane) — **офер** строго при предусловиях: docker-Plane + + подтверждённый пользователем контейнер БД + согласие с показом SQL + идемпотентный INSERT + + обязательная пост-верификация; только INSERT/SELECT; ввод валидируется (анти-инъекция); + сбой → fail-safe в Path A. (Отличие от Bundled adr-0038, где bootstrap владеет инсталляцией + и пишет безусловно.) +6. **Машинная охрана нормативов тиража:** C-1 ORCH-100 (идентичные токены орка/watchdog → + отказ шага), §6.4/D10 ORCH-009 (непустые `branch_protections` на `main` → FAIL с лечением, + удаление — никогда), guard ORCH-058 (`ORCH_STAGING_PORT == прод-порт` → fail-closed), + когерентная тройка прод-порта — одной чистой функцией. +7. **Канон не форкается; норматив сопровождения расширен:** LITE_SETUP.md получает подраздел + `### 1.1. Быстрый путь` (пиннинг «13 разделов в порядке» цел байт-в-байт; ручной маршрут — + канон и fallback для MANUAL-шагов); footer-норматив NFR-5: «меняешь шаги тиража → обнови + LITE_SETUP.md **и `scripts/setup_lite.py`** в том же PR». Канон-знания в скрипте запрещены + структурно: кирпичи — только субпроцессами; имён Plane-статусов в скрипте нет вообще + (зеркало `FORBIDDEN_STATUS_NEEDLES`); smoke — ссылкой на §11. +8. **Анти-дрейф — постоянная CI-гарантия:** `tests/test_setup_lite_script.py` (ast stdlib-only, + зеркала delete/status-needle-наборов, кирпичи упомянуты, unit чистых функций: вердикты + скана, классификатор discovery, когерентность портов, рендер env с маркером, builder + onboard-аргументов, step-движок/resume, exit-контракт, дефолт `apply`, секрет-гигиена + транскрипта) + аддитивный тест в `tests/test_lite_setup_doc.py` (скрипт упомянут в доке; + существующие ассерты и кортеж `SECTIONS` не правятся). + +### Что НЕ меняется +`src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.env.example`, `.env.watchdog.example`, +кирпичи `gen_secrets.py`/`onboard_project.py`/`bootstrap_bundle.py`, `deploy/**`, +`onboarding/**`; `STAGE_TRANSITIONS`, состав `QG_CHECKS`, семантика `check_*`, machine-verdict +ключи, схема БД — байт-в-байт. Kill-switch не вводится (активация — только явный запуск +оператора; паттерн ORCH-009/102/103). Прод-контейнер в рамках задачи не рестартуется. + +## Альтернативы +- **plan-дефолт (строгий D5 ORCH-009/103)** — отвергнуто для wizard-инструмента: воспроизводит + порог, который задача убирает; безопасность дана гарантиями п.2, а не выбором режима. +- **State-файл прогресса / answers-file / файл-отчёт** — отвергнуты: новые артефакты = новые + поверхности дрейфа и утечки; реальность как источник resume + env-prefill + stdout покрывают + те же кейсы. +- **Безусловная SQL-автоматизация webhook (как Bundled)** — отвергнуто: в Lite БД Plane — чужая + прод-инсталляция (см. п.5). +- **Новый раздел LITE_SETUP / перенумерация** — отвергнуто: ломает пиннинг «13 разделов» и + внешние §-ссылки; подраздел §1.1 даёт то же бесплатно. + +## Последствия +- Порог входа Lite: «прочитай 13 разделов» → «запусти один файл»; ошибки конфигурации ловятся + в момент ввода; число обращений «не взлетело» падает. +- Платформа получает переиспользуемые нормативы: wizard-контракт consent-gated мутаций (п.2), + маркер managed-файла (п.3), headless-канал env-prefill+`--yes` (п.4) — для будущих + операторских CLI. +- Цена: двойной источник маршрута (док+скрипт) — под нормативом same-PR и анти-дрейф тестами; + дефолт `apply` расходится с семейством — зафиксировано явным тест-ассертом с обоснованием. +- Откат: удалить скрипт, тест-модуль, §1.1/footer и аддитивный тест — состояние 1:1 + (scripts+docs+tests, без миграций). + +## Связи +adr-0037 (ORCH-102 — канон LITE_SETUP: автоматизируемый маршрут и пиннинг структуры), +adr-0038 (ORCH-103 — bootstrap-паттерн: step-движок, manual-checkpoint, no-delete, кирпичи +субпроцессами), adr-0036 (ORCH-101 — «дефолт=боевое», gen_secrets, guard ORCH-058), +adr-0035 (ORCH-009 — onboarding-CLI: 22 статуса, D10 branch protection), adr-0033 (ORCH-100 — +C-1 независимые Telegram-каналы). Детально — +`docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md`, +`07-infra-requirements.md`, `10-tech-risks.md`. diff --git a/docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md b/docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md new file mode 100644 index 0000000..6d6549d --- /dev/null +++ b/docs/work-items/ORCH-104/06-adr/ADR-001-setup-lite-interactive-installer.md @@ -0,0 +1,470 @@ +--- +work_item: ORCH-104 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-11 +model_used: claude-opus-4-8 +--- + +# ADR-001: Интерактивный installer Lite-тиража — `scripts/setup_lite.py` (wizard поверх канона LITE_SETUP) + +Work Item: **ORCH-104** — Установочный скрипт Lite-тиража (интерактивный installer) +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0040-lite-interactive-installer.md`** +(дополняет серию канонов тиража adr-0035…0038: Lite получает исполняемый инструмент; +вводится норматив сопровождения «док + скрипт в одном PR» и wizard-контракт consent-gated +мутаций, обязательный для будущих операторских CLI платформы). + +## Статус +Proposed + +## Контекст + +Эпик ORCH-10 закрыт по обоим типам: Type A — Lite **документом** (ORCH-102: +`docs/deployment/LITE_SETUP.md`, 13 нормативных разделов, ~30+ ручных fenced-команд), Type B — +Bundled **комплектом + скриптом** (ORCH-103: `deploy/bundled/` + `scripts/bootstrap_bundle.py`). +Асимметрия: у Lite нет инструмента — порог входа высокий (BRD §1.1: оператор вручную собирает +значения, которые машина определяет сама; ошибки конфигурации проявляются поздно). Бизнес-запрос +Владельца (BRD §1.2): один установочный файл, который выполняет установку, запрашивает данные в +момент установки, сканирует систему с офером доустановки и даёт выбор при нескольких инсталляциях. + +Факты, сверенные с репо (не изобретать): + +- **Паттерн step-движка готов:** `scripts/bootstrap_bundle.py` (ORCH-103 D5) — режимы + `plan` (дефолт)/`apply`/`verify`, check→ensure (повторный запуск = каскад skip), exit `0/2/1`, + `manual_checkpoint(title, instructions, verify, max_tries=3)` (без TTY — немедленный exit 2), + чистые функции `parse_env`/`render_env`/`preflight_verdict`/`build_plan` под unit-тестами, + `_ensure_venv` (host-venv для onboard-кирпича, probe `import httpx, pydantic`), + `_psql` (SQL через stdin — секреты не в argv), инвариант `APPLY_STEPS == build_plan()`. +- **Кирпичи:** `scripts/gen_secrets.py` (`--write PATH` x-mode: существующий файл → exit 2, + перезапись только `--force`; `secrets.token_hex(32)` → 64 hex); `scripts/onboard_project.py` + (`plan|apply|verify`, обязательные `--name/--repo/--prefix/--stack/--test-cmd/--prod-port/ + --staging-port/--webhook-url`, опции `--gitea-owner/--env-file/--json`, exit 0/2/1). +- **Анти-дрейф канона:** `tests/test_lite_setup_doc.py` пиннит кортеж `SECTIONS` — + заголовки `## 1.`…`## 13.` в порядке маршрута (TC-01), «каждый раздел §2–§13 несёт + fenced-команду» проверяется только для `SECTIONS[1:]` (§1 — без требования команд), каждый + упомянутый в доке `ORCH_*`/`WATCHDOG_*`-токен обязан существовать в `.env.example` + (`_ENV_TOKEN_RE`), fenced-блоки сканируются на `FORBIDDEN`-литералы и секрет-эвристику + (hex≥32 / alnum≥40). +- **Структурная гигиена скриптов:** `tests/test_bootstrap_script.py` — ast-скан stdlib-allowlist, + `FORBIDDEN_DELETE_NEEDLES` (`volume rm`, `rm -rf`, `down -v`, `compose down`, `rmtree`, + `os.remove`, `.unlink`), `FORBIDDEN_STATUS_NEEDLES` (`Backlog`, `To Analyse`, `Confirm Deploy`, + …) — собственный канон статусов в скрипте запрещён. +- **Сеть нашего контура:** все три сервиса корневого `docker-compose.yml` — + `network_mode: host` → из контейнера орка `http://127.0.0.1:` достигает + сервисов хоста; URL-кандидаты discovery на published-портах валидны для API-ключей. +- **Целевой режим резолва модели/эффорта, статусов и секретов** — только кирпичи/конфиг + (ORCH-41/74, `plane_sync._PLANE_NAME_TO_KEY`, gen_secrets): скрипт канон-знаний не дублирует. + +ТЗ (02-trz.md §12) оставило архитектору OQ-1…OQ-6. Ниже — пакет решений D1…D12. +Тип изменения — **scripts + docs + tests**: рантайм `src/**` байт-в-байт (BR-8/AC-13). + +## Решение + +### Сводка + +Новый операторский CLI **`scripts/setup_lite.py`** (имя финализировано, зеркалит +`docs/deployment/LITE_SETUP.md`): python stdlib-only wizard, автоматизирующий маршрут +LITE_SETUP §2–§12. Режимы — семейные **`plan` / `apply` / `verify`**, но **дефолт — `apply`** +(интерактивная установка; осознанное отступление от plan-default семейства — D2): бизнес-цель +«одна команда» — суть задачи, а безопасность дефолта обеспечена структурно (фаза 0 ≡ plan, +ранний guard `.env`, per-action consent на каждую мутацию, non-TTY → exit 2 до мутаций). +Step-движок check→ensure, 10 нормативных шагов (D3), exit `0/2/1`, resume = повторный запуск +без state-файла (реальность — единственный источник истины; коллизию «resume против guard +существующего `.env`» решает маркер-строка managed-файла — D6). Скан предусловий с офером +доустановки per-package consent'ом (D4), discovery docker-инсталляций Plane/Gitea строго по +image-префиксам с выбором пользователя (D5), интерактивный сбор ключей §4.2 с немедленной +верификацией и секрет-гигиеной (getpass, env-prefill + `--yes` для headless — D10), webhook +Plane CE: Path A (UI) — рекомендация, Path Б (SQL) — офер под пятью предусловиями (D8), +машинная охрана нормативов C-1 / §6.4 / ORCH-058 (D9), onboarding строго кирпичом +plan→согласие→apply→verify (D11). Канон не форкается: LITE_SETUP получает подраздел +`### 1.1. Быстрый путь` (пиннинг «13 разделов» цел байт-в-байт — D12). + +### D1 — Имя, место, рамка: `scripts/setup_lite.py`, один файл, stdlib-only (OQ-1-часть) + +- **`scripts/setup_lite.py`** — один файл в `scripts/` рядом с кирпичами; имя зеркалит док-канон + (`LITE_SETUP.md` ⇄ `setup_lite.py` — мнемоническая пара «инструкция ⇄ инструмент»). + Альтернатива `bootstrap_lite.py` отвергнута: bootstrap в платформе уже значит «доводка + Bundled-стека» (ORCH-103); у Lite семантика иная — установка поверх инфраструктуры заказчика. +- **python stdlib-only** (NFR-1): запуск `python3 scripts/setup_lite.py` из корня чекаута на + голом python3 ДО venv и до `docker compose up`; импорты — только stdlib-allowlist (ast-скан, + паттерн `test_bootstrap_script.py::STDLIB_ALLOWED`); `src.*` не импортируется; канон-знания — + только субпроцессами кирпичей (`gen_secrets.py`, `onboard_project.py`). +- **Язык UX — русский** (паттерн gen_secrets/bootstrap_bundle); каждое сообщение шага ссылается + на соответствующий § LITE_SETUP как на полный канон (NFR-4). +- Рамка Lite не расширяется: Linux x86_64 + Docker/Compose v2; установка Plane/Gitea — вне + объёма (подсказка «нет инфраструктуры → Bundled»); teardown/uninstall отсутствуют. + +### D2 — Режимы и дефолт: `plan`/`apply`/`verify`, дефолт — `apply`-wizard (OQ-1) + +- **Закрытый набор режимов** — семейная лексика ORCH-009/103: `plan`, `apply`, `verify` + (никаких новых слов `wizard`/`install`: оператор, выучивший bootstrap_bundle, понимает + setup_lite мгновенно; TC-01 «набор режимов закрыт»). +- **Контракты `plan` и `verify` — 1:1 с семейством:** `plan` — строгий read-only (скан + предусловий + discovery + автодетект + печать плана шагов; ноль мутаций ФС/docker/сети; + exit 0 — блокеров нет / 2 — есть); `verify` — read-only пост-проверка (`/health`+`/queue`+ + `/metrics`, состав «ровно орк+watchdog», stateless-чистота §12, `onboard_project.py verify` + субпроцессом при доступном venv). Оба — полноценные non-TTY-режимы (CI-пригодны). +- **Дефолт без аргументов — `apply`** (отступление от plan-default семейства, осознанное и + тестируемое): бизнес-запрос Владельца — «один установочный файл, который на автомате + выполняет установку» (BRD §1.2 п.1); plan-default воспроизвёл бы порог, который задача + призвана убрать. **Семейный инвариант сохранён в его сути** — «запуск без аргументов не + мутирует без явного согласия» — но другим механизмом: у bootstrap_bundle `apply` мутирует + без per-action вопросов (его согласие = выбор режима), поэтому там безопасен только + plan-default; у setup_lite **каждая мутация отдельно согласуется** (FR-1), поэтому + apply-default безопасен по построению: + 1. **Фаза 0 `apply` ≡ `plan`:** read-only скан + discovery + печать плана; первый вопрос + оператору задаётся только после неё («Продолжить установку? [y/N]» — дефолт-ответ N); + 2. **Ранний guard `.env`:** существующий немаркированный `.env`/`.env.watchdog` (D6) → + отказ exit 2 ДО первого вопроса установки (NFR-7: живой хост защищён); + 3. **Per-action consent:** каждая мутация (установка пакета, запись файла, SQL, `up`, + рестарт) — отдельное согласие с печатью точной команды; отказ → честный MANUAL + с эквивалентной командой, не молчаливый пропуск; + 4. **non-TTY:** `apply` без `--yes` → немедленный честный exit 2 с подсказкой + (никаких зависаний и мутаций — D10). +- Тест-контракт (новый модуль, D12): `parse_args([]).mode == "apply"` — **сознательно** + зеркальный к `test_plan_is_default_mode` bootstrap'а ассерт с комментарием-обоснованием; + набор режимов закрыт `choices=("plan", "apply", "verify")`. +- **Флаги:** `--force` (разрешить перезапись существующих НЕмаркированных `.env*` — см. D6; + печать пути + согласие всё равно запрашивается), `--yes` (headless-consent, D10). Параметры + проекта заказчика (для onboarding-шага) — опциональные `--project-*`-флаги как + альтернатива интерактивному вводу (точный список — developer, зеркало sandbox-флагов + bootstrap_bundle). + +### D3 — Step-движок: 10 нормативных шагов, check→ensure, без state-файла (FR-1) + +- **Нормативный план `apply`** (механика — паттерн `build_plan()`/`APPLY_STEPS` ORCH-103; + инвариант «APPLY_STEPS == build_plan()» — под тестом): + + | # | Шаг | § LITE_SETUP | Суть | + |---|-----|--------------|------| + | 1 | `scan` | §2, §7 | read-only скан предусловий + автодетект хост-параметров + ранний guard `.env` (D6) | + | 2 | `prereqs` | §2, §7 | доустановка MISSING per-package consent'ом / MANUAL (D4) | + | 3 | `discovery` | §5, §6 | обнаружение инсталляций Plane/Gitea, выбор / ручной ввод (D5) | + | 4 | `collect` | §4.2, §5–§8 | интерактивный сбор ключей с немедленной верификацией (D9, D10) | + | 5 | `render-env` | §4 | сборка `.env`/`.env.watchdog` от канонов + gen_secrets + `docker compose config` (D6, D7) | + | 6 | `plane-webhook` | §5.4 | Path A manual-checkpoint / Path Б офер SQL (D8) | + | 7 | `gitea-guards` | §6 | branch_protections == `[]` (FAIL+лечение), ssh-pubkey manual-step (D9) | + | 8 | `up` | §9 | `docker compose up -d --build` с согласия; состав «ровно орк+watchdog»; health-чек | + | 9 | `onboard` | §10 | кирпич plan→согласие→apply→verify; `ORCH_PROJECTS_JSON` → `.env`; управляемый рестарт (D11) | + | 10 | `report` | §11, §12 | stateless-проверка; итоговая таблица PASS/FAIL/MANUAL; smoke-инструкция ссылкой на §11 | + +- **check→ensure:** каждый шаг сперва проверяет «уже сделано?» по реальности (файл существует и + маркирован; токен валиден API-пробой; контейнеры бегут; webhook существует) и при PASS + пропускается. **State-файл НЕ вводится** (альтернатива отвергнута: новый артефакт = новая + поверхность дрейфа/устаревания; реальность — единственный источник истины; resume после + manual-step = просто повторный запуск — каскад skip доводит до первого незавершённого шага, + NFR-6; паттерн bootstrap_bundle). +- **Exit-коды — именованные константы** `EXIT_OK=0` / `EXIT_MANUAL=2` / `EXIT_ERROR=1` + (контракт FR-1/AC-11; исключения `ManualStop`/`SetupError` — паттерн bootstrap). +- **Smoke-инструкция шага 10 — ссылкой на LITE_SETUP §11**, без текста статусов: скрипт не несёт + имён Plane-статусов вообще (новый тест зеркалит `FORBIDDEN_STATUS_NEEDLES` — D12); вердикт + «тираж PASS» остаётся за оператором (FR-10). +- **Итоговая таблица** (FR-10): шаг → PASS/FAIL/MANUAL/skip; для MANUAL — что сделать + § дока; + для FAIL — диагноз и лечение (зеркало §13). Опциональный файл-отчёт НЕ вводится (stdout + + exit-код достаточны; меньше артефактов — меньше утечек, NFR-3). + +### D4 — Скан предусловий и оферы установки: per-package consent, закрытый набор менеджеров (OQ-4) + +- **Вердикты — чистая функция** `prereq_verdicts(facts) -> [(item, OK|MISSING|WARN|MANUAL, + detail)]` от read-only снимка фактов хоста (паттерн `collect_facts`/`preflight_verdict`): + ОС/арх (`uname -sm`; не-Linux/не-x86_64 → **WARN «вне контура Lite»**, не FAIL), docker, + compose v2, git, python3, node, дистрибутив claude-code (`npm root -g` → + `@anthropic-ai/claude-code`), аутентификация CLI (читаемость `~/.claude/.credentials.json` + uid'ом будущего контейнера — §7.2), группа docker (`getent group docker`), uid/gid владельца + каталога репо (§2.2), ssh-каталог (§2.4), свободность портов (§2.5). Ни один пункт перечня + FR-2 не пропускается молча (AC-2). +- **Оферы установки — исполняются скриптом, но только с явного per-package согласия** + (бизнес-запрос п.3 «предлагает установить»; BRD R-2): печать **точной команды** ДО исполнения + → согласие → исполнение → **re-check фактом** (повторная проверка версии/наличия; для compose + обязательно `docker compose version` — нативные репо дистрибутивов могут нести v1!) → + не сошлось → честный MANUAL со ссылкой на официальную инструкцию. Отказ от согласия → + MANUAL с той же командой (TC-05). +- **Закрытый набор пакетных менеджеров: `apt-get` / `dnf` / `yum` / `zypper`** (детект по + наличию бинаря, в этом порядке; `apt-get`, не `apt` — стабильный CLI). Неопределимый + менеджер (например, pacman/alpine) → MANUAL с готовыми generic-командами и ссылкой на + § LITE_SETUP — не падение (TC-06). Точные имена пакетов per-менеджер — developer (карта- + константа в скрипте); политика зафиксирована здесь. +- **Sudo-честность:** требуется root, а его нет (не root и sudo недоступен) → MANUAL с командой + под sudo; скрипт не пытается эскалировать привилегии сам. +- **Спец-случаи:** node+claude-code — офер `npm install -g @anthropic-ai/claude-code` (node — + пакетом менеджера); ssh-ключи — офер `ssh-keygen -t ed25519` + печать pubkey как manual-step + «добавить в Gitea» (§2.4/§6.2); **OAuth-логин claude CLI не автоматизируется** — только + верификация результата + manual-step (§7.2, допущение BRD §6). +- **Автодетект хост-параметров (FR-5)** — спрашивается только подтверждение, не значения: + `ORCH_RUN_UID`/`ORCH_RUN_GID` (владелец `ORCH_HOST_REPOS_DIR` / текущий пользователь), + `ORCH_DOCKER_GID`, `ORCH_HOST_NODE_BIN` (`which node`), `ORCH_HOST_CLAUDE_CODE_DIR` + (`npm root -g`), `ORCH_AGENT_HOME_DIR`/`ORCH_HOST_CLAUDE_DIR`/`ORCH_HOST_CLAUDE_JSON` + (HOME из §2.2), `ORCH_DEPLOY_HOST_REPO_PATH` (корень чекаута). + +### D5 — Discovery Plane/Gitea: строго image-префиксы, выбор всегда за пользователем (FR-3) + +- **Источник — только локальный Docker:** перечисление контейнеров (`docker ps` с форматом, + дающим имя/образ/published-порты/метку `com.docker.compose.project`); группировка контейнеров + в «инсталляции» по метке compose-проекта (без метки → группа-одиночка по имени контейнера). +- **Опознание — строго по префиксам образов** (анти-ложноположительность R-6; имена + контейнеров/проектов НЕ используются как признак): Plane — образы `makeplane/*`; + Gitea — `gitea/gitea*` и `docker.gitea.com/gitea*`. Посторонние образы в кандидаты не + попадают (AC-3). Список префиксов — константа скрипта (расширение = осознанная правка под + тестом). +- **URL-кандидаты из published-портов:** Plane — порт контейнера-входа (образ + `makeplane/plane-proxy`; нет proxy → инсталляция показывается без URL-префилла); + Gitea — published-порт на контейнерный 3000. Предлагаются: API-URL `http://127.0.0.1:` + (валидно: корневой compose — `network_mode: host`, контейнер орка видит published-порты хоста + через loopback) и публичный URL `http://:` (hostname — ввод/подтверждение + оператора; для webhook'ов нужен адрес, достижимый со стороны Plane/Gitea). +- **Поведение по числу кандидатов (BR-4):** `0` → ручной ввод URL + честная подсказка «Lite не + устанавливает Plane/Gitea; нет инфраструктуры — маршрут Bundled (BUNDLED_SETUP.md)»; `1` → + префилл по умолчанию с подтверждением; `≥2` → нумерованный список (compose-проект, образы, + порты) + выбор. Пункт **«ввести вручную» доступен всегда** (включая случай 1 и ≥2). +- **Best-effort, never-block:** docker недоступен / ошибка перечисления / native- или + k8s-инсталляция → ручной ввод URL, не FAIL и не падение (TC-09). Discovery заполняет только + кандидаты URL; **токены всегда вводит пользователь** (FR-4), выбранный кандидат всё равно + проходит токен-верификацию. +- **Постгрес-кандидат для Path Б (D8):** из выбранной Plane-инсталляции — контейнеры с образом + `postgres*` ТОГО ЖЕ compose-проекта; подтверждение пользователя обязательно. + +### D6 — Сборка `.env`/`.env.watchdog`: рендер от канонов + маркер managed-файла (FR-6; решает «resume против guard») + +- **Рендер от канонов** `.env.example`/`.env.watchdog.example` чистой функцией (паттерн + `render_env` bootstrap: строки/комментарии канона сохраняются, значения подставляются; + принцип ORCH-101 «дефолт = боевое значение» — записываются только собранные отличия). + `WATCHDOG_TG_BOT_TOKEN`/`WATCHDOG_TG_CHAT_ID` кладутся **только** в `.env.watchdog` + (ловушка файла-носителя §4.3); подсказки-дефолты промптов — из `.env.example`/автодетекта, + **никогда** из боевых значений (TC-15: FORBIDDEN-набор переиспользуется). +- **Маркер managed-файла — ключ к resume:** первая строка каждого собранного скриптом файла — + фиксированный комментарий-маркер `# managed by scripts/setup_lite.py (ORCH-104)`. Семантика + guard'а на шаге `scan`: + - файла нет → штатный путь (рендер на шаге 5); + - файл есть, **маркера нет** → это чужой/живой конфиг → **отказ exit 2 без `--force`**, + файл байт-в-байт не тронут (NFR-7/AC-5/TC-14; `--force` = явная перезапись с согласием); + - файл есть, **маркер есть** → собран нами ранее → **resume-ensure**: дозаполнение + недостающих ключей, существующие значения не перетираются (NFR-6; без маркера resume после + manual-step на шаге 6+ был бы невозможен — guard отбивал бы собственный артефакт). +- **Webhook-секреты — строго кирпичом:** субпроцесс `gen_secrets.py --write /fragment` + → парс фрагмента → перенос двух ключей (паттерн `step_secrets` bootstrap); свежий выпуск, + боевые секреты не используются (stateless §12). Запись live-файлов — права `600`, содержимое + не печатается (NFR-3). +- **Проверка шага:** `docker compose config` → PASS/FAIL (§4 «Проверка»; ошибка интерполяции → + диагноз «ищите незакрытую кавычку/невалидный JSON в ORCH_PROJECTS_JSON»). + +### D7 — Порты: busy-check, когерентная тройка одной функцией, staging ≠ prod fail-closed (FR-5) + +- Busy-check прод/staging-портов сокетом (паттерн `_port_busy`); занят → предложить + альтернативу (ввод с дефолтом «порт+N»). +- **Когерентность тройки — механически, одной чистой функцией** `port_overrides(prod_port) -> + {ORCH_DEPLOY_PROD_TARGET_PORT, WATCHDOG_METRICS_URL, ORCH_POST_DEPLOY_BASE_URL}` — рассинхрон + структурно невозможен (TC-16; ловушка §2.5/§4.2 закрывается кодом, не дисциплиной). +- **`ORCH_STAGING_PORT == прод-порт` → отказ fail-closed** на вводе (значение не принято, + re-prompt; инвариант ORCH-058, усиленный ORCH-101; TC-17). + +### D8 — Webhook Plane CE: Path A (UI) — рекомендация, Path Б (SQL) — офер под предусловиями (OQ-3) + +Каверза Plane CE: webhook не экспонирован во внешнем `/api/v1` (§5.4). В Bundled bootstrap +выполняет SQL-вставку безусловно — он **владеет** инсталляцией (сам её создал, знает контейнер +и креды). В Lite Plane — **продукт заказчика**: контейнер/пароль БД скрипту неизвестны, +инсталляция может быть не-docker, мутация чужой прод-БД инвазивна (BRD R-3, NFR-2: +«UI-путь предпочтителен»). Решение — двухпутёвый шаг с приоритетом UI: + +- **Path A (UI) — дефолт-рекомендация:** `manual_checkpoint`-контракт (печать точной инструкции + §5.4 с подставленными URL приёмника и именем секрета — значение секрета НЕ печатается, только + имя ключа) → подтверждение оператора → верификация. Верификация Path A: при доступных + координатах БД (введены для Path Б/проверки) — механический `SELECT url, is_active FROM + webhooks`; иначе — честная деградация: шаг фиксируется **MANUAL** («подтверждено оператором, + сквозная проверка доставки — smoke §11»), НЕ выдаётся за механически проверенный PASS (NFR-4). +- **Path Б (SQL INSERT) — офер автоматизации**, исполняется ТОЛЬКО при всех предусловиях: + 1. Plane-инсталляция — docker, и **Postgres-контейнер выбран и подтверждён пользователем** + (кандидаты — D5: `postgres*`-образы того же compose-проекта); + 2. **явное согласие с показом точного SQL** (INSERT канона §5.4) ДО исполнения; + 3. пароль БД Plane — **скрытый ввод** (или env-prefill, D10); в argv не попадает — + SQL и `PGPASSWORD` передаются через stdin/env субпроцесса (паттерн `_psql` bootstrap); + 4. **идемпотентность:** сначала `SELECT count(*)` по URL приёмника (`deleted_at IS NULL`) — + уже зарегистрирован → skip (паттерн `_exists` bootstrap); + 5. **обязательная пост-верификация** `SELECT url, is_active` → нет строки → шаг НЕ PASS. +- **Только INSERT/SELECT** — никаких UPDATE/DELETE (no-delete распространяется на чужую БД); + **slug и прочие подстановки валидируются** (`^[a-z0-9-]+$` для slug; UUID — `uuid.uuid4()` + локально; секрет — через psql-переменную/stdin, не конкатенация в argv) — анти-SQL-инъекция + на пользовательском вводе (отличие от bundle, где значения самогенерированные). +- Любой сбой Path Б / отказ согласия / не-docker Plane → **fail-safe в Path A** (manual-checkpoint + c инструкцией UI-пути; мутирующий вызов не произведён — TC-20). + +### D9 — Машинная охрана нормативов: C-1, §6.4, верификация каждого ввода (FR-4/FR-7) + +- **Немедленная верификация каждого введённого значения фактическим вызовом** (FR-4): + Plane-токен — `GET /api/v1/workspaces//projects/` c `X-API-Key` → 200; Gitea-токен — + `GET /api/v1/user` → 200 (+ логин владельца → префилл `ORCH_GITEA_OWNER`); Telegram — + `getMe` → `"ok":true` + helper chat-id через `getUpdates`; публичный URL оркестратора — + синтаксическая валидация + предупреждение «достижимость со стороны Plane/Gitea проверится + на smoke». Неуспех → re-prompt с диагнозом, **лимит 3 попытки** (паттерн + `manual_checkpoint(max_tries=3)`) → MANUAL/exit 2, не бесконечный цикл (TC-10). +- **C-1 (ORCH-100) — машинно:** токен watchdog-бота **байт-в-байт равен** токену орка → отказ + шага с объяснением запрета и требованием другого токена (TC-18); оба токена — скрытый ввод. +- **§6.4 — branch protection:** `GET /api/v1/repos///branch_protections` непуст → + **FAIL шага с лечением** (текст норматива: правила удалить, иначе ложные HOLD — §13.7); + скрипт правила **сам не удаляет** (no-delete; TC-19). Проверка — для репо проекта заказчика + после onboarding (шаг 9) и/или по введённому owner/repo. +- **§7.3:** наличие непустых `ORCH_AGENT_MODEL_DEFAULT`/`ORCH_AGENT_EFFORT_DEFAULT` в собранном + `.env` (дефолты канона `.env.example` достаточны — проверка, не ввод). + +### D10 — non-TTY и headless: env-prefill + явный `--yes`; answers-file отвергнут (OQ-2) + +- **Интерактив — за инжектируемым I/O** (NFR-5): примитивы `ask(key, secret=False)` / + `consent(action)` принимают источники ввода/вывода параметрами → unit-тесты со + скриптованными ответами без TTY (TC-10…TC-12). +- **Env-prefill — неинтерактивная альтернатива без новой лексики:** `ask()` ПЕРЕД промптом + проверяет переменную окружения процесса с **тем же каноническим именем ключа** + (`ORCH_PLANE_API_TOKEN`, `WATCHDOG_TG_BOT_TOKEN`, …) — значение найдено → принимается без + промпта, **но верификация выполняется как обычно** (неверный токен из env → в TTY re-prompt, + в headless exit 2). Словарь имён один — `.env.example`; никакого нового формата. +- **`--yes` — headless-consent:** явный флаг = заранее данное согласие на consent-вопросы + (аналог семантики «выбор режима apply = согласие» bootstrap_bundle). non-TTY матрица: + - `apply` без `--yes` → немедленный честный **exit 2** с перечнем того, что потребует + интерактива, и подсказкой (`plan`/`verify` доступны; TTY или `--yes`+env-prefill) — + зависание исключено (TC-12); + - `apply --yes` → consent'ы подтверждены флагом; значения — только из env-prefill; + отсутствует обязательное значение → exit 2 (не молчаливый дефолт); manual-step → exit 2 + (resume — повторный запуск); + - `plan`/`verify` — работают в non-TTY полноценно (exit по результату). +- **Answers-file отвергнут:** секреты в файле-вне-`.env` = новая поверхность утечки (NFR-3), + новый формат = новый канон под сопровождение; env-prefill покрывает кейс без этих издержек. + +### D11 — Onboarding: строго кирпич, plan→согласие→apply→verify, управляемый рестарт (OQ-6) + +- **Драйв субпроцессом** (прецедент `step_onboard` bootstrap_bundle), НЕ «печать готовой + команды»: печать разорвала бы «одну команду» на самом ценном шаге. Усиление против bundle — + **сначала `plan`**: вывод плана кирпича показывается пользователю → согласие → `apply` → + `verify` (FR-9/TC-22). +- **Запуск кирпича — host-venv** (канон ONBOARDING; кирпичу нужны httpx/pydantic): + идемпотентный ensure venv — паттерн `_ensure_venv` (probe импорта → `pip install -r + requirements.txt`); сам setup_lite от venv не зависит (stdlib-only). +- **Аргументы — детерминированная чистая функция** `build_onboard_args(answers, mode) -> list` + от собранных ответов (`--name/--description/--repo/--gitea-owner/--prefix/--stack/--test-cmd/ + --prod-port/--staging-port/--webhook-url` + `--env-file <корневой .env>` + `--json`) — + unit-тестируемо без сети (AC-10). Webhook-url строится от публичного URL оркестратора + (`https:///webhook/gitea`). +- **Трансляция исходов:** exit 2 кирпича (остались ручные шаги) → **MANUAL** итогового отчёта + (не успех); exit 1 → FAIL. `ORCH_PROJECTS_JSON` — из JSON-отчёта `apply` (строка + `instructions` `ORCH_PROJECTS_JSON=…` — фактический контракт кирпича) → запись в `.env` + с согласия. +- **Управляемый рестарт — только собственного свежеподнятого контура** (§10): проверка тихого + окна (`GET /queue` без running-job) → `docker compose up -d --force-recreate orchestrator` → + пост-проверка `/health`+`/queue`. Чужие/боевые контейнеры не трогаются (NFR-7; рестартуется + контур, поднятый шагом 8 этого же прогона). +- Собственная реализация статусов/лейблов/репо/webhook в setup_lite **запрещена** (BR-7; + 22 статуса остаются за `plane_sync._PLANE_NAME_TO_KEY` через кирпич; структурный тест — D12). + +### D12 — Документация и анти-дрейф: §1.1 в LITE_SETUP (пиннинг цел), новый тест-модуль (OQ-5, FR-11) + +- **Размещение быстрого пути — подраздел `### 1.1. Быстрый путь: setup_lite.py` внутри + `## 1. Рамка Lite`** (+ упоминание в шапке-цитате дока). Заголовки `## 1.`…`## 13.` не + меняются → пиннинг `test_doc_exists_with_all_13_sections_in_order` и нумерация ссылок + (§5.4, §13.7 и т.д. из других доков/скриптов) сохраняются **байт-в-байт**; ручной маршрут + §2–§13 — канон и fallback для MANUAL-шагов. Fenced-блок §1.1 (команда запуска + режимы) + обязан проходить существующие сканы дока: только плейсхолдеры, упоминаемые env-токены — + только существующие в `.env.example` (`_ENV_TOKEN_RE`-ассерт). Альтернативы отвергнуты: + `## 0`/14-й раздел/перенумерация — ломают пиннинг и внешние ссылки на §-номера без выгоды. +- **Footer-норматив LITE_SETUP расширяется** (FR-11): «меняешь шаги тиража → обнови этот док + **и `scripts/setup_lite.py`** в том же PR» (симметрично NFR-5 ORCH-102). +- **`tests/test_setup_lite_script.py`** (имя финализировано; по образцу + `test_bootstrap_script.py` + тест-план TC-01…TC-25): ast-скан stdlib-only и отсутствия + `src.*`; **зеркала обоих needle-наборов** (`FORBIDDEN_DELETE_NEEDLES` — delete-операций нет; + `FORBIDDEN_STATUS_NEEDLES` — собственного канона статусов нет); упоминание кирпичей + `gen_secrets.py`/`onboard_project.py`/`LITE_SETUP.md`; unit чистых функций (вердикты + предусловий, классификатор discovery, `port_overrides`, рендер env c маркером, builder + аргументов onboard, step-движок/resume, exit-контракт, дефолт-режим `apply`, закрытость + режимов); секрет-гигиена (инжектированный транскрипт без значений секретов); import без + side effects. +- **`tests/test_lite_setup_doc.py` — только аддитивно:** новый тест «LITE_SETUP.md упоминает + `setup_lite.py` (быстрый путь) и сохраняет ручной маршрут» (TC-27); существующие ассерты, + включая кортеж `SECTIONS`, **не правятся** (пиннинг структуры не меняется — D12 п.1). +- **Синхронно в том же PR** (правило агентов №2 / NFR-8): `CHANGELOG.md` (`feat:`); витрина + `docs/overview/README.md` (строка маршрута «Развернуть у себя» — упомянуть установочный + скрипт; `business.md` — при необходимости); паспорт `CLAUDE.md` (секция ORCH-104); + `docs/architecture/README.md` — обновлён на этой стадии (блок Type A дополнен инсталлером). + +### Что НЕ меняется + +`src/**`, корневой `docker-compose.yml`, `Dockerfile`, `.env.example`, `.env.watchdog.example`, +`scripts/gen_secrets.py`, `scripts/onboard_project.py`, `scripts/bootstrap_bundle.py`, +`deploy/**`, `onboarding/**`, промпты `.openclaw/agents/**`; `STAGE_TRANSITIONS`, состав +`QG_CHECKS`, семантика `check_*`, machine-verdict ключи, схема БД — **байт-в-байт** (BR-8/AC-13). +Kill-switch не вводится: активация — только явный запуск CLI человеком на целевом хосте +(паттерн ORCH-009/102/103); в нашем контуре артефакт инертен. Прод-контейнер в рамках задачи +не рестартуется (выкат — штатно: staging 8501 → `Confirm Deploy`). + +## Альтернативы + +- **`plan`-дефолт (строгое следование D5 ORCH-009/103)** — отвергнуто для этого инструмента: + воспроизводит порог входа, который задача убирает (бизнес-запрос «одна команда» — BRD §1.2); + безопасность дефолта достигается per-action consent + ранним guard + non-TTY exit 2 (D2), а + не выбором режима. `plan` сохранён как полноценный строгий read-only режим. +- **Отдельное имя режима `wizard`/`install`** — отвергнуто: новая лексика против выученной + семейной (`plan`/`apply`/`verify`); дешевле объяснить «apply интерактивен», чем вводить слово. +- **State-файл прогресса для resume** — отвергнуто: новый артефакт = поверхность + дрейфа/устаревания; check→ensure по реальности уже даёт resume (паттерн bootstrap); коллизия + с guard'ом `.env` решена маркером (D6) без состояния. +- **Answers-file для headless** — отвергнуто: секреты на диске вне `.env` (NFR-3), новый формат + под сопровождение; env-prefill с каноническими именами + `--yes` покрывает кейс (D10). +- **Безусловная автоматизация SQL-вставки webhook (как в bundle)** — отвергнуто: в Lite БД + Plane — чужая прод-инсталляция; без подтверждённого контейнера/согласия/пост-верификации + это R-3; UI-путь — рекомендация, SQL — офер (D8). +- **Только печать команд установки пакетов (никогда не исполнять)** — отвергнуто: ломает + бизнес-запрос п.3 («предлагает установить»); per-package consent + re-check дают тот же + уровень безопасности с лучшим UX (D4). +- **Discovery по именам контейнеров/compose-проектов** — отвергнуто: ложноположительные + срабатывания на чужих контейнерах (R-6); строго image-префиксы + выбор за пользователем (D5). +- **Новый раздел LITE_SETUP (`## 0`/14-й/перенумерация)** — отвергнуто: ломает пиннинг + «13 разделов в порядке» и внешние ссылки на §-номера; подраздел §1.1 даёт то же без издержек + (D12). +- **Опциональный файл-отчёт прогона** — отвергнуто (TRZ §9 оставлял на усмотрение): stdout + + exit-код покрывают наблюдаемость; файл с транскриптом — лишняя поверхность для случайного + попадания чувствительных значений. + +## Последствия + +- **+** Порог входа Lite падает с «прочитай 13 разделов и выполни ~30 команд» до «запусти один + файл и отвечай на вопросы»; ошибки конфигурации ловятся в момент ввода (немедленная + верификация), а не на smoke. +- **+** Нулевой дрейф канонов: статусы/секреты/onboarding — только кирпичами; маршрут — только + LITE_SETUP (ссылки из каждого шага); форма закреплена анти-дрейф тестами с зеркалами уже + существующих needle-наборов. +- **+** Рантайм байт-в-байт, наш прод недостижим по построению: ранний guard немаркированного + `.env` (живой хост), no-delete, никаких операций с `main`, рестарт — только собственного + свежеподнятого контура. +- **−** Дефолт `apply` отличается от plan-default семейства — осознанная цена бизнес-цели; + митигировано структурными гарантиями D2 (фаза 0 ≡ plan, consent, non-TTY exit 2) и явным + тест-ассертом с комментарием-обоснованием. +- **−** Двойной источник маршрута (док + скрипт) — поверхность дрейфа; митигировано нормативом + «док + скрипт в одном PR» (footer LITE_SETUP, adr-0040) и анти-дрейф тестами (кирпичи, + env-ключи ⊂ канона, упоминание скрипта в доке). +- **−** Оферы установки пакетов вмешиваются в систему заказчика — митигировано per-package + consent с печатью точной команды, закрытым набором менеджеров, re-check'ом и честным MANUAL. +- **−** Path Б мутирует чужую Plane-БД — митигировано пятью предусловиями D8 (docker + + подтверждённый контейнер + согласие с показом SQL + идемпотентный INSERT + пост-верификация) + и приоритетом UI-пути. +- **Откат:** удалить `scripts/setup_lite.py`, `tests/test_setup_lite_script.py`, вернуть §1.1 и + footer LITE_SETUP.md, аддитивный тест в `test_lite_setup_doc.py`, записи + CHANGELOG/CLAUDE.md/README/overview — состояние репо 1:1 (scripts+docs+tests, без миграций); + поведение платформы не меняется в обе стороны. + +## Ссылки + +- BRD: `docs/work-items/ORCH-104/01-brd.md` +- TRZ: `docs/work-items/ORCH-104/02-trz.md` (OQ-1…OQ-6 → D1…D12) +- Acceptance: `docs/work-items/ORCH-104/03-acceptance-criteria.md`; тест-план: `04-test-plan.yaml` +- Сквозной ADR: `docs/architecture/adr/adr-0040-lite-interactive-installer.md` +- Риски: `docs/work-items/ORCH-104/10-tech-risks.md`; инфра: `07-infra-requirements.md` +- Предшественники: adr-0037 (ORCH-102 — канон LITE_SETUP/13 разделов), adr-0038 (ORCH-103 — + bootstrap-паттерн: режимы/step-движок/manual-checkpoint/no-delete), adr-0036 (ORCH-101 — + «дефолт=боевое», gen_secrets, guard ORCH-058), adr-0035 (ORCH-009 — onboarding-CLI, D10 + branch protection) +- Сверено по коду/репо: `scripts/bootstrap_bundle.py` (`manual_checkpoint(max_tries=3)`, + `render_env`/`parse_env`, `_ensure_venv`, `_psql` stdin, `step_onboard` → `instructions` + `ORCH_PROJECTS_JSON=`, `APPLY_STEPS == build_plan()`), `scripts/gen_secrets.py` (`--write` + x-mode/exit 2/`--force`, token_hex(32)), `scripts/onboard_project.py` (CLI: `plan|apply|verify`, + обязательные флаги, `--env-file`/`--json`), `tests/test_lite_setup_doc.py` (кортеж `SECTIONS`, + `_ENV_TOKEN_RE`, FORBIDDEN-импорт, секрет-эвристика, «команды только в SECTIONS[1:]»), + `tests/test_bootstrap_script.py` (`STDLIB_ALLOWED`, `FORBIDDEN_DELETE_NEEDLES`, + `FORBIDDEN_STATUS_NEEDLES`), `docker-compose.yml` (`network_mode: host` ×3), + `docs/deployment/LITE_SETUP.md` (§2–§13), `.env.example` (канон имён ключей) diff --git a/docs/work-items/ORCH-104/07-infra-requirements.md b/docs/work-items/ORCH-104/07-infra-requirements.md new file mode 100644 index 0000000..3e35a28 --- /dev/null +++ b/docs/work-items/ORCH-104/07-infra-requirements.md @@ -0,0 +1,65 @@ +--- +work_item: ORCH-104 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-11 +model_used: claude-opus-4-8 +--- + +# 07 — Инфра-требования: ORCH-104 — Установочный скрипт Lite-тиража (интерактивный installer) + +Work Item: **ORCH-104** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable: топология **нашего** прода не меняется — файл создан для +> аудитопригодности с явными `N/A` и фиксацией предусловий **целевого хоста заказчика** +> (на котором скрипт исполняется). Решения — `06-adr/ADR-001-setup-lite-interactive-installer.md`. + +## I-1. Топология / окружения + +- **Наш прод/staging (mva154):** `N/A` — задача scripts+docs+tests; контейнеры, порты, сеть, + тома, compose-файлы не меняются. В нашем контуре скрипт инертен (активация — только явный + запуск человеком; запуска на mva154 не предполагается вовсе). +- **Целевой хост заказчика (где скрипт работает):** контур Lite без изменений — Linux x86_64, + Docker Engine + Compose v2, git, python3, node (LITE_SETUP §2). Скрипт сам **сканирует** эти + предусловия и предлагает доустановить недостающее (per-package consent, ADR D4); результат + установки — ровно базовый Lite-контур: `orchestrator` + `orchestrator-watchdog` + (`orchestrator-staging` НЕ поднимается — строго за `profiles: [staging]`). +- **Discovery-предположение:** Plane/Gitea заказчика обнаруживаются только как + docker-инсталляции локального хоста (image-префиксы `makeplane/*`, `gitea/gitea*`, + `docker.gitea.com/gitea*` — ADR D5); native/k8s-инсталляции — честный ручной ввод URL + (не FAIL). Сетевая достижимость Plane/Gitea с хоста оркестратора — предусловие заказчика + (BRD §6). + +## I-2. Переменные окружения / секреты + +- **Новых ключей `Settings`/`.env.example`/`.env.watchdog.example` НЕТ** (TRZ §8) — каноны + только читаются как шаблоны рендера. +- Скрипт **пишет** целевые `.env`/`.env.watchdog` на хосте заказчика: от канонов-example, + права `600`, первая строка — маркер managed-файла `# managed by scripts/setup_lite.py + (ORCH-104)` (ADR D6: немаркированный существующий файл → отказ exit 2 без `--force`). +- Секреты: webhook-секреты — только свежий выпуск кирпичом `gen_secrets.py` (субпроцесс); + внешние токены (Plane/Gitea/Telegram ×2) — скрытый ввод оператора с немедленной верификацией; + значения нигде не печатаются и не логируются (NFR-3). Боевые секреты исходного хоста не + используются даже как подсказки-дефолты (stateless, LITE_SETUP §12). +- Headless-prefill: переменные окружения процесса с каноническими именами ключей + явный + `--yes` (ADR D10); answers-file не вводится. + +## I-3. Деплой / рестарт + +- **Рестарт нашего прод-контейнера НЕ требуется и не выполняется** (self-hosting инвариант): + изменение — файлы `scripts/` + `tests/` + docs; выкат задачи — штатный конвейер + (deploy-staging 8501 → `Confirm Deploy`). +- На **хосте заказчика** скрипт выполняет `docker compose up -d --build` и управляемый + `--force-recreate orchestrator` **только собственного свежеподнятого** контура, после + проверки тихого окна `GET /queue` (ADR D11); чужие/уже бегущие контейнеры не трогаются без + согласия (NFR-7); delete-операций нет вообще. + +## I-4. CI/CD + +- `.gitea/workflows/` — **без изменений**. Новый структурный/unit тест-модуль + `tests/test_setup_lite_script.py` + аддитивный тест в `tests/test_lite_setup_doc.py` + попадают в существующий прогон `pytest tests/ -q` (детерминированы: без TTY/сети/docker — + инжектируемый I/O, моки, tmp_path; NFR-5). +- Инфра-предусловий прод-образа нет (скрипт не входит в рантайм-контейнер как зависимость; + pip-зависимости не добавляются — stdlib-only). 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..48baae1 --- /dev/null +++ b/docs/work-items/ORCH-104/10-tech-risks.md @@ -0,0 +1,47 @@ +--- +work_item: ORCH-104 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-11 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-104 — Установочный скрипт Lite-тиража (интерактивный installer) + +Work Item: **ORCH-104** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Риски реализации и эксплуатации интерактивного +> installer'а; решения — `06-adr/ADR-001-setup-lite-interactive-installer.md` (D1…D12). +> Развивает реестр BRD §8 (R-1…R-6). + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | **Случайный запуск на хосте с живым продом** (R-1): дефолт-режим — `apply`-wizard, не `plan` | Низ. | Выс. | Структурные гарантии D2: фаза 0 `apply` ≡ `plan` (read-only), ранний guard НЕмаркированного `.env` → exit 2 ДО первого вопроса (D6), per-action consent с дефолт-ответом «нет», non-TTY → exit 2 до мутаций (D10); `--force` требует явного набора + согласия; на нашем mva154 запуск не предполагается вовсе | +| TR-2 | **Оферы установки пакетов** (R-2) — вмешательство в систему заказчика; нативные репо могут дать не то (compose v1, старый docker) | Сред. | Сред. | Per-package consent с печатью точной команды ДО исполнения; закрытый набор менеджеров apt-get/dnf/yum/zypper; **re-check фактом после установки** (в т.ч. `docker compose version` → v2), не сошлось → MANUAL с официальной инструкцией; нет менеджера/sudo/отказ → честный MANUAL (D4) | +| TR-3 | **SQL-вставка webhook в чужую Plane-БД** (R-3): инвазивная мутация прод-инсталляции заказчика; пользовательский ввод в SQL | Сред. | Выс. | Path A (UI) — дефолт-рекомендация; Path Б — только при 5 предусловиях D8 (docker-Plane + подтверждённый пользователем postgres-контейнер + согласие с показом SQL + идемпотентный INSERT + обязательная пост-верификация SELECT); только INSERT/SELECT (no UPDATE/DELETE); секрет/пароль — stdin/env, не argv; валидация slug `^[a-z0-9-]+$` (анти-инъекция); любой сбой → fail-safe в Path A | +| TR-4 | **Дрейф скрипта от канона LITE_SETUP** (R-4): два источника маршрута расходятся | Сред. | Сред. | Канон-знания не дублируются (кирпичи субпроцессами, статусы — за `plane_sync` через onboarding, smoke — ссылкой на §11); footer-норматив «док + скрипт в одном PR» (FR-11/adr-0040); анти-дрейф: упоминание скрипта в доке, env-ключи ⊂ `.env.example`, зеркала needle-наборов (D12) | +| TR-5 | **Зависание/недетерминизм в non-TTY** (R-5): CI/обвязка заказчика виснет на ожидании ввода | Низ. | Сред. | `isatty`-гейт: `apply` без `--yes` в non-TTY → немедленный exit 2 с подсказкой; headless — только env-prefill + явный `--yes`; `plan`/`verify` — полноценные non-TTY режимы; весь интерактив за инжектируемым I/O (детерминированные тесты TC-12) (D10) | +| TR-6 | **Ложноположительный discovery** (R-6): чужой контейнер опознан как Plane/Gitea; неверный URL уезжает в конфиг | Низ. | Сред. | Опознание строго по image-префиксам (не по именам); выбор всегда за пользователем + пункт «ввести вручную» всегда; токен-верификация выбранного URL обязательна (FR-4) — неверный кандидат не пройдёт `collect` (D5/D9) | +| TR-7 | **Утечка секретов** через транскрипт wizard'а / отчёт / файлы | Низ. | Выс. | Скрытый ввод (getpass-класс); значения не печатаются и не логируются (только имена ключей); итоговая таблица без значений; live-файлы `600`; файл-отчёт прогона сознательно НЕ вводится; анти-дрейф: тест транскрипта (TC-11) + секрет-эвристика на fenced-блоках дока (D3/D6/D10) | +| TR-8 | **Коллизия resume ⟷ guard существующего `.env`**: после manual-step повторный запуск отбивается собственным артефактом ЛИБО guard ослабляется и перетирает чужой конфиг | Сред. | Выс. | Маркер managed-файла (первая строка): без маркера → отказ exit 2 (чужой конфиг), с маркером → resume-ensure без перетирания значений (D6); семантика покрыта unit-тестами (TC-03/TC-14) | +| TR-9 | **Разнообразие хостов**: экзотический дистрибутив/пакетный менеджер/нестандартный docker — скан даёт неверный вердикт | Сред. | Низ. | Вердикты — чистые функции от фактов (тестируемы на фикстурах); неопределимое → честный MANUAL с generic-командами и ссылкой на § LITE_SETUP (никогда не падение); не-Linux/не-x86_64 → WARN «вне контура» (рамка Lite не расширяется) (D4) | +| TR-10 | **Дрейф интерфейсов кирпичей** (`gen_secrets.py`/`onboard_project.py`): смена флагов/формата отчёта молча ломает installer | Низ. | Сред. | Контракты кирпичей закреплены их собственными тестами (`test_secrets_gen`, onboarding-тесты); builder аргументов — чистая функция под unit-тестом (ломается громко в CI); exit-коды кирпичей транслируются честно (2 → MANUAL, 1 → FAIL) (D11); норматив same-PR симметричен | + +## Сводный вывод + +Доминирующий класс — **эксплуатационные риски целевого хоста заказчика** (TR-2/TR-3/TR-9): +наш прод они не затрагивают и гасятся per-action consent'ом, контрактом manual-checkpoint, +честными MANUAL-деградациями и обязательной верификацией каждого ввода. Специфика ORCH-104 +против предшественников — два новых внимательных места: **дефолт-режим `apply`** (TR-1; закрыт +структурными гарантиями D2, а не дисциплиной) и **маркер managed-файла** (TR-8; единственный +механизм, примиряющий идемпотентный resume с защитой чужого `.env`). + +Рисков для прод-конвейера self-hosting **нет по построению** (BR-8: рантайм байт-в-байт, +kill-switch не нужен — активация только явным запуском оператора; полный регресс и все +существующие анти-дрейф тесты остаются зелёными, пиннинг «13 разделов» LITE_SETUP не меняется). +Эскалация `arch:major-change` не требуется: новых стадий/компонентов рантайма/смены БД нет — +задача целиком в слое дистрибуции (паттерн ORCH-101/102/103). Возврат в анализ не требуется: +ТЗ выполнимо без нарушения принципов архитектуры.