ORCH-117: sandbox-only fail-closed guard for Plane writes from test process (fix regression ORCH-114) #139

Merged
admin merged 7 commits from feature/ORCH-117-bug-test-staging-plane-writes- into main 2026-06-15 22:32:06 +03:00
Owner

ORCH-117 — sandbox-only fail-closed изоляция записи в Plane (регресс ORCH-114)

Тип: fix (bug → escalate full-cycle). Закрывает корневой класс инцидента ORCH-114: тест/worktree-процесс выполнил РЕАЛЬНУЮ запись (PATCH …/issues/… state=<Done> + комментарий «Stage: deploy → done») против боевого Plane-проекта, т.к. тест/staging-процессы наследуют живой боевой Plane-токен (PLANE_HEADERS/PROJECT_ID захвачены литералами на импорте plane_sync — подмена env/токена постфактум бесполезна), и ничто не принуждало их писать только в sandbox. Симметрия прецеденту tests/conftest.py::_no_telegram.

Решение (по ADR-001 / adr-0046)

  • Новый чистый leaf src/plane_write_guard.py (never-raise в боевом пути, паттерн deploy_status_guard/serial_gate): decide(project_id, op, work_item) -> (ALLOW|BLOCK, reason) + audit_block/audit_allow.
  • Врезан в 3 примитива записи plane_sync (update_issue_state / add_comment / _set_issue_state_direct) через хелпер _guard_allows_writeна момент вызова, сразу после _resolve_project_id и до любого сетевого шага (ни GET, ни PATCH/POST).
  • Активен только в тест-процессе ("pytest" in sys.modules / PYTEST_CURRENT_TEST); боевой и staging рантаймы (uvicorn src.main:app) — строгий no-op, byte-for-byte.
  • В тест-процессе default-deny: запись разрешена ⇔ opt-in plane_test_write_enabled И проект ∈ sandbox-allowlist plane_test_sandbox_projects (дефолт = единственный SANDBOX). Боевой проект запрещён даже при opt-in (allowlist sandbox-only); нерезолвимый проект → блок (fail-closed).
  • Второй независимый sandbox-bound слой — autouse-floor tests/conftest.py::_plane_sandbox_only (opt-in OFF для всего сьюта).
  • Умышленно без prod-блок kill-switch (NFR-6 / anti-drift): выключателя-чёрного-хода нет.
  • Аудит: блок → громкий структурный ERROR; sandbox-allow → INFO.

Инвариант сохранён

STAGE_TRANSITIONS / реестр QG_CHECKS / семантика и имена check_* / machine-verdict-ключи / схема БД — байт-в-байт не тронуты (это изоляция клиента Plane, не Quality Gate и не стадия).

Тесты / документация

  • tests/test_orch117_plane_write_isolation.pyTC-01 обязательный регресс ORCH-114 (красный до врезки, зелёный после) + TC-02…TC-14. Бэйпас-фикстуры добавлены 4 pre-existing тестам, асёртящим mocked-httpx у примитивов записи.
  • Полный регресс: 2068 passed (pytest tests/ -q -p no:cacheprovider --strict-markers).
  • Docs: CLAUDE.md, docs/architecture/README.md, docs/operations/INFRA.md, .env.example (ORCH_PLANE_TEST_WRITE_ENABLED / ORCH_PLANE_TEST_SANDBOX_PROJECTS), CHANGELOG.md.

ADR: docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md, сквозной docs/architecture/adr/adr-0046-sandbox-only-plane-write-guard.md.

Refs: ORCH-117

🤖 Generated with Claude Code

## ORCH-117 — sandbox-only fail-closed изоляция записи в Plane (регресс ORCH-114) **Тип:** `fix` (bug → escalate full-cycle). Закрывает корневой класс инцидента **ORCH-114**: тест/worktree-процесс выполнил РЕАЛЬНУЮ запись (`PATCH …/issues/… state=<Done>` + комментарий «Stage: deploy → done») против **боевого** Plane-проекта, т.к. тест/staging-процессы наследуют живой боевой Plane-токен (`PLANE_HEADERS`/`PROJECT_ID` захвачены литералами **на импорте** `plane_sync` — подмена env/токена постфактум бесполезна), и **ничто** не принуждало их писать только в sandbox. Симметрия прецеденту `tests/conftest.py::_no_telegram`. ### Решение (по ADR-001 / adr-0046) - **Новый чистый leaf `src/plane_write_guard.py`** (never-raise в боевом пути, паттерн `deploy_status_guard`/`serial_gate`): `decide(project_id, op, work_item) -> (ALLOW|BLOCK, reason)` + `audit_block`/`audit_allow`. - Врезан в **3 примитива записи** `plane_sync` (`update_issue_state` / `add_comment` / `_set_issue_state_direct`) через хелпер `_guard_allows_write` — **на момент вызова**, сразу после `_resolve_project_id` и **до** любого сетевого шага (ни GET, ни PATCH/POST). - Активен **только в тест-процессе** (`"pytest" in sys.modules` / `PYTEST_CURRENT_TEST`); боевой и staging рантаймы (`uvicorn src.main:app`) — строгий **no-op, byte-for-byte**. - В тест-процессе **default-deny**: запись разрешена ⇔ opt-in `plane_test_write_enabled` **И** проект ∈ sandbox-allowlist `plane_test_sandbox_projects` (дефолт = единственный SANDBOX). Боевой проект запрещён **даже при opt-in** (allowlist sandbox-only); нерезолвимый проект → блок (fail-closed). - Второй независимый sandbox-bound слой — autouse-floor `tests/conftest.py::_plane_sandbox_only` (opt-in OFF для всего сьюта). - **Умышленно без prod-блок kill-switch** (NFR-6 / anti-drift): выключателя-чёрного-хода нет. - Аудит: блок → громкий структурный ERROR; sandbox-allow → INFO. ### Инвариант сохранён `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты** (это изоляция клиента Plane, не Quality Gate и не стадия). ### Тесты / документация - `tests/test_orch117_plane_write_isolation.py` — **TC-01** обязательный регресс ORCH-114 (красный до врезки, зелёный после) + TC-02…TC-14. Бэйпас-фикстуры добавлены 4 pre-existing тестам, асёртящим mocked-httpx у примитивов записи. - Полный регресс: **2068 passed** (`pytest tests/ -q -p no:cacheprovider --strict-markers`). - Docs: `CLAUDE.md`, `docs/architecture/README.md`, `docs/operations/INFRA.md`, `.env.example` (`ORCH_PLANE_TEST_WRITE_ENABLED` / `ORCH_PLANE_TEST_SANDBOX_PROJECTS`), `CHANGELOG.md`. ADR: `docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md`, сквозной `docs/architecture/adr/adr-0046-sandbox-only-plane-write-guard.md`. Refs: ORCH-117 🤖 Generated with [Claude Code](https://claude.com/claude-code)
admin added 6 commits 2026-06-15 21:32:22 +03:00
Close the root class of incident ORCH-114: a pytest/worktree process performed a
REAL write (PATCH issues state=<Done> + comment) against the PRODUCTION Plane
project, because test/staging processes inherit the live Plane token
(PLANE_HEADERS/PROJECT_ID are captured at import — a post-hoc env/token swap is a
no-op) and nothing forced them to write only to the sandbox. Symmetric to the
existing _no_telegram autouse floor.

- New pure never-raise leaf src/plane_write_guard.py (decide/audit_block/
  audit_allow), wired into the 3 plane_sync write primitives (update_issue_state /
  add_comment / _set_issue_state_direct) via _guard_allows_write, AT CALL TIME,
  before any network step. Active ONLY in a test process (pytest in sys.modules /
  PYTEST_CURRENT_TEST); live + staging runtimes (uvicorn) are a strict no-op.
- In a test process: default-deny. A write is allowed iff opt-in
  (plane_test_write_enabled) AND target project in the sandbox allowlist
  (plane_test_sandbox_projects, default = the one SANDBOX id). Prod is blocked even
  with opt-in (allowlist sandbox-only); unresolved project -> block (fail-closed).
- Independent second layer: tests/conftest.py::_plane_sandbox_only autouse floor.
  Intentionally NO prod-block kill-switch (anti back-door, NFR-6).
- Audit: block -> loud ERROR; sandbox-allow -> INFO.
- Bypass fixtures for the 3 (+1) pre-existing tests that assert on the mocked
  write primitive's httpx call (header/URL/state logic), the guard is no Quality
  Gate: STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict / DB schema
  untouched.
- Tests: tests/test_orch117_plane_write_isolation.py (TC-01 mandatory ORCH-114
  regression + TC-02..TC-14). Docs: CLAUDE.md, architecture/README.md,
  operations/INFRA.md, .env.example, CHANGELOG.md.

Refs: ORCH-117
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tester(ET): auto-commit from tester run_id=720
All checks were successful
CI / test (push) Successful in 1m13s
CI / test (pull_request) Successful in 1m13s
f26908ffc4
admin force-pushed feature/ORCH-117-bug-test-staging-plane-writes- from fd3a37855c to f26908ffc4 2026-06-15 21:32:22 +03:00 Compare
admin merged commit 13589fcaf4 into main 2026-06-15 22:32:06 +03:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/orchestrator#139