--- work_item: ORCH-117 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-15 model_used: claude-opus-4-8 escalate: full-cycle --- # 02 — ТЗ (TRZ): ORCH-117 — sandbox-only fail-closed изоляция записи в Plane Work Item: **ORCH-117** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода. > Архитектурное обоснование/решения (точка перехвата, признак тест-процесса, протокол opt-in) — > задача архитектора (`06-adr/`). Ниже — требования и ограничения, привязанные к реальным модулям. ## 1. Сводка изменения Ввести **fail-closed гард записи в Plane**: любая мутирующая запись (state-PATCH / comment-POST), исходящая из **тест-процесса** (pytest/worktree), блокируется по умолчанию и допускается **только** при явном аудируемом opt-in **и** целевом проекте из **sandbox-allowlist**. Боевой рантайм-процесс оркестратора и staging-рантайм (8501) не затронуты (гард для них — no-op). Перехват выполняется **на момент вызова** примитивов записи `src/plane_sync.py` (а не на импорте, где токен уже захвачен). Дефолтная тестовая поза — блокировка, через autouse-страховку в `tests/conftest.py` (по образцу `_no_telegram`). Изменение — bugfix-изоляция: **не** Quality Gate, **не** стадия; `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не трогаются. ## 2. Задействованные модули / пути | Путь | Действие | Зачем | |------|----------|-------| | `src/plane_sync.py` | изменить | Врезать гард в 3 примитива записи: `update_issue_state` (`httpx.patch`, стр. 861), `add_comment` (`httpx.post`, стр. 885), `_set_issue_state_direct` (`httpx.patch`, стр. 1047). Учесть захват `PLANE_HEADERS`/`PROJECT_ID` на импорте (стр. 17/57). | | `src/plane_write_guard.py` *(кандидат, имя — на усмотрение архитектора)* | создать *(вероятно)* | Чистый leaf-гард (never-raise в боевом пути, по образцу `serial_gate`/`cancel`/`deploy_status_guard`): `decide(project_id, op, work_item) -> ALLOW \| BLOCK` + детект тест-процесса + sandbox-allowlist. Альтернатива/комбинация — обёртка над `httpx`/autouse-фикстура (решает ADR). | | `src/config.py` | изменить | Новые ключи: интеграционный opt-in флаг записи из тестов, sandbox-allowlist проектов, (опц.) kill-switch гарда. Дефолты = безопасные (fail-closed в тестах). | | `tests/conftest.py` | изменить | Новая autouse-фикстура fail-closed (блок Plane-записи во всех тестах по умолчанию), по образцу `_no_telegram`. Тесты sandbox-e2e переопределяют её своим opt-in (как `test_*` переопределяют `_disable_*`). | | `.env.example` | изменить | Канон новых `ORCH_*` ключей (opt-in/allowlist/kill-switch) с безопасными дефолтами. | | `scripts/staging_check.py` | проверить/при необходимости адаптировать | Block C (E2E в SANDBOX) должен остаться рабочим: реальная запись в sandbox под opt-in. `SANDBOX_PROJECT_ID` (стр. 283) — источник идентификатора sandbox. | | `CLAUDE.md`, `docs/architecture/README.md`, `docs/operations/INFRA.md` | изменить | Документировать инвариант изоляции тест/staging-записи (golden source наравне с кодом). | | `tests/test_orch117_plane_write_isolation.py` *(имя — кандидат)* | создать | Покрытие FR-1…FR-6, включая обязательный регресс ORCH-114 (TC-01). | > ⚠️ Список модулей **не** предписывает архитектуру. Точку перехвата (низкий чокпоинт в `plane_sync` > у `httpx.patch/post` vs обёртка транспорта vs autouse-фикстура) и признак тест-процесса выбирает > архитектор в ADR. ТЗ фиксирует **требования к поведению**, а не способ реализации. ## 3. Функциональные требования ### FR-1 — Fail-closed блок записи в боевой проект из тест-процесса (BR-1, BR-2) В **тест-процессе** (pytest/worktree) любой вызов `update_issue_state` / `add_comment` / `_set_issue_state_direct` с целевым `project_id` **вне** sandbox-allowlist (в частности боевой ORCH `7a79f0a9-5278-49cd-9007-9a338f238f9c` и любой боевой enduro-проект) **НЕ должен** выполнять `httpx.patch`/`httpx.post` — запись блокируется. Свойство **fail-closed**: при невозможности достоверно определить целевой проект → блокировать (NFR-1). Гард читает контекст **в момент вызова** (NFR-4), не полагается на токен/`os.environ.setdefault`. ### FR-2 — Разрешение только в sandbox при явном аудируемом opt-in (BR-2, BR-3, BR-5) Запись из тест-процесса допускается ⇔ **одновременно**: (а) включён выделенный интеграционный opt-in-флаг **и** (б) целевой `project_id` ∈ sandbox-allowlist (по умолчанию — единственный `8c5a3025-4f9d-4190-b79f-fa06276bb27e`). При выполнении обоих условий примитив выполняет реальный `httpx`-вызов в SANDBOX. Отсутствие любого условия → блок (default-deny). Запись в боевой проект запрещена **даже при включённом opt-in** (allowlist sandbox-only). ### FR-3 — Дефолтная тестовая поза fail-closed (BR-4) При обычном `pytest tests/` (без явного opt-in) autouse-страховка `conftest.py` гарантирует, что **ни один** тест не пишет в Plane (все 3 примитива заблокированы/застаблены). Тесты sandbox-e2e, которым нужна реальная запись, **явно** включают opt-in в собственной фикстуре/монкипатче (поверх autouse), ограничивая реальную запись своим scope — паттерн уже применён для `_disable_merge_verify`/`_disable_transition_lease`/`_no_telegram` в `conftest.py`. ### FR-4 — Детект тест-процесса vs боевой/staging рантайм (NFR-2, NFR-3) Гард активен **только** в тест-процессе. Признак тест-процесса (например `PYTEST_CURRENT_TEST` в env / `pytest` в `sys.modules` / явный конфиг-флаг тест-режима — выбор за ADR) обязан: - **не** срабатывать в боевом рантайм-процессе оркестратора → боевая запись в Plane = байт-в-байт как прежде (no-op гарда); - **не** срабатывать в staging-рантайме (8501) → staging пишет в SANDBOX как прежде (staging — реальный процесс, не pytest). ### FR-5 — Аудит/наблюдаемость (BR-6) - Каждая **заблокированная** запись → структурный лог уровня WARNING/ERROR с полями: целевой `project_id`, `work_item`, операция (`state`/`comment`), причина (`prod-project-in-test` / `opt-in-disabled` / `ambiguous-target`). Сообщение должно делать инцидент класса ORCH-114 **очевидным**. - Каждая **разрешённая** sandbox-запись из тест-процесса → audit-строка (INFO) с `project_id` и операцией. - (Опц., на усмотрение архитектора) read-only-видимость состояния гарда (флаг/allowlist) — без обязательного нового эндпоинта. ### FR-6 — Поведение блокировки (NFR-5) - В **боевом пути** гард **never-raise**: его внутренний сбой/недоступность не роняет конвейер и не блокирует легитимную боевую запись (для боевого процесса гард в принципе no-op — FR-4). - В **тест-процессе** срабатывание гарда — **громкое**: запись подавляется и аудируется; допустимо жёсткое исключение/ассерт-фрэндли поведение, чтобы регресс-тест (TC-01) был детерминированно красным до фикса и зелёным после. Конкретная семантика (no-op-стаб vs raise) — решение ADR, но **наблюдаемый контракт**: «0 реальных PATCH/POST в боевой проект из pytest». ### FR-7 — Kill-switch без чёрного хода (NFR-6) Если вводится kill-switch гарда — он **не должен** при выключении переоткрывать запись в **боевой** проект из тест-процесса. Допустимое поведение «выключено» = деградация к прежнему (до-ORCH-117), но без молчаливого разрешения прод-записи из pytest сверх того, что было; запись в SANDBOX из тестов управляется **только** opt-in-флагом + allowlist (FR-2), а не общим kill-switch. ## 4. Изменения API **Нет** обязательных. Никаких новых публичных эндпоинтов изоляция не требует. (Опционально архитектор может добавить read-only-видимость состояния гарда, например блок в `GET /queue` — не обязательно.) ## 5. Изменения схемы БД **Нет.** Изоляция — рантайм-гард по конфигу/окружению; персистентного состояния не требует. Схема БД не трогается (NFR-2). ## 6. Требования к новым/изменённым QG checks **Нет.** Это **не** Quality Gate и **не** под-гейт. `QG_CHECKS` / `check_*` / `STAGE_TRANSITIONS` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`/ `coverage_status:`) — **байт-в-байт не тронуты** (инвариант ORCH-019 NFR-1: срезается/меняется только не-гейтовое поведение; здесь — изоляция записи). Гард — свойство клиента Plane, не гейт конвейера. ## 7. Совместимость / регресс - **Боевой рантайм:** гард — no-op (FR-4) → запись в Plane байт-в-байт как до ORCH-117 (NFR-2). - **Staging-рантайм (8501):** реальный процесс, sandbox-проект по конфигу → пишет в SANDBOX как прежде (NFR-3). `scripts/staging_check.py` Block C (E2E) должен остаться зелёным. - **Существующий тест-сьют:** autouse fail-closed-фикстура не должна ломать существующие тесты — большинство тестов **мокируют** `plane_*`/`add_comment` (например `tests/test_auto_labels_integration.py:58` `monkeypatch.setattr(stage_engine, "plane_add_comment", MagicMock())`), поэтому реальная запись и так не происходит; гард лишь делает это **гарантией по умолчанию**. Прежние неработающие `os.environ.setdefault("ORCH_PLANE_API_TOKEN","test-token")` строки можно не трогать — гард не зависит от них (NFR-4). - **Sandbox-e2e:** под явным opt-in + allowlist реальная запись в SANDBOX сохраняется (BR-5). - **Kill-switch:** при выключении гарда (если введён) — деградация к прежнему поведению, **без** переоткрытия прод-записи из тестов (FR-7/NFR-6). - **Обратимость:** дефолты безопасные (fail-closed в тестах); включение реальной записи — только явным opt-in. - **Артефакты pipeline:** создаёт/обновляет `docs/work-items/ORCH-117/06-adr/ADR-001-*.md` (architect, после эскалации), `10-tech-risks.md`; в этом PR — обновление `.env.example`, `CLAUDE.md`, `docs/architecture/README.md`, `docs/operations/INFRA.md`.