133 lines
14 KiB
Markdown
133 lines
14 KiB
Markdown
---
|
||
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`.
|