--- verdict: APPROVED work_item: ORCH-103 stage: review author_agent: reviewer status: approved created_at: 2026-06-11 model_used: claude-opus-4-8 type: review work_item_id: ORCH-103 version: 1 --- # Review ORCH-103 — ORCH-10b Bundled-тираж: весь стек одним комплектом + bootstrap-скрипт > Машинный вердикт читается ТОЛЬКО из `verdict:` во frontmatter. ## Summary PR закрывает Type B эпика ORCH-10 строго по ТЗ и ADR-001 (D1–D11): новый каталог `deploy/bundled/` (самодостаточный compose 16 сервисов, project name `orchestrator-bundle`, пиннинг неподвижными тегами, одна bridge-сеть, только человеческие порты, мина `GITEA__webhook__ALLOWED_HOST_LIST` закрыта), `scripts/bootstrap_bundle.py` (stdlib-only, `plan`-дефолт/`apply`/`verify`, step-движок check→ensure, exit 0/2/1, ноль delete-операций), конфиг-канон `deploy/bundled/.env.example` (ни одного дефолтного пароля), `docs/deployment/BUNDLED_SETUP.md` (14 разделов канона D10) и три содержательных анти-дрейф тест-модуля. Рантайм байт-в-байт: `git diff main` не содержит `src/**`, корневого `docker-compose.yml`, `Dockerfile`, `.gitea/workflows/**` (AC-6 подтверждён diff-stat'ом). Полный регресс: **`pytest tests/ -q` → 1844 passed, 0 failed** (AC-5); существующие анти-дрейф тесты (`test_lite_setup_doc.py`, `test_no_host_hardcodes.py`, канон ORCH-009) не правились. Документация обновлена в том же PR по всем точкам §8 ТЗ. Findings — только P2/P3, блокеров нет → **APPROVED**. ## Оси проверки ### 1. Соответствие ТЗ (02-trz.md, 03-acceptance-criteria.md) - **FR-1** ✅ — отдельный bundle-compose; состав = ADR D1/D3 (тест `test_bundle_has_exactly_the_adr_service_set` фиксирует множество); пиннинг всех сторонних образов литералом (TC-03); тома — именованные с префиксом проекта + bind строго внутри project dir (TC-04); достижимость в обе стороны — сервис-DNS (D4) + `ALLOWED_HOST_LIST`; карта портов в доке §2, конфликт порта → отказ preflight; staging-контура нет вовсе. - **FR-2** ✅ — последовательность шагов 1–9 ТЗ воспроизведена 1:1 в `APPLY_STEPS` (тест `test_apply_steps_match_normative_plan` держит соответствие нормативному плану); идемпотентность — check→ensure/skip (AC-8 покрыт unit'ами resume/«противоречивое состояние»); exit-контракт 0/2/1 (`test_exit_code_contract`); манифест manual-step честный: инструкция → подтверждение → API-верификация, без TTY — немедленный exit 2. - **FR-3** ✅ — webhook-секреты строго субпроцессом `gen_secrets.py` (структурный тест); bundle-креды — stdlib `secrets` (token_hex ≥16 байт, unit проверяет длину); в репо только пустые плейсхолдеры (`test_bundle_secrets_in_example_are_empty_placeholders`); права 600; без перетирания без `--force-secrets` (`test_merge_missing_secrets_never_overwrites_without_force`). - **FR-4** ✅ — BUNDLED_SETUP.md: все 14 разделов в порядке маршрута, fenced-команды + «Проверка:»/PASS/FAIL в каждом исполняемом разделе, общие шаги ссылками на LITE_SETUP §5–§8 / ONBOARDING / REPLICATION §4 (форк канона отсутствует), fail-closed имена `Confirm Deploy`/`STOP` и «22 статуса» — сверкой импорта `plane_sync._PLANE_NAME_TO_KEY`. - **FR-5** ✅ — три модуля без docker/сети/LLM; FORBIDDEN — импорт из `test_no_host_hardcodes.py` (один источник истины); секрет-эвристика hex≥32/alnum≥40 с негативным самочеком (не-evergreen); key-set-sync `${VAR}` ⊆ bundle-канона; заморозка корневого compose зеркалом ассерта ORCH-102. - **FR-6** ✅ — smoke = REPLICATION §4 поверх bundle (§11 дока, без форка), минимальный сигнал «артефакты 01–04 в ветке» зафиксирован. - **AC-матрица:** AC-4…AC-9 в файловой/структурной части — PASS (TC-01…TC-12 зелёные). AC-1/AC-2/AC-3/AC-8(повторный прогон) в e2e-части — **ручная приёмка** на чистом хосте/VM по рамке самих AC (в CI docker/LLM не гоняются) — остаётся за стадией приёмки, см. P2-2. ### 2. Соответствие ADR (06-adr/ADR-001, adr-0038) - D1–D11 реализованы без отклонений; сквозной `adr-0038-bundled-replication-canon.md` заведён. - **Прогрессивная автоматизация webhook (D7)** — единственное место, где реализация глубже буквы ADR: `step_plane_webhook` регистрирует workspace-webhook прямой записью в Postgres инсталляции. Сверено: это **не новый канон, а переиспользование** документированного «пути Б» LITE_SETUP §5.4 (тот же INSERT-контракт, колонка-в-колонку), контракт чекпоинта сохранён (верификация SELECT'ом; при отказе — fallback на честный manual-step с той же проверкой), схема стабильна благодаря пину `v0.23.1`. D7 явно разрешает такую замену manual-step → ensure «без правки ADR». Нарушения нет; в доке §7 чекпоинт 4 описан честно. - **Трассировка (TRACEABILITY):** правка чужого маркированного блока одна — строка Type B в `REPLICATION.md` §1 (артефакт ORCH-101); это запланированная точка расширения, прямо предписанная ТЗ FR-6/§8 — инвариант ORCH-101 не сломан. Остальные изменения аддитивны (новые файлы / новые секции CLAUDE.md, README архитектуры, CHANGELOG). - Нормативы предшественников соблюдены: branch protection НЕ настраивается (D10 ORCH-009 / INV-4 — явно в D6 и §14.6 дока), compose не форкается (adr-0037), «дефолт = боевое» не нарушен (корневой `.env.example` не тронут). ### 3. Качество кода - `pytest tests/ -q`: **1844 passed, 0 failed** (66s); новые тесты содержательные — unit'ы чистых функций покрывают позитив/негатив/resume/противоречивое состояние, эвристики имеют негативный самочек, ast-скан stdlib-allowlist реально закрыт. - Бизнес-логика скрипта аккуратная: never-print секретов в stdout (только имена ключей), маскированный лог при падении clone (токен в URL не утекает в вывод), `_psql` через stdin (секрет не в argv), fail-fast preflight до любых мутаций, диагностика «кто не дождался + хвост логов». Не багфикс-трек (feature) — требование регресс-теста-фиксатора BR-4/ORCH-019 неприменимо. - Замечания P2/P3 — ниже; ни одно не ломает инварианты конвейера и не относится к рантайму платформы (скрипт исполняется только оператором на целевом хосте). ### 4. Документация — обязательная проверка Выполнена явно, см. раздел «Документация» ниже. Обновлено всё требуемое в том же PR. ## Findings ### P0 — Blocker - (нет) ### P1 — Must fix - (нет) ### P2 — Should fix - [ ] **P2-1. Секреты в argv субпроцессов** (`scripts/bootstrap_bundle.py`): `step_init_gitea` передаёт `GITEA_ADMIN_PASSWORD` аргументом `--password` в `docker compose exec gitea gitea admin user create …`, а `step_agent_git` — токен в clone-URL аргументом `git clone http://oauth2:@…`. Значения видимы в `ps` хоста на время исполнения. Формально AC-8 («секрет виден в stdout/логе») не нарушен, но это против духа NFR-3 (ТЗ FR-2) и непоследовательно с собственной argv-гигиеной скрипта (`_psql` прогоняет секреты через stdin с явным комментарием «секреты не попадают в argv, NFR-3»). Рекомендация: для clone — использовать `git -c credential.helper`/`GIT_ASKPASS` либо дописать компромисс в TR-7 (10-tech-risks) и шапку скрипта; для `gitea admin user create` альтернатив CLI мало — минимум зафиксировать окно экспозиции в TR-7. Не блокер: разовая операция оператора на одноарендном целевом хосте, угроза-модель совпадает с уже зафиксированным компромиссом TR-7. - [ ] **P2-2. Замер цифр «Требований к хосту» отложен на приёмку** (AC-4): BUNDLED_SETUP §2 декларирует «подтверждаются замером приёмочного развёртывания» — на момент ревью цифры (8 GB / 40 GB / 4 vCPU) синхронизированы с константами preflight структурным тестом, но фактический замер ещё не зафиксирован. По рамке 03-acceptance-criteria (e2e — ручная приёмка вне CI) это допустимо, однако при ручной приёмке AC-1/AC-2 результат замера нужно зафиксировать (13-test-report / 15-staging-log или правка §2), иначе FAIL-условие AC-4 «цифры с потолка» останется формально незакрытым. ### P3 — Nice to have - [ ] **P3-1.** `step_plane_webhook`: `slug`/`secret` интерполируются в SQL-строку без экранирования одинарных кавычек. Секрет — hex от `gen_secrets` (безопасен), slug — операторский ввод; кавычка в slug уронит INSERT. Риск минимален (ON_ERROR_STOP + fail-safe fallback на manual-step), но дешёвое `value.replace("'", "''")` сняло бы класс целиком. - [ ] **P3-2.** `run_verify`: при одновременном health-FAIL и onboard `exit 2` функция возвращает `EXIT_MANUAL` (2), маскируя ошибку (ожидался бы приоритет `1`). Поведенческая мелочь read-only режима. ## Документация | Артефакт | Статус | |----------|--------| | `CLAUDE.md` | ✅ новый раздел «Bundled-тираж (ORCH-103)» (паттерн ORCH-101/102) | | `docs/architecture/README.md` | ✅ блок Type B — Bundled рядом с 10-common/Lite | | `CHANGELOG.md` | ✅ запись `feat: ORCH-103` (детальная, D1–D11) | | `docs/operations/REPLICATION.md` §1 | ✅ Type B → ✅ ORCH-103 + ссылка на BUNDLED_SETUP.md | | `docs/deployment/BUNDLED_SETUP.md` | ✅ создан, 14 разделов канона D10, держится тестом | | ADR | ✅ work-item `06-adr/ADR-001-…` + сквозной `adr-0038-bundled-replication-canon.md` | | `07-infra-requirements.md`, `10-tech-risks.md` (TR-1…TR-9) | ✅ заведены архитектором, кросс-рефы из кода сходятся | | `.gitignore` | ✅ `deploy/bundled/repos/` (NFR-3) | | `README.md` «Известные ограничения» (ORCH-079) | ✅ проверено явно: PR не закрывает ни один из 3 открытых пунктов (Telegram 48h / intra-repo deps / пакетный автоном) — обновление витрины не требуется | `src/**` не изменён (PR — deploy/scripts/docs/tests), поэтому P0-правило «`src/` изменён без документации» неприменимо; документация при этом обновлена полностью. ## Итог `verdict: APPROVED` — P0/P1 отсутствуют; P2-1/P2-2 и P3 рекомендуется снять follow-up'ом или при ручной приёмке Bundled-развёртывания (AC-1/AC-2/AC-3/AC-8 e2e-часть).