Files
orchestrator/docs/work-items/ORCH-117/02-trz.md

133 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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`.