Files
orchestrator/docs/work-items/ORCH-117/01-brd.md

22 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used, escalate
work_item stage author_agent status created_at model_used escalate
ORCH-117 analysis analyst ready-for-review 2026-06-15 claude-opus-4-8 full-cycle

01 — BRD / Bug-report: ORCH-117 — test/staging Plane writes must be sandbox-only and never mutate prod

Work Item: ORCH-117 · Repo: orchestrator · Стадия: analysis · Трек: Bug → эскалация в full-cycle

⚠️ escalate: full-cycle (ADR-001 D5 ORCH-019). Задача помечена Bug, но по сути это архитектурный + safety-critical (self-hosting) дефект изоляции окружений: нужно решение о том, где ставить fail-closed-чокпоинт записи в Plane, как детектировать тест-процесс (pytest/worktree) в отличие от staging-runtime, и как устроен явный аудируемый opt-in для sandbox-интеграции. Это требует ADR (политика изоляции + точка перехвата). Поэтому выпускается полный analysis-пакет, а не облегчённый bug-пакет. Оператор снимает багфикс-трек: POST /bug-fast-track/escalate?work_item=ORCH-117 → задача пойдёт через стадию architecture (architect выпустит ADR для политики изоляции/чокпоинта).


1. Бизнес-контекст и проблема

Симптом (наблюдаемое — установленный факт из инцидента ORCH-114)

Во время тестирования ORCH-114 тестовый/worktree-путь выполнил РЕАЛЬНУЮ запись в Plane против ПРОДАКШН-проекта ORCH. В логах Plane зафиксировано:

PATCH /issues/dd57ad23... state=3738cd3c...   # 3738cd3c == Done
+ comment: "Stage: deploy → done" (branch feature/orch114)

То есть notify_stage_change("ORCH-114", "deploy", "done"), запущенный из тестового процесса, смутировал боевую задачу в Plane — поставил Done и оставил комментарий. Это «ложный Done»: прод-доска показала задачу завершённой по действию тестов, а не конвейера.

Причина симптома (установленный факт, верифицировано в коде)

Тест/staging-процессы имеют доступ к живому Plane-токену и workspace и не принуждаются писать только в ORCH Sandbox:

  1. Токен берётся из боевого окружения контейнера. В src/plane_sync.py:17 PLANE_HEADERS = {"X-API-Key": settings.plane_api_token} фиксируется на импорте модуля; settings.plane_api_token читается из env контейнера, где боевой токен уже установлен.
  2. Защита в тестах не работает. Тестовые модули делают os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") (например tests/test_deploy_terminal_sync.py:19, tests/test_orch026_serialize_integration.py:16 и ~20 др.). os.environ.setdefault НЕ перекрывает уже установленную в контейнере переменную → в проде это no-op → тесты наследуют реальный токен. Вдобавок, даже если бы env переписывался ПОСЛЕ импорта plane_sync, PLANE_HEADERS уже захвачен литералом на импорте и не пересчитывается (см. п.1) — подмена токена постфактум бесполезна.
  3. Целевой проект по умолчанию — боевой. src/plane_sync.py:57 PROJECT_ID = settings.plane_project_id or "7a79f0a9-5278-49cd-9007-9a338f238f9c" — дефолт указывает на боевой ORCH-проект; ничто не принуждает тест-процесс адресовать только sandbox 8c5a3025-4f9d-4190-b79f-fa06276bb27e (identifier SANDBOX).
  4. Нет fail-closed-гарда на путях записи. Все мутирующие вызовы Plane проходят через три примитива в src/plane_sync.pyupdate_issue_state (httpx.patch, стр. 861), add_comment (httpx.post, стр. 885), _set_issue_state_direct (httpx.patch, стр. 1047) — и ни один не проверяет, выполняется ли он в тест-процессе и легитимен ли целевой проект.

Прецедент в кодовой базе (почему фикс уместен и как его форму подсказать)

tests/conftest.py уже содержит ровно тот же класс защиты для Telegram: autouse-фикстура _no_telegram глушит send_telegram, потому что «pytest на проде слал РЕАЛЬНЫЕ Telegram-сообщения Славе» (дословно из докстринга conftest). Аналогичная autouse-страховка для Plane-записи отсутствует — это пробел того же рода, который и реализовался инцидентом ORCH-114. Sandbox как понятие уже существует: scripts/staging_check.py:283 фиксирует SANDBOX_PROJECT_ID = "8c5a3025-4f9d-4190-b79f-fa06276bb27e", а проверка B6 утверждает инвариант «sandbox present ∧ prod-ET absent ∧ prod-ORCH absent» — но только как read-only верификация доступа, а не как гард записи.

Локализация (куда смотреть архитектору/разработчику)

  • Чокпоинт записи — три примитива в src/plane_sync.py (update_issue_state, add_comment, _set_issue_state_direct); все set_issue_*/notify_* сводятся к ним. Гард логично ставить максимально близко к фактическому httpx.patch/post (низкий чокпоинт ловит любой путь, включая будущие).
  • Захват токена на импортеPLANE_HEADERS/PROJECT_ID модульного уровня (plane_sync.py:17,57): подмена env после импорта не лечит; гард обязан перехватывать на момент вызова.
  • Дефолт тестового окруженияtests/conftest.py (autouse, fail-closed по образцу _no_telegram).
  • Конфиг opt-insrc/config.py (новые ключи интеграционного включения + sandbox-allowlist).
  • Детект тест-процесса — в src/ сейчас нет механизма (PYTEST_CURRENT_TEST/sys.modules не используются для этого); его предстоит ввести и/или опереть на явный конфиг-флаг.

Вывод: устойчивость должна быть на стороне системы — запись в Plane из тест/worktree-процесса в боевой проект должна быть физически невозможна (fail-closed), независимо от того, какой токен оказался в окружении; sandbox-запись разрешается только при явном аудируемом opt-in и только в проект SANDBOX.

2. Объём (scope)

В объёме

  • Жёсткая fail-closed изоляция записи в Plane: прогон unit/test/full-regression (pytest, в т.ч. из worktree) не может мутировать боевые Plane-проекты (state-PATCH и/или comment-POST) — даже при наличии живого боевого токена в окружении.
  • Sandbox-only для реальных тестов: staging / full-real e2e-тесты, которым нужна настоящая запись в Plane, адресуют только проект ORCH Sandbox (8c5a3025-…); любой другой целевой проект (особенно боевой 7a79f0a9-…) — запрещён.
  • Явный аудируемый opt-in: запись в Plane из тест-процесса возможна исключительно при одновременном выполнении: (а) включён выделенный интеграционный флаг, (б) целевой проект ∈ sandbox-allowlist. Отсутствие любого условия → запись блокируется.
  • Дефолт тестов fail-closed: autouse-страховка в tests/conftest.py (по образцу _no_telegram) блокирует Plane-запись по умолчанию во всех тестах.
  • Наблюдаемость/аудит: каждая заблокированная запись логируется структурно (WARNING/ERROR с целевым project_id, work_item, операцией); каждая разрешённая sandbox-запись — audit-строкой.
  • Док/конфиг: обновить .env.example, CLAUDE.md, docs/architecture/README.md, docs/operations/INFRA.md (и, при необходимости, docs/deployment/* про тестовую изоляцию).
  • Обязательный регресс-тест: воспроизводит инцидент ORCH-114 — красный до фикса, зелёный после.

Вне объёма

  • Изменение STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict ключей / схемы БД (это bugfix-изоляция, а не Quality Gate и не стадия).
  • Изменение поведения боевого рантайма оркестратора (не-pytest процесс): прод обязан писать в Plane как прежде, гард для него — no-op.
  • Изменение поведения staging-рантайма (8501): staging — реальный процесс оркестратора, работающий только с sandbox-проектом по конфигу; он должен по-прежнему писать в SANDBOX.
  • Запрет/контроль ручных операций оператора (вне технической власти системы).
  • Выбор конкретного механизма детекта тест-процесса и точки перехвата (гард в plane_sync vs обёртка httpx vs autouse-фикстура vs комбинация) — зона архитектора (ADR).
  • Массовая чистка/нормализация существующих os.environ.setdefault(...) строк в тестах сверх необходимого (можно оставить как есть — гард не должен от них зависеть; см. NFR-4).

3. Заинтересованные стороны

  • Заказчик/оператор (Слава) — страдает от «ложных Done» и шумных комментариев в боевой Plane, порождённых тестами; принимает результат.
  • Self-hosting конвейер orchestrator — прямой потребитель: целостность боевой Plane-доски как источника индикации (слой B, ORCH-066) не должна искажаться тест-прогонами.
  • Все проекты на общем инстансе (enduro-trails) — косвенно: тестовая запись не должна задевать чужие боевые проекты в общем workspace.
  • Разработчики/CI — потребители sandbox-e2e: должны сохранить возможность реальной проверки против SANDBOX.

4. Бизнес-требования (BR)

  • BR-1 — Прогон pytest (unit/integration/full-regression), в том числе из worktree, НЕ должен выполнять мутирующую запись в Plane (state-PATCH и/или comment-POST) против боевых проектов — даже при наличии живого боевого токена в окружении. Это fail-closed свойство: запись в боевой проект из тест-процесса невозможна.
  • BR-2 — Реальная запись в Plane из тест/staging-контекста разрешена только в проект ORCH Sandbox (8c5a3025-4f9d-4190-b79f-fa06276bb27e); любой иной целевой проект (в т.ч. боевой ORCH 7a79f0a9-… и боевой enduro-проект) — запрещён.
  • BR-3 — Реальная sandbox-запись из тест-процесса включается только явным аудируемым opt-in: одновременно (а) выделенный интеграционный флаг включён и (б) целевой проект ∈ sandbox-allowlist. Отсутствие любого условия → запись блокируется (default-deny).
  • BR-4 — Дефолтная тестовая поза — fail-closed: при обычном pytest tests/ (без явного opt-in) ни один тест не может писать в Plane (autouse-страховка в conftest.py, по образцу существующего _no_telegram).
  • BR-5Sandbox e2e сохраняется: с включённым opt-in и целевым проектом SANDBOX реальная запись в Plane успешно проходит (регрессии sandbox-сценария нет).
  • BR-6Наблюдаемость/аудит: каждая заблокированная попытка записи логируется структурно (целевой project_id, work_item, операция, причина блокировки); каждая разрешённая sandbox-запись — audit-строкой. Инцидент класса ORCH-114 должен быть видимым, а не молчаливым.
  • BR-7Документация и конфиг обновлены в том же PR (golden source наравне с кодом): .env.example, CLAUDE.md, docs/architecture/README.md, docs/operations/INFRA.md.

5. Нефункциональные требования (NFR)

  • NFR-1 (fail-closed / default-deny) — при любой неопределённости (не удаётся достоверно определить целевой проект / окружение / тест-контекст) запись в тест-контексте запрещается. «Не знаю» ⇒ «не пишу».
  • NFR-2 (нулевая регрессия боевого рантайма) — реальный прод-процесс оркестратора (не pytest) пишет в Plane байт-в-байт как до ORCH-117; гард для него — no-op. STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict ключи / схема БД — не тронуты.
  • NFR-3 (staging-рантайм не сломан) — staging-инстанс (8501) — реальный процесс оркестратора (не pytest), сконфигурированный на sandbox-проект; он должен по-прежнему писать в SANDBOX. Детект обязан отличать тест-процесс (pytest/worktree) от staging-runtime.
  • NFR-4 (устойчивость к захвату токена на импорте) — фикс не должен полагаться на подмену settings.plane_api_token/env постфактум (бесполезно из-за модульного захвата PLANE_HEADERS/ PROJECT_ID, plane_sync.py:17,57) и не должен зависеть от неработающего os.environ.setdefault(...) в тестах. Перехват — на момент вызова примитива записи.
  • NFR-5 (надёжность / self-hosting safety) — гард изолирован и never-raise в боевом пути (по образцу leaf'ов serial_gate/cancel/deploy_status_guard): сбой/недоступность логики гарда не роняет боевой конвейер и не блокирует легитимную боевую запись. В тест-процессе срабатывание гарда должно быть громким (блок + аудит, при необходимости — жёсткий fail), чтобы дефект всплыл, а не замаскировался.
  • NFR-6 (обратимость / kill-switch) — поведение под флагом по конвенции проекта, но дефолт = безопасный (fail-closed в тестах). Kill-switch не должен позволять случайно переоткрыть запись в боевой проект из тестов без явного аудируемого opt-in (BR-3); т.е. «выключить защиту полностью» не равно «разрешить запись в прод из pytest».
  • NFR-7 (область / композиция) — изменение скоупится на изоляцию тест/staging-записи; не ухудшает поведение для прочих репо/боевого рантайма; совместимо с ORCH-066 (статусная модель), ORCH-094 (deploy-status guard), ORCH-061 (sandbox-infra tolerance staging_check).

6. Допущения и ограничения

  • Все мутирующие записи в Plane проходят через 3 примитива src/plane_sync.py (update_issue_state, add_comment, _set_issue_state_direct) — это единый узкий чокпоинт (верифицировано: все set_issue_*/notify_* сводятся к ним).
  • Боевой проект ORCH = 7a79f0a9-5278-49cd-9007-9a338f238f9c (дефолт PROJECT_ID); sandbox = 8c5a3025-4f9d-4190-b79f-fa06276bb27e (SANDBOX, уже зафиксирован в scripts/staging_check.py:283).
  • PLANE_HEADERS/PROJECT_ID захватываются на импорте модуля — гард обязан читать актуальное состояние/контекст в момент вызова, не на импорте.
  • Тест-процесс достоверно отличим (например по PYTEST_CURRENT_TEST в env, по наличию pytest в sys.modules, и/или по явному конфиг-флагу тест-режима) — выбор признака — вопрос ADR; признак должен быть надёжным и не давать ложноположительных срабатываний в боевом/staging рантайме (NFR-2/NFR-3).
  • Конкретный механизм (гард-leaf в plane_sync / обёртка над httpx / autouse-фикстура / их комбинация) и протокол opt-in — открытый вопрос архитектуры, решается в 06-adr/.

7. Критерии успеха

Прогон pytest с живым боевым токеном в окружении физически не может смутировать боевой ORCH-проект (0 PATCH/POST в боевой проект); sandbox-e2e против SANDBOX по-прежнему работает при явном opt-in; боевой и staging рантаймы — без регресса; каждая блокировка/разрешение записи — наблюдаема (аудит-лог); док/конфиг обновлены; обязательный регресс-тест красный до фикса, зелёный после. Детальные PASS/FAIL — 03-acceptance-criteria.md.

8. Риски

  • Ложноположительный детект тест-процесса в боевом/staging рантайме → блокировка легитимной боевой/sandbox записи (молчаливая потеря индикации Plane). Митигирует NFR-2/NFR-3 + аудит (BR-6).
  • Ложноотрицательный детект (тест-процесс не распознан) → дефект остаётся → нужен надёжный признак + fail-closed по умолчанию (NFR-1) + дефолтная autouse-страховка (BR-4).
  • Захват на импорте (PLANE_HEADERS/PROJECT_ID): неверная точка перехвата (на импорте, а не на вызове) даст ложное чувство защиты — жёсткое ограничение для архитектора (NFR-4).
  • Kill-switch как чёрный ход: грубо реализованный «выключатель» может переоткрыть запись в прод из тестов — запрещено (NFR-6).
  • Кросс-каттинг с ORCH-066 (Plane-индикация), ORCH-094 (deploy-status guard), ORCH-061 (staging sandbox-infra). Детали/митигации — 10-tech-risks.md (заполняет архитектор).