From 44db94e462f7ec97542750cbe0453548a918ba42 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 7 Jun 2026 17:03:11 +0000 Subject: [PATCH] architect(ET): auto-commit from architect run_id=327 --- docs/architecture/README.md | 36 ++- .../adr/adr-0012-security-gate.md | 63 +++++ .../ORCH-022/06-adr/ADR-001-security-gate.md | 235 ++++++++++++++++++ .../ORCH-022/07-infra-requirements.md | 56 +++++ .../ORCH-022/08-data-requirements.md | 26 ++ docs/work-items/ORCH-022/10-tech-risks.md | 16 ++ 6 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 docs/architecture/adr/adr-0012-security-gate.md create mode 100644 docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md create mode 100644 docs/work-items/ORCH-022/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-022/08-data-requirements.md create mode 100644 docs/work-items/ORCH-022/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 0b1d743..6ec3974 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -36,7 +36,7 @@ created → analysis → architecture → development → review → testing → | deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) | | done | — | — | — | -**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058). +**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043), check_staging_image_fresh (ORCH-058), check_security_gate (ORCH-022 — design). **Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. @@ -155,6 +155,38 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION` образа, без миграций). Подробнее: [adr-0008](adr/adr-0008-staging-image-provenance.md), детально — `docs/work-items/ORCH-058/06-adr/ADR-001-staging-image-provenance.md`. +### Security-гейт: secret-scanning + dependency audit перед мержем (ORCH-022 — design) +Автономный конвейер вливал ветку в `main` без проверки на утёкший секрет (ключ/токен/пароль/ +приватный ключ) и уязвимую зависимость (CVE); для self-hosting один секрет/CVE через одну +задачу уезжал в общий прод всех проектов (CLAUDE.md §8). ORCH-022 вводит детерминированный +(без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, рядом с merge-gate +(ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди edge-под-гейтов +(ДО merge-gate). Паттерн соседей: leaf `src/security_gate.py` (never-raise) + тонкая обёртка +`check_security_gate` в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`. +`STAGE_TRANSITIONS` и схема БД — **без изменений**. +- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне + аллоулиста `.gitleaks.toml` → вклад в FAIL. Offline → гарантия «секрет всегда блокирует» + не зависит от сети (безусловна). +- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity` + (дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open + + громкий warning** (анти-петля ORCH-061; флаг `security_dep_audit_fail_closed` для строгого + режима). best-effort при доступности фида. +- **ПЕРВЫМ, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки ДО rebase + не «обвиняет» задачу в CVE из обновившегося `main`; до захвата merge-lease → при FAIL lease + освобождать не нужно. +- **Артефакт `17-security-report.md`** (YAML-frontmatter `security_status`/`secrets_found`/ + `deps_blocking`/`deps_warning`/`deps_audit_degraded`); вердикт читается ТОЛЬКО из + frontmatter (гейт пишет → читает обратно через `parse_security_status` → возвращает: единый + источник истины), negative-токен авторитетен, битый/нет → fail-closed. +- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3, + затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046). +- **Условность как ORCH-35/43/58:** `security_gate_enabled` + `security_gate_repos` (пусто → + только self-hosting); never-raise; таймаут `security_scan_timeout_s`; гейт не деплоит/не + рестартит прод. v1 — Python-only; SAST/мульти-стек — follow-up (BR-14). + +Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально — +`docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`. + ### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано) Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде, нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча @@ -306,4 +338,4 @@ ORCH-065 вводит фоновый watchdog, чтобы смерть проц Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md). --- -*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест).* +*Актуально на 2026-06-07. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. Статусы доработок: ORCH-036 (исполняемый самодеплой `deploy`, adr-0007) — реализовано; ORCH-043 (merge-gate, adr-0006) — design, ветка feature/ORCH-043; ORCH-053 (reconciler, adr-0007, src/reconciler.py) — реализовано; ORCH-060 (F-1 skip escalated/Blocked/Needs-Input, `docs/work-items/ORCH-060/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-060 (Guard 1 `developer_retry_count>=MAX_DEVELOPER_RETRIES` + Guard 2 `plane_sync.fetch_issue_state` Blocked/Needs-Input, флаг `ORCH_RECONCILE_SKIP_BLOCKED_ENABLED`); ORCH-058 (провенанс staging-образа: check_staging_image_fresh + staging_check свежего образа + хук-guard, adr-0008) — реализовано в ветке feature/ORCH-058 (обновлять также при изменении src/image_freshness.py, scripts/orchestrator-deploy-hook.sh, Dockerfile); ORCH-061 (толерантность staging-вердикта к инфра-FAIL C9a/C9b, adr-0009, `docs/work-items/ORCH-061/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-061 (обновлять также при изменении src/staging_verdict.py, scripts/staging_check.py, флаг staging_infra_tolerance_enabled); ORCH-021 (post-deploy наблюдение прода + реакция на деградацию, adr-0010, `docs/work-items/ORCH-021/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-021-post-deploy-rollback (reserved-agent job `post-deploy-monitor`: арм в src/stage_engine.py блок `next_stage == "done"`, тик `run_post_deploy_monitor` + перехват в src/agents/launcher.py ДО _spawn; чистая логика src/post_deploy.py never-raise; флаги `post_deploy_*` в src/config.py; блок `post_deploy` в `/queue`; артефакт 16-post-deploy-log.md; self-hosting всегда ALERT_ONLY — тик не рестартит прод; обновлять также при изменении src/post_deploy.py / арм-блока / launcher-перехвата); ORCH-065 (job-reaper + проактивный реклейм merge-lease + идемпотентная финализация merge, adr-0011, `docs/work-items/ORCH-065/06-adr/ADR-001`) — реализовано в ветке feature/ORCH-065 (новый daemon-поток src/job_reaper.py + старт/стоп в src/main.py lifespan; колонка `jobs.pid` через _ensure_column + проставление в src/agents/launcher.py `_spawn`; функции реклейма lease `pid_alive`/`reclaim_stale_lease` + guard `pr_already_merged` в src/merge_gate.py (консультируется merge-актором — промпт `.openclaw/agents/deployer.md`); флаги `reaper_*`/`lease_reclaim_*` в src/config.py; блок `reaper` в `/queue`; обновлять также при изменении этих мест); ORCH-022 (security-гейт: secret-scanning gitleaks + dependency audit pip-audit как под-гейт ребра `deploy-staging → deploy` ПЕРВЫМ, adr-0012, `docs/work-items/ORCH-022/06-adr/ADR-001`) — **design**, ветка feature/ORCH-022-security-secret-scanning (при реализации: новый leaf src/security_gate.py never-raise + check_security_gate в src/qg/checks.py `QG_CHECKS` + врезка _handle_security_gate в src/stage_engine.py блок `current_stage == "deploy-staging"` ПЕРВОЙ; флаги `security_*` в src/config.py; gitleaks в Dockerfile, pip-audit в requirements.txt, `.gitleaks.toml` в корне; артефакт 17-security-report.md; обновлять также при изменении этих мест).* diff --git a/docs/architecture/adr/adr-0012-security-gate.md b/docs/architecture/adr/adr-0012-security-gate.md new file mode 100644 index 0000000..048ad32 --- /dev/null +++ b/docs/architecture/adr/adr-0012-security-gate.md @@ -0,0 +1,63 @@ +# adr-0012: Security-гейт — secret-scanning + dependency audit перед мержем + +- **Статус:** proposed +- **Дата:** 2026-06-07 +- **Задача:** ORCH-022 +- **Детальный ADR:** `docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md` + +## Контекст +Оркестратор автономен: `developer` пишет код без человека-фильтра. Перед слиянием ветки в +`main` нет проверки на утёкший секрет (ключ/токен/пароль/приватный ключ) и уязвимую +зависимость (CVE). Для self-hosting один общий прод-инстанс обслуживает все проекты с общей +БД — секрет/CVE через одну задачу попадает в прод всех (CLAUDE.md §self-hosting, §8). Фактический +мерж PR в `main` делает `deployer` в начале стадии `deploy`. + +## Решение +Детерминированный (без LLM) **security-гейт как под-гейт ребра `deploy-staging → deploy`**, +рядом с merge-gate (ORCH-043) и image-freshness (ORCH-058), исполняемый **ПЕРВЫМ** среди +edge-под-гейтов (ДО merge-gate). `STAGE_TRANSITIONS` не меняется; в `QG_CHECKS` добавлен +`check_security_gate`. Паттерн — как у соседей: leaf-модуль `src/security_gate.py` +(never-raise) + тонкая обёртка в `QG_CHECKS` + врезка `_handle_security_gate` в `advance_stage`. + +- **Secret-scanning (`gitleaks`, offline):** скан `origin/main..HEAD`; любой секрет вне + аллоулиста (`.gitleaks.toml`) → вклад в FAIL. Offline → гарантия «секрет всегда блокирует» + не зависит от сети. +- **Dependency audit (`pip-audit`, OSV/PyPI):** severity ≥ `security_dep_block_severity` + (дефолт `HIGH`) → FAIL; ниже / UNKNOWN → warning. Недоступность фида → **fail-open + + громкий warning** (анти-петля; флаг `security_dep_audit_fail_closed` для строгого режима). +- **ПЕРВЫМ на ребре, ДО merge-gate:** дёшево фейлить до дорогих rebase/rebuild; скан ветки + ДО rebase не «обвиняет» задачу в CVE, притащенной обновившимся `main` (анти-петля + ORCH-061); до захвата merge-lease → при FAIL lease освобождать не нужно. +- **Артефакт `17-security-report.md`** с YAML-frontmatter (`security_status`, + `secrets_found`, `deps_blocking`, `deps_warning`, `deps_audit_degraded`); вердикт читается + ТОЛЬКО из frontmatter (канон), negative-токен авторитетен; битый/нет → fail-closed. +- **FAIL → откат на `development`** + developer-retry (общий `_developer_retry_count`, cap 3, + затем `set_issue_blocked` + Telegram); `task_desc` несёт дословные находки (ORCH-046). +- **Условность (как ORCH-35/43/58):** `security_gate_enabled` + `security_gate_repos`; пусто + → реально только self-hosting (`orchestrator`), прочие репо — no-op pass. +- **never-raise**, таймаут `security_scan_timeout_s`, гейт не деплоит/не рестартит прод. + +## Альтернативы +- **Вариант R (review-стадия):** diff может разойтись с мержем в `main`; merge-edge — последняя + страховка. Отклонено. +- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо + выражаются статусом коммита; коуплинг с раннером. Отклонено для v1 (точка расширения). +- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как в ORCH-043). + Отклонено. +- **fail-closed dep-audit / аудит после rebase:** ложные откаты → петля. Отклонено. +- **Новая колонка retry в БД:** не нужна (переиспользуем `_developer_retry_count`). + +## Последствия +- Класс «тихо влитый секрет/CVE» закрыт: секреты — безусловно (offline), CVE — best-effort при + доступности фида. Самоприменение CLAUDE.md §8 без человека. +- Плата: ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); внешние инструменты + (gitleaks в образе, pip-audit в зависимостях); время скана на каждом прогоне (ограничено + таймаутом); v1 — Python-only (SAST/мульти-стек — follow-up WI). +- Сквозное изменение (новый QG + edge-под-гейт) → `arch:major-change`; прод-деплой ORCH-022 — + строго через staging-гейт (8501), без рестарта прод-контейнера. + +## Связи +adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness — +условность/never-raise/fail-closed), adr-0003 (условный гейт / `is_self_hosting_repo`), +adr-0009 (анти-петля ложных FAIL, ORCH-061), ORCH-046 (дословный reason в `task_desc`), +ORCH-9/15 (мульти-стек — будущая зависимость), ORCH-2 (worktree-изоляция). diff --git a/docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md b/docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md new file mode 100644 index 0000000..d38dfff --- /dev/null +++ b/docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md @@ -0,0 +1,235 @@ +# ADR-001: Security-гейт — secret-scanning + dependency audit перед мержем + +- **Статус:** Accepted (proposed → принято архитектором ORCH-022) +- **Дата:** 2026-06-07 +- **Задача:** ORCH-022 +- **Связанный global ADR:** `docs/architecture/adr/adr-0012-security-gate.md` +- **Источники:** `01-brd.md` (BR-1..BR-14), `02-trz.md` (FR-1..FR-7, §4 варианты, §7 инварианты), + `03-acceptance-criteria.md` (AC-1..AC-21). + +--- + +## Контекст + +Оркестратор автономен: `developer`-агент пишет код без человека-фильтра. Перед слиянием +ветки задачи в `main` нет автоматической проверки на утёкший секрет (ключ/токен/пароль/ +приватный ключ) и на уязвимую зависимость (известная CVE). Для self-hosting это особенно +опасно: один общий прод-инстанс обслуживает все проекты с общей БД — секрет или CVE, +просочившийся через одну задачу, попадает в прод всех проектов (CLAUDE.md §self-hosting, §8). + +Конвейер уже содержит линию детерминированных страховок на ребре `deploy-staging → deploy` +(непосредственно перед фактическим мержем PR в `main`, который делает `deployer` в начале +стадии `deploy`): + +- **merge-gate** (ORCH-043, `check_branch_mergeable`) — догон `main` + re-test + сериализация; +- **image-freshness** (ORCH-058, `check_staging_image_fresh`) — провенанс staging-образа. + +Оба построены по одному паттерну: **leaf-модуль чистой логики (never-raise) + тонкая обёртка +в `QG_CHECKS` + врезка-обработчик `_handle_*` в `advance_stage`**, с условным раскатом +(`*_enabled` + `*_repos`, реально только для self-hosting при пустом scope) и откатом на +`development` с developer-retry (cap `MAX_DEVELOPER_RETRIES = 3`). + +Открытые вопросы BRD §8 / TRZ §4, требующие решения архитектора: +1. Размещение гейта в пайплайне (review / merge-edge / CI-job). +2. Где запускается сканер (CI-job через `check_ci_green` / отдельный QG-чек). +3. Degrade при недоступном CVE-фиде (fail-open / fail-closed). +4. Выбор инструментов (gitleaks/trufflehog; pip-audit/trivy). + +--- + +## Решение + +### Р-1. Размещение — Вариант M (под-гейт ребра `deploy-staging → deploy`), ПЕРВЫМ среди edge-под-гейтов + +Security-гейт реализуется как **детерминированный под-гейт того же ребра** +`deploy-staging → deploy`, что merge-gate и image-freshness, и исполняется **ПЕРВЫМ** — +**ДО** merge-gate. `STAGE_TRANSITIONS` **не меняется** (триггер — то же событие «staging- +deployer завершился»; инвариант TRZ §7.1). + +Порядок врезок в `advance_stage` (блок `current_stage == "deploy-staging"`): + +``` +check_staging_status (PASS, существующий QG стадии) + → security-gate (НОВЫЙ, _handle_security_gate) ← первым + → merge-gate (_handle_merge_gate) + → image-freshness (_handle_image_freshness) + → Phase A (self-deploy approve) +``` + +**Почему merge-edge, а не review (Вариант R):** +- BRD-требование «перед слиянием в `main`» удовлетворяют оба, но на review-стадии diff + может разойтись с тем, что реально вольётся в `main` (параллельная задача двигает `main` + вперёд между review и merge). Merge-edge — последняя точка перед фактическим мержем. +- Переиспользуется готовая машинерия отката/retry/нотификаций edge-под-гейтов + (минимальный blast-radius, инвариант TRZ §7). + +**Почему ПЕРВЫМ (до merge-gate), а не после image-freshness:** +- **Дёшево фейлить.** merge-gate (rebase + re-test, минуты) и image-freshness (docker + rebuild, до 1200с) — дорогие. Нет смысла гонять их на ветке с секретом/CVE. +- **Корректность для секретов.** Секрет живёт в собственных коммитах ветки; + rebase онто `main` его не добавляет и не убирает → скан диапазона `origin/main..HEAD` + до rebase ловит ровно те коммиты, что попадут в `main`. +- **Анти-петля для зависимостей.** Аудит ветки **до** rebase оценивает то, что вносит + ИМЕННО эта задача (её `requirements.txt`/diff), а не уязвимость, которую притащил в + ветку обновившийся `main`. Аудит после rebase «обвинял» бы задачу в чужой (main'овой) + CVE → ложный откат `→ development` → петля (прецедент ORCH-061). Скан до rebase этого + избегает. +- **Проще, чем image-freshness.** Гейт исполняется ДО захвата merge-lease → при FAIL + **lease освобождать не нужно** (в отличие от `_handle_image_freshness`). Чистый откат. + +**Почему не CI-job (Вариант C):** пороги severity, warning-vs-block, аллоулист и +машиночитаемый артефакт-вердикт плохо выражаются одним статусом коммита Gitea; путь +коуплится с CI-раннером. Отклонено для v1; оставлено как точка расширения (BR-14). + +### Р-2. Инструменты + +- **Secret-scanning — `gitleaks`.** Полностью **offline** (без сетевого фида → гарантия + «секрет всегда блокирует» не зависит от сети, BR-2), один статический бинарь, + детерминированный, конфиг + аллоулист в репо (`.gitleaks.toml`, BR-13), поддержка + `--log-opts="origin/main..HEAD"` (скан диапазона), JSON-отчёт, exit-code контракт + (0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента). Бинарь устанавливается в + `Dockerfile` (Go-бинарь, не pip-пакет) — см. `07-infra-requirements.md`. +- **Dependency audit — `pip-audit`.** Python-native (v1-стек — сам оркестратор, Python), + читает `requirements.txt`, источник advisory — OSV/PyPI, JSON-выход, ставится через + `requirements.txt`. trivy/trufflehog отклонены как тяжелее/контейнер-ориентированные для + v1-цели «Python-only» (A3). + +Конкретные инструменты — деталь реализации; контракт гейта (вход: repo/branch/wi, +выход: `(bool, reason)` + артефакт) от них не зависит, заменяемы за leaf-модулем. + +### Р-3. Degrade при недоступном CVE-фиде — **fail-open + громкий warning** (дефолт) + +`pip-audit` требует сети (OSV/PyPI advisory DB). Недоступность фида **по умолчанию**: +- **fail-open**: dep-audit не даёт FAIL по причине недоступности фида (иначе — ложные + откаты `→ development` → петля при сетевых проблемах прод-инстанса, прецедент ORCH-061); +- **громко**: в артефакте `deps_audit_degraded: true`, лог `logger.warning`, Telegram-алерт. +- **Секреты не деградируют:** gitleaks offline → гарантия BR-2 безусловна даже при + отсутствии сети. Деградирует ТОЛЬКО dep-audit. +- **Конфигурируемо:** флаг `security_dep_audit_fail_closed` (дефолт `false`) позволяет + Owner'у переключить на fail-closed (недоступность фида → FAIL) без редеплоя кода. + +Это разделяет две гарантии: «нет секрета в прод» — **безусловная**; «нет известной CVE» — +**best-effort при доступности фида**. Закреплено в acceptance (AC-7). + +### Р-4. Пороги классификации (A4, BR-10) + +- **Секреты:** любой подтверждённый (не из аллоулиста) секрет → **вклад в FAIL** (всегда + блок; флаг `security_secrets_block`, дефолт `true`). +- **Зависимости:** severity ≥ `security_dep_block_severity` (дефолт `HIGH`) → **вклад в + FAIL** (`deps_blocking`); ниже порога (`MEDIUM`/`LOW`) → **warning** (`deps_warning`, + не блокирует, фиксируется в теле). +- **Severity = UNKNOWN** (OSV/advisory без CVSS — частый случай pip-audit): трактуется как + **ниже порога → warning**, никогда не авто-блок (анти-петля). Логируется. + +### Р-5. Артефакт и вердикт (FR-3, BR-6, канон проекта) + +- Новый артефакт **`17-security-report.md`** (следующий свободный номер; финализировано). +- YAML-frontmatter: + ``` + --- + security_status: PASS # PASS | FAIL + secrets_found: 0 + deps_blocking: 0 + deps_warning: 2 + deps_audit_degraded: false + --- + ``` + Тело — человекочитаемый список находок (секреты: файл/правило/маскированное совпадение; + CVE: пакет/версия/идентификатор/severity). +- **Единый источник истины:** гейт вычисляет находки → пишет артефакт → **читает вердикт + обратно через `parse_security_status(content)`** (frontmatter-парсер по образцу + `_parse_deploy_status`/`_parse_staging_status`) → возвращает этот вердикт. Так возвращаемый + `(bool, reason)` гарантированно == frontmatter артефакта (канон «машинный вердикт — только + из YAML-frontmatter, никогда из прозы», AC-8). Negative-токен (`FAIL`) авторитетен. +- Битый/отсутствующий frontmatter / нет поля `security_status` → `(False, reason)` — + fail-closed на чтении вердикта (AC-9). + +### Р-6. Поведение красного гейта (FR-4, BR-5) + +`security_status: FAIL` → врезка `_handle_security_gate` (по образцу +`_handle_image_freshness`, но БЕЗ работы с lease — гейт до его захвата): +- `update_task_stage(development)` + `enqueue_job("developer", …)`; +- retry-счётчик — **существующий** `_developer_retry_count` (общий с merge/freshness; + без новой колонки, TRZ §6); cap `MAX_DEVELOPER_RETRIES = 3` → при исчерпании + `set_issue_blocked` + Telegram; +- `task_desc` несёт **дословную причину** (какие секреты/файлы, какие пакеты/CVE/severity) + по образцу ORCH-046 — не только ссылку на артефакт (AC-12); +- `notify_qg_failure` + Plane-коммент (наблюдаемость BR-11). + +PASS → `return False` из обработчика → `advance_stage` идёт к merge-gate (тишина, без шума). + +### Р-7. Условный раскат и устойчивость (FR-5/FR-6) + +- `check_security_gate(repo, work_item_id, branch)` в `QG_CHECKS`; обёртка делегирует в + `src/security_gate.py` (ленивый импорт во избежание цикла — по образцу + `_check_staging_image_fresh`). +- Условность: `security_gate_enabled=False` → `(True, "security-gate disabled")`; + `security_gate_repos` (CSV) пусто → реально только `is_self_hosting_repo` → прочие репо + `(True, "security-gate N/A for ")` (AC-14/AC-15). +- **never-raise** (двойной guard как `check_branch_mergeable`): любая ошибка (нет бинаря, + таймаут, исключение) → `(False, reason)`, исключение не уходит в `advance_stage` (AC-16). +- Таймаут сканирования `security_scan_timeout_s` (дефолт 300) на каждый внешний вызов + (`subprocess … timeout=`) — превышение → детерминированный degrade-вердикт (AC-17). + +### Р-8. Self-hosting safety (инвариант TRZ §7.5, AC-19) + +Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает +деплой-хук, не рестартит и не трогает прод-контейнер (8500/8501). + +--- + +## Точки касания (для developer; reviewer проверяет полноту — AC-20) + +| Модуль | Изменение | +|--------|-----------| +| `src/security_gate.py` (**новый leaf**) | `security_gate_applies`, `scan_secrets`, `audit_dependencies`, `classify_severity`, `compute_verdict`, `write_security_report`, `parse_security_status`, `check_security_gate`. never-raise, fail-closed на чтении вердикта. По образцу `image_freshness.py`. | +| `src/qg/checks.py` | `check_security_gate` (тонкая обёртка, ленивый импорт) + регистрация в `QG_CHECKS`. | +| `src/stage_engine.py` | `_handle_security_gate(...)` + врезка ПЕРВОЙ в блоке `current_stage == "deploy-staging"` (до `_handle_merge_gate`). FAIL → откат на `development`. never-raise. **`STAGE_TRANSITIONS` НЕ меняется.** | +| `src/config.py` | `security_gate_enabled` (True), `security_gate_repos` (""), `security_dep_block_severity` ("HIGH"), `security_scan_timeout_s` (300), `security_dep_audit_fail_closed` (False), `security_secrets_block` (True) — с docstring по образцу ORCH-043/058. | +| `Dockerfile` | Установка `gitleaks` (release-бинарь). | +| `requirements.txt` | `pip-audit`. | +| `.gitleaks.toml` (**новый, корень репо**) | Конфиг правил + аллоулист (`.env.example`-плейсхолдеры, тест-фикстуры) — BR-13. | +| `.openclaw/agents/developer.md` | (Опц.) краткая инструкция про устранение security-находок при заворотах. | +| `tests/` | `test_security_gate.py`, `test_qg_security.py`, `test_stage_engine_security_gate.py` (см. `04-test-plan.yaml`). | +| **Документация** | `CLAUDE.md` (артефакт 17-…), `docs/architecture/README.md` (таблица гейтов + реестр QG + раздел), `CHANGELOG.md`, `.env.example` (`ORCH_SECURITY_*`), global `adr-0012`. | + +--- + +## Альтернативы (отклонены) + +- **Вариант R (review-стадия):** раньше/дешевле, но diff может разойтись с тем, что + вольётся в `main`; merge-edge уже закрывает «последнюю страховку». +- **Вариант C (CI-job через `check_ci_green`):** пороги/severity/аллоулист/артефакт плохо + выражаются статусом коммита; коуплинг с CI-раннером. → точка расширения BR-14. +- **fail-closed dep-audit по умолчанию:** ложные откаты при сетевых сбоях → петля. → + только опционально через флаг. +- **Аудит после rebase (как анкер image-freshness):** обвиняет задачу в CVE из `main` → + петля. → скан ветки ДО merge-gate. +- **Новая стадия `security`:** «пустая» стадия без агента не имеет триггера (как + отклонено в ORCH-043). → под-гейт ребра. +- **Новая колонка retry в БД:** не нужна — переиспользуем `_developer_retry_count`. + +--- + +## Последствия + +**Плюсы.** Структурно невозможно тихо влить секрет (безусловно) или известную CVE +(best-effort) в `main`/прод автономной системы. Самоприменение CLAUDE.md §8. Минимальный +blast-radius: `STAGE_TRANSITIONS`/схема БД не меняются, переиспользован готовый паттерн. + +**Минусы / плата.** Ещё один «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`). +Добавлены внешние инструменты (gitleaks-бинарь в образ, pip-audit в зависимости). Время +сканирования добавляется к каждому прогону (ограничено таймаутом). Dep-audit best-effort +при сетевых сбоях (осознанный компромисс против петли). v1 — Python-only (A3); мульти-стек +и SAST — follow-up WI (BR-14). + +**Раскат.** Сквозное изменение конвейера (новый QG + новый edge-под-гейт) → лейбл +`arch:major-change`. Прод-деплой ORCH-022 — строго через staging-гейт (8501), без рестарта +прод-контейнера в рамках задачи (self-hosting safety). + +## Связи + +adr-0006 (merge-gate — паттерн edge-под-гейта/отката), adr-0008 (image-freshness — +условность/never-raise/fail-closed), adr-0003 (`is_self_hosting_repo` — образец условности), +adr-0009/ORCH-061 (анти-петля ложных FAIL), ORCH-046 (дословный reason в `task_desc`), +ORCH-9/15 (мульти-стек — будущая зависимость). diff --git a/docs/work-items/ORCH-022/07-infra-requirements.md b/docs/work-items/ORCH-022/07-infra-requirements.md new file mode 100644 index 0000000..d48f588 --- /dev/null +++ b/docs/work-items/ORCH-022/07-infra-requirements.md @@ -0,0 +1,56 @@ +# 07 — Инфраструктурные требования: Security-гейт (ORCH-022) + +См. `06-adr/ADR-001-security-gate.md` (Р-2, Р-3, Р-8). Топология не меняется (один сервер +mva154, Docker Compose). Новые требования — только инструменты сканирования и сетевой доступ +к CVE-фиду. + +## I-1. Бинарь `gitleaks` в образе +- **Что:** статический Go-бинарь `gitleaks` (secret-scanning), устанавливается в `Dockerfile` + (НЕ pip-пакет). Зафиксировать версию (pinned release) для детерминизма. +- **Почему в образе, а не на хосте:** гейт исполняется внутри контейнера оркестратора + (`advance_stage`); сканируется per-task worktree, смонтированный в контейнер. +- **Оффлайн:** gitleaks не требует сети (правила локальны) → гарантия «секрет всегда + блокирует» (BR-2) не зависит от доступности интернета. +- **Контракт exit-кодов:** 0 = чисто, 1 = найдены секреты, ≥2 = ошибка инструмента + (≥2 → never-raise degrade-вердикт гейта). + +## I-2. `pip-audit` в зависимостях +- **Что:** Python-пакет `pip-audit` (dependency audit), добавляется в `requirements.txt` + (pinned-версия). +- **Источник advisory:** OSV / PyPI advisory DB — **требует сетевого доступа** (исходящий + HTTPS к OSV/PyPI). +- **Цель v1:** аудит `requirements.txt` корня репо (Python-стек, A3). Мульти-стек — follow-up. + +## I-3. Сетевой доступ к CVE-фиду (degrade-политика) +- **Требование:** исходящий HTTPS из прод-контейнера к OSV/PyPI advisory. +- **При недоступности (Р-3):** **fail-open + громкий warning** по умолчанию — dep-audit не + краснит гейт из-за сетевого сбоя (анти-петля ORCH-061); фиксируется + `deps_audit_degraded: true` + Telegram + лог. Флаг `security_dep_audit_fail_closed` + (дефолт `false`) — для перевода в строгий режим без редеплоя кода. +- **Секреты не зависят от сети** (I-1) — критическая гарантия безусловна. + +## I-4. Конфиг-файлы в репозитории (версионируемые, BR-13) +- `.gitleaks.toml` (корень репо): правила + аллоулист заведомо-безопасных совпадений + (плейсхолдеры `.env.example`, тест-фикстуры). Версионируется, ревьюится как код. + +## I-5. Env-флаги (`.env.example` + хост `.env`/`.env.staging`) +| Переменная | Дефолт | Назначение | +|------------|--------|-----------| +| `ORCH_SECURITY_GATE_ENABLED` | `true` | глобальный kill-switch | +| `ORCH_SECURITY_GATE_REPOS` | `` (пусто) | CSV scope; пусто → только self-hosting | +| `ORCH_SECURITY_DEP_BLOCK_SEVERITY` | `HIGH` | порог блокировки зависимостей | +| `ORCH_SECURITY_SCAN_TIMEOUT_S` | `300` | таймаут каждого внешнего вызова сканера | +| `ORCH_SECURITY_DEP_AUDIT_FAIL_CLOSED` | `false` | строгий режим при недоступном фиде | +| `ORCH_SECURITY_SECRETS_BLOCK` | `true` | секреты блокируют (всегда по дефолту) | + +Секреты-значения в гит НЕ коммитятся (CLAUDE.md §8) — только дефолты в `.env.example`. + +## I-6. Ресурсы и тайминги +- Время сканирования добавляется к каждому прогону задачи на ребре `deploy-staging → deploy`, + ограничено `ORCH_SECURITY_SCAN_TIMEOUT_S` (по образцу `merge_retest_timeout_s`). +- Гейт исполняется ДО merge-gate/image-freshness (дёшево фейлить до дорогих rebase/rebuild). + +## I-7. Self-hosting safety (инвариант) +Гейт **только читает/сканирует** (git, gitleaks, pip-audit, запись артефакта). Не вызывает +деплой-хук, не рестартит/не трогает прод-контейнер (8500/8501). Прод-деплой ORCH-022 — строго +через staging-гейт (8501). diff --git a/docs/work-items/ORCH-022/08-data-requirements.md b/docs/work-items/ORCH-022/08-data-requirements.md new file mode 100644 index 0000000..9445746 --- /dev/null +++ b/docs/work-items/ORCH-022/08-data-requirements.md @@ -0,0 +1,26 @@ +# 08 — Требования к схеме БД: Security-гейт (ORCH-022) + +## Решение: схема БД НЕ меняется + +Миграций нет. Обоснование (соответствует TRZ §6 и паттерну edge-под-гейтов ORCH-043/058): + +1. **Вердикт гейта — артефакт-файл** `17-security-report.md` (YAML-frontmatter), как + `14-deploy-log.md` / `15-staging-log.md`. Не хранится в БД. +2. **Состояние/идемпотентность** — детерминированная пересборка вердикта при каждом тике + (гейт чистый, без долгоживущего состояния между прогонами); sentinel-файлы НЕ требуются + (в отличие от deploy-state/post-deploy-state — там асинхронный self-restart). +3. **Retry-счётчик** — переиспользуется существующий `_developer_retry_count(task_id)` + (подсчёт по `jobs`/`agent_runs`), общий с merge-gate/image-freshness. **Новой колонки + `security_retry` НЕ вводим** (TRZ §6: предпочесть подсчёт по `jobs`/`agent_runs`). Это + корректно: security-FAIL, как merge/freshness-FAIL, откатывает на `development` и + запускает developer — он и есть единица retry; общий cap=3 защищает от петли. + +## Используемые существующие таблицы (без изменений) +- `tasks` — стадия задачи (`update_task_stage` при откате на `development`). +- `jobs` — enqueue `developer` при FAIL; основа `_developer_retry_count`. +- `agent_runs` — usage/duration; основа подсчёта retry. + +## Что НЕ делаем +- Не добавляем таблицу findings/CVE-журнала (история находок — в артефактах per-task; петля + уроков ORCH-8 читает артефакт). +- Не добавляем колонок в `tasks`/`jobs`. diff --git a/docs/work-items/ORCH-022/10-tech-risks.md b/docs/work-items/ORCH-022/10-tech-risks.md new file mode 100644 index 0000000..47a76c3 --- /dev/null +++ b/docs/work-items/ORCH-022/10-tech-risks.md @@ -0,0 +1,16 @@ +# 10 — Технические риски: Security-гейт (ORCH-022) + +| ID | Риск | Вероятность / Влияние | Митигация (заложена в ADR-001) | +|----|------|----------------------|-------------------------------| +| R-1 | **Ложные срабатывания → петля отката** `→ development` (прецедент ORCH-061 staging-loop). | Средн. / Выс. | Аллоулист `.gitleaks.toml` (BR-13); cap `MAX_DEVELOPER_RETRIES=3` → эскалация (`set_issue_blocked`+Telegram); конфигурируемый порог severity; kill-switch; UNKNOWN-severity → warning, не блок. | +| R-2 | **Недоступность CVE-фида** даёт ложный красный/исключение. | Средн. / Выс. | fail-open + громкий warning по умолчанию (Р-3); `deps_audit_degraded:true`; флаг `security_dep_audit_fail_closed` для строгого режима. Секреты offline → не затронуты. | +| R-3 | **Скан вешает worker-слот** (зависший gitleaks/pip-audit) → стоит конвейер всех проектов (общий инстанс, `max_concurrency`). | Низк. / Выс. | `security_scan_timeout_s` (300) на каждый внешний вызов; never-raise degrade-вердикт; гейт ПЕРВЫМ на ребре (фейлит до дорогих rebase/rebuild). | +| R-4 | **Исключение гейта роняет `advance_stage`** → встаёт движок. | Низк. / Выс. | Двойной never-raise guard (внешний+внутренний) как `check_branch_mergeable`; AC-16/TC-11. | +| R-5 | **Скан после rebase обвиняет задачу в CVE из `main`** → петля. | — (устранён дизайном) | Гейт исполняется ДО merge-gate (скан ветки до rebase); Р-1. | +| R-6 | **Отсутствие бинаря `gitleaks` в образе** (забыт в Dockerfile) → гейт всегда degrade. | Низк. / Средн. | Установка в Dockerfile (I-1), pinned-версия; TC-11 (нет бинаря → `(False,reason)`, never-raise); проверяется на staging (8501) до прода. | +| R-7 | **pip-audit без severity (UNKNOWN)** → либо ложный блок, либо пропуск. | Средн. / Средн. | UNKNOWN → warning (не блок), логируется; осознанный анти-петля компромисс; ужесточение — follow-up. | +| R-8 | **Self-hosting: гейт трогает прод** (рестарт/деплой). | — (запрещено дизайном) | Гейт только читает/сканирует; AC-19/TC-21; прод-деплой ORCH-022 — через staging-гейт. | +| R-9 | **Drift вердикта vs артефакта** (возврат ≠ frontmatter). | Низк. / Средн. | Единый источник: гейт пишет артефакт → читает обратно через `parse_security_status` → возвращает (Р-5); AC-8. | +| R-10 | **Регресс существующих гейтов/стадий** (сломан `QG_CHECKS`/`STAGE_TRANSITIONS`). | Низк. / Выс. | `STAGE_TRANSITIONS` не меняется; новый чек — аддитивно в реестр; полный прогон `tests/` (TC-20); staging-гейт перед прод. | +| R-11 | **v1 Python-only** — секреты/CVE в не-Python стеке (JS/Android) не ловятся. | — (вне scope v1, A3) | Условность scope; точка расширения мульти-стек/SAST (BR-14); зависимость ORCH-9/15 зафиксирована. | +| R-12 | **Стоимость времени** на каждом прогоне задачи. | Низк. / Низк. | Таймаут; гейт первым (ранний выход); только self-hosting по умолчанию. |