analyst(ET): auto-commit from analyst run_id=716
This commit is contained in:
213
docs/work-items/ORCH-117/01-brd.md
Normal file
213
docs/work-items/ORCH-117/01-brd.md
Normal file
@@ -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` (заполняет архитектор).
|
||||
132
docs/work-items/ORCH-117/02-trz.md
Normal file
132
docs/work-items/ORCH-117/02-trz.md
Normal file
@@ -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`.
|
||||
145
docs/work-items/ORCH-117/03-acceptance-criteria.md
Normal file
145
docs/work-items/ORCH-117/03-acceptance-criteria.md
Normal file
@@ -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 (регресс-нейтральность) |
|
||||
113
docs/work-items/ORCH-117/04-test-plan.yaml
Normal file
113
docs/work-items/ORCH-117/04-test-plan.yaml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user