22 KiB
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:
- Токен берётся из боевого окружения контейнера. В
src/plane_sync.py:17PLANE_HEADERS = {"X-API-Key": settings.plane_api_token}фиксируется на импорте модуля;settings.plane_api_tokenчитается из env контейнера, где боевой токен уже установлен. - Защита в тестах не работает. Тестовые модули делают
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) — подмена токена постфактум бесполезна. - Целевой проект по умолчанию — боевой.
src/plane_sync.py:57PROJECT_ID = settings.plane_project_id or "7a79f0a9-5278-49cd-9007-9a338f238f9c"— дефолт указывает на боевой ORCH-проект; ничто не принуждает тест-процесс адресовать только sandbox8c5a3025-4f9d-4190-b79f-fa06276bb27e(identifierSANDBOX). - Нет fail-closed-гарда на путях записи. Все мутирующие вызовы Plane проходят через три
примитива в
src/plane_sync.py—update_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-in —
src/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_syncvs обёрткаhttpxvs 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); любой иной целевой проект (в т.ч. боевой ORCH7a79f0a9-…и боевой 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-5 — Sandbox 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(заполняет архитектор).