From c0bd751acf64a819cf7f2227248ab5b72dc18586 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 15 Jun 2026 20:39:41 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=716 --- docs/work-items/ORCH-117/01-brd.md | 213 ++++++++++++++++++ docs/work-items/ORCH-117/02-trz.md | 132 +++++++++++ .../ORCH-117/03-acceptance-criteria.md | 145 ++++++++++++ docs/work-items/ORCH-117/04-test-plan.yaml | 113 ++++++++++ 4 files changed, 603 insertions(+) create mode 100644 docs/work-items/ORCH-117/01-brd.md create mode 100644 docs/work-items/ORCH-117/02-trz.md create mode 100644 docs/work-items/ORCH-117/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-117/04-test-plan.yaml diff --git a/docs/work-items/ORCH-117/01-brd.md b/docs/work-items/ORCH-117/01-brd.md new file mode 100644 index 0000000..0c136c8 --- /dev/null +++ b/docs/work-items/ORCH-117/01-brd.md @@ -0,0 +1,213 @@ +--- +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 +--- + +# 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.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_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-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` (заполняет архитектор). diff --git a/docs/work-items/ORCH-117/02-trz.md b/docs/work-items/ORCH-117/02-trz.md new file mode 100644 index 0000000..d87dc65 --- /dev/null +++ b/docs/work-items/ORCH-117/02-trz.md @@ -0,0 +1,132 @@ +--- +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`. diff --git a/docs/work-items/ORCH-117/03-acceptance-criteria.md b/docs/work-items/ORCH-117/03-acceptance-criteria.md new file mode 100644 index 0000000..3a510ce --- /dev/null +++ b/docs/work-items/ORCH-117/03-acceptance-criteria.md @@ -0,0 +1,145 @@ +--- +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 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-117 — sandbox-only fail-closed изоляция записи в Plane + +Work Item: **ORCH-117** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** +(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам +репозитория. + +--- + +## AC-1 — Регресс ORCH-114: живой прод-токен + pytest не мутируют боевой проект (ОБЯЗАТЕЛЬНЫЙ) + +**Условие:** В тест-процессе с **живым боевым** `ORCH_PLANE_API_TOKEN` в окружении вызывается +`notify_stage_change("ORCH-114", "deploy", "done")` (и/или прямые `update_issue_state`/`add_comment`/ +`_set_issue_state_direct`) с целевым боевым проектом `7a79f0a9-5278-49cd-9007-9a338f238f9c`. +- **PASS:** **ноль** реальных `httpx.patch`/`httpx.post` уходит в боевой проект (мок `httpx` не + вызван для prod-URL **или** гард блокирует до сетевого вызова). Существует тест, который **красный + до фикса** (воспроизводит запись инцидента) и **зелёный после**. +- **FAIL:** хоть один PATCH/POST достигает боевого проекта из тест-процесса; либо регресс-тест + отсутствует; либо он зелёный и до фикса (значит ничего не проверяет). + +--- + +## AC-2 — Sandbox-e2e сохранён: запись в SANDBOX под opt-in проходит + +**Условие:** В тест-процессе включён явный интеграционный opt-in **и** целевой проект — SANDBOX +`8c5a3025-4f9d-4190-b79f-fa06276bb27e`. +- **PASS:** примитивы записи выполняют реальный `httpx`-вызов в SANDBOX (в тесте — через мок, + подтверждающий, что вызов разрешён и адресован sandbox-URL); `scripts/staging_check.py` Block C + (E2E в SANDBOX) остаётся работоспособным. +- **FAIL:** запись в SANDBOX заблокирована при корректном opt-in; либо sandbox-e2e сломан. + +--- + +## AC-3 — Sandbox-only даже с opt-in: боевой проект запрещён всегда + +**Условие:** В тест-процессе включён opt-in, но целевой проект — боевой (`7a79f0a9-…` ORCH или +боевой enduro-проект). +- **PASS:** запись блокируется (allowlist sandbox-only) независимо от opt-in; аудит-лог фиксирует + причину `prod-project-in-test`. +- **FAIL:** включённый opt-in разрешает запись в любой проект, включая боевой. + +--- + +## AC-4 — Default-deny: без opt-in запись из тестов заблокирована (fail-closed) + +**Условие:** Обычный `pytest tests/` без явного opt-in; целевой проект — любой (sandbox или боевой). +- **PASS:** все 3 примитива (`update_issue_state`, `add_comment`, `_set_issue_state_direct`) не + делают реальной записи; autouse-фикстура `conftest.py` обеспечивает это по умолчанию во всех + тестах. Неопределённый/неразрешимый целевой проект → блок (NFR-1). +- **FAIL:** без opt-in возможна реальная запись в Plane; либо autouse-страховка отсутствует. + +--- + +## AC-5 — Нулевая регрессия боевого рантайма + +**Условие:** Процесс — **не** pytest (боевой рантайм оркестратора). +- **PASS:** гард — no-op; запись в Plane выполняется как до ORCH-117 (тот же URL/headers/payload). + `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict ключи/схема БД — **байт-в-байт не + изменены** (проверяемо diff'ом / структурными тестами). +- **FAIL:** боевая запись подавлена/изменена; либо изменены гейты/схема БД. + +--- + +## AC-6 — Staging-рантайм пишет в SANDBOX + +**Условие:** Процесс — staging-рантайм (8501), **не** pytest; сконфигурирован на sandbox-проект. +- **PASS:** детект тест-процесса **не** срабатывает для staging → запись в SANDBOX проходит как + прежде; staging не приравнивается к pytest. +- **FAIL:** staging-запись в SANDBOX заблокирована (ложноположительный детект тест-процесса). + +--- + +## AC-7 — Устойчивость к захвату токена на импорте + +**Условие:** `PLANE_HEADERS`/`PROJECT_ID` захвачены на импорте `plane_sync` (стр. 17/57); тест +выполняет запись, не подменяя токен/env постфактум. +- **PASS:** гард срабатывает **на момент вызова** примитива и блокирует прод-запись независимо от + того, какой токен в `PLANE_HEADERS`; защита **не** опирается на `os.environ.setdefault(...)`/ + подмену `settings.plane_api_token`. +- **FAIL:** защита зависит от подмены токена/env и потому не срабатывает в проде (как было до фикса). + +--- + +## AC-8 — Аудит/наблюдаемость блокировок и разрешений + +**Условие:** Происходит блокировка записи (или разрешённая sandbox-запись). +- **PASS:** на блокировку эмитится структурный WARNING/ERROR с `project_id`, `work_item`, операцией и + причиной; на разрешённую sandbox-запись — audit-INFO. Сообщения делают инцидент видимым. +- **FAIL:** блокировка/разрешение происходят молча (нет логов с требуемыми полями). + +--- + +## AC-9 — Kill-switch без чёрного хода + +**Условие:** Если введён kill-switch гарда — он выключен. +- **PASS:** выключение деградирует к прежнему (до-ORCH-117) поведению, но **не** разрешает молча + запись в **боевой** проект из pytest сверх того, что было; реальная sandbox-запись из тестов + управляется только opt-in + allowlist (не общим kill-switch). +- **FAIL:** общий kill-switch служит чёрным ходом, переоткрывающим прод-запись из тест-процесса. + +--- + +## AC-10 — Документация и конфиг обновлены (golden source) + +**Условие:** PR закрывает ORCH-117. +- **PASS:** обновлены `.env.example` (новые `ORCH_*` ключи с безопасными дефолтами), `CLAUDE.md`, + `docs/architecture/README.md`, `docs/operations/INFRA.md` — описан инвариант изоляции тест/staging + записи в Plane. ADR выпущен (`06-adr/ADR-001-*.md`). +- **FAIL:** код есть, документация/конфиг не обновлены (по правилу reviewer'а ORCH — finding ≥P1). + +--- + +## AC-11 — Полный регресс зелёный + +**Условие:** `pytest tests/ -q` после фикса. +- **PASS:** весь сьют зелёный (автоматическая autouse-страховка не ломает существующие тесты). +- **FAIL:** появились падения/флапы из-за внедрённого гарда/фикстуры. + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1 / FR-6 (обязательный регресс ORCH-114) | +| AC-2 | BR-5 / FR-2 | +| AC-3 | BR-2 / FR-2 | +| AC-4 | BR-3 / BR-4 / FR-2 / FR-3 / NFR-1 | +| AC-5 | NFR-2 / FR-4 | +| AC-6 | NFR-3 / FR-4 | +| AC-7 | NFR-4 / FR-1 | +| AC-8 | BR-6 / FR-5 | +| AC-9 | NFR-6 / FR-7 | +| AC-10 | BR-7 | +| AC-11 | NFR-2 (регресс-нейтральность) | diff --git a/docs/work-items/ORCH-117/04-test-plan.yaml b/docs/work-items/ORCH-117/04-test-plan.yaml new file mode 100644 index 0000000..81c5601 --- /dev/null +++ b/docs/work-items/ORCH-117/04-test-plan.yaml @@ -0,0 +1,113 @@ +work_item: ORCH-117 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +title: "Sandbox-only fail-closed изоляция записи в Plane (регресс ORCH-114)" +framework: pytest +scope: > + Покрывает fail-closed гард записи в Plane: блок прод-записи из тест-процесса даже при живом + боевом токене (обязательный регресс ORCH-114), sandbox-only-разрешение под явным opt-in, + default-deny по умолчанию, отличие тест-процесса от боевого/staging рантайма, перехват на + момент вызова (устойчивость к захвату токена на импорте), аудит. ВНЕ покрытия: реальные сетевые + вызовы к боевому Plane (запрещены самим фиксом) — всё через мок httpx; выбор механизма детекта — + зона ADR (тесты проверяют поведение, не реализацию). +notes: > + TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс инцидента ORCH-114: красный до фикса, зелёный после. Все три + примитива записи (update_issue_state / add_comment / _set_issue_state_direct) проверяются на + блок/разрешение через мок httpx.patch/httpx.post (никаких реальных сетевых вызовов). Полный + регресс tests/ должен оставаться зелёным (autouse fail-closed-фикстура не ломает существующие + тесты, большинство из которых уже мокируют plane_*/add_comment). Боевой ID проекта в тестах — + 7a79f0a9-5278-49cd-9007-9a338f238f9c; sandbox — 8c5a3025-4f9d-4190-b79f-fa06276bb27e. + +tests: + - id: TC-01 + type: integration + description: "РЕГРЕСС ORCH-114: pytest-env + живой прод-токен → notify_stage_change('ORCH-114','deploy','done') на боевой проект НЕ делает ни одного httpx.patch/post (мок httpx не вызван для prod-URL / гард блокирует). Красный до фикса, зелёный после." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-02 + type: unit + description: "update_issue_state в тест-процессе с целевым боевым проектом 7a79f0a9-… → блок (httpx.patch не вызван); аудит-причина prod-project-in-test." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-03 + type: unit + description: "add_comment в тест-процессе с боевым проектом → блок (httpx.post не вызван)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-04 + type: unit + description: "_set_issue_state_direct в тест-процессе с боевым проектом → блок (httpx.patch не вызван). Покрывает все set_issue_* (Done/In Review/Blocked/…), сводящиеся к этому примитиву." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-05 + type: unit + description: "Default-deny: без явного opt-in запись в тест-процессе блокируется для ЛЮБОГО целевого проекта (в т.ч. sandbox)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-06 + type: unit + description: "Sandbox-разрешение: opt-in включён + целевой проект SANDBOX 8c5a3025-… → реальный httpx-вызов разрешён и адресован sandbox-URL (мок подтверждает вызов)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-07 + type: unit + description: "Sandbox-only даже с opt-in: opt-in включён, но целевой проект боевой → блок (allowlist sandbox-only), независимо от opt-in." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-08 + type: unit + description: "Fail-closed при неопределённости: целевой project_id неразрешим/пуст в тест-процессе → блок (NFR-1 'не знаю ⇒ не пишу')." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-09 + type: unit + description: "Устойчивость к захвату на импорте: PLANE_HEADERS содержит реальный токен, env/settings не подменяются постфактум → гард всё равно блокирует прод-запись на момент вызова (не зависит от os.environ.setdefault / подмены plane_api_token)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-10 + type: unit + description: "Нулевая регрессия боевого рантайма: при имитации НЕ-pytest процесса гард = no-op, httpx.patch/post вызывается с прежним URL/headers/payload (запись в Plane как до ORCH-117)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-11 + type: unit + description: "Staging != pytest: имитация staging-рантайма (sandbox-проект, не тест-процесс) → запись в SANDBOX проходит (детект тест-процесса не срабатывает ложно)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-12 + type: unit + description: "Аудит: на блокировку эмитится структурный WARNING/ERROR с project_id/work_item/операцией/причиной (caplog); на разрешённую sandbox-запись — audit-INFO." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-13 + type: integration + description: "Дефолтная autouse-страховка conftest: репрезентативный advance стадии в обычном тесте не делает реальной записи в боевой Plane (страховка активна по умолчанию для всего сьюта)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-14 + type: unit + description: "Kill-switch без чёрного хода: при выключенном kill-switch гарда запись в БОЕВОЙ проект из pytest всё равно не разрешается молча (реальная sandbox-запись управляется только opt-in+allowlist)." + module: tests/test_orch117_plane_write_isolation.py + expected: PASS + + - id: TC-15 + type: integration + description: "Полный регресс tests/ зелёный — внедрённая autouse fail-closed-фикстура не ломает существующие тесты (smoke: pytest tests/ -q)." + module: tests/ + expected: PASS