From 9538103eff888ba0e4cd4586432629283315f9a0 Mon Sep 17 00:00:00 2001 From: Slava Date: Sat, 6 Jun 2026 08:03:16 +0300 Subject: [PATCH 1/7] docs: init ORCH-048 business request --- docs/work-items/ORCH-048/00-business-request.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/work-items/ORCH-048/00-business-request.md diff --git a/docs/work-items/ORCH-048/00-business-request.md b/docs/work-items/ORCH-048/00-business-request.md new file mode 100644 index 0000000..61cb4cd --- /dev/null +++ b/docs/work-items/ORCH-048/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: staging B6 check reads registry from host worktree, not staging container + +Work Item ID: ORCH-048 + +## Description + +TBD From 8b5b1f00569fd71fde623fefaa9e397bfb54a7be Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 6 Jun 2026 05:06:33 +0000 Subject: [PATCH 2/7] analyst(ET): auto-commit from analyst run_id=145 --- docs/work-items/ORCH-048/01-brd.md | 86 ++++++++++++++++ docs/work-items/ORCH-048/02-trz.md | 95 ++++++++++++++++++ .../ORCH-048/03-acceptance-criteria.md | 67 +++++++++++++ docs/work-items/ORCH-048/04-test-plan.yaml | 97 +++++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100644 docs/work-items/ORCH-048/01-brd.md create mode 100644 docs/work-items/ORCH-048/02-trz.md create mode 100644 docs/work-items/ORCH-048/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-048/04-test-plan.yaml diff --git a/docs/work-items/ORCH-048/01-brd.md b/docs/work-items/ORCH-048/01-brd.md new file mode 100644 index 0000000..fad309d --- /dev/null +++ b/docs/work-items/ORCH-048/01-brd.md @@ -0,0 +1,86 @@ +# 01 — Business Requirements Document (BRD) + +**Work Item:** ORCH-048 +**Title:** staging B6 check reads registry from host worktree, not staging container +**Stage:** analysis +**Author:** analyst +**Date:** 2026-06-06 + +--- + +## 1. Контекст и проблема + +`scripts/staging_check.py` — suite живых проверок staging-стенда orchestrator (порт 8501, ADR-0003). Деплоер запускает его на стадии `deploy-staging` и пишет `staging_status:` в `15-staging-log.md`. FAIL любого чека = откат на `development`. + +Блок B содержит проверку **B6 «Registry: sandbox present, prod ET/ORCH absent»** — она должна подтверждать, что в staging-реестре проектов есть только sandbox-проект и НЕТ боевых проектов (enduro-trails / orchestrator). Это страховка изоляции: staging не должен обслуживать прод-проекты. + +**B6 даёт ложный FAIL** (`prod-ET=YES(BAD!)`, `prod-ORCH=YES(BAD!)`), хотя сама изоляция реестра в staging РАБОТАЕТ корректно. + +### Root cause (подтверждён прямым запуском, Стрим, 06.06) + +- Внутри контейнера `orchestrator-staging` `known_plane_project_ids()` корректно отдаёт `count=1, sandbox=True, ET=False, ORCH=False`. `.env.staging` верно задаёт `ORCH_PROJECTS_JSON` = только sandbox. **Изоляция реестра исправна.** +- Все остальные чеки (A1–A3, B4, B5, блок C E2E) обращаются к работающему staging-инстансу по HTTP и **зелёные**. +- **B6 — единственный чек, который не ходит по HTTP, а импортирует Python-код локально.** В блоке B6 (строки ~263–284) выполняется: + ```python + sys.path.insert(0, "/repos/orchestrator") # ХОСТ-worktree + importlib.reload(sys.modules["src.projects"]) # подхватывает env ТЕКУЩЕГО процесса + from src.projects import known_plane_project_ids + ``` +- Деплоер по факту запускает скрипт **с хоста** (`.openclaw/agents/deployer.md`: `python3 scripts/staging_check.py --base-url http://localhost:8501`). В env хост-процесса `ORCH_PROJECTS_JSON` НЕ задан → `src.projects` грузит встроенный `_DEFAULT_PROJECTS` (ET + ORCH) → `known_plane_project_ids()` возвращает боевые id → **ложный FAIL**. +- Иными словами, B6 проверяет реестр НЕ того окружения, реестр которого реально использует staging-инстанс. Гипотеза деплоера про misconfig staging-контейнера — **опровергнута**. + +## 2. Бизнес-цель + +B6 должен достоверно отражать реестр проектов **именно работающего staging-инстанса** (изолированное окружение), а не реестр, восстановленный из локального импорта в произвольном process-env. При этом B6 обязан по-прежнему ловить реальное нарушение изоляции. + +## 3. Заинтересованные стороны + +| Роль | Интерес | +|------|---------| +| Deployer-агент | Достоверный сигнал staging-гейта; нет ложных откатов на development | +| Owner / прод | Изоляция staging от прод-проектов реально проверяется (не ложно-зелёная и не ложно-красная) | +| Self-hosting pipeline | `deploy-staging` — обязательная страховка перед прод-деплоем орка; ложный FAIL блокирует доставку всех ORCH-задач | + +## 4. Объём (Scope) + +### В объёме +- Исправление блока B6 в `scripts/staging_check.py`, чтобы он читал реестр в окружении staging-инстанса. +- Тест на корректность B6: оба исхода (PASS при чистой изоляции; FAIL при попадании прод-проекта в staging-реестр). +- Обновление документации B6 (`docs/operations/STAGING_CHECK.md`, при необходимости `docs/architecture/README.md`/CHANGELOG) в том же PR. + +### Вне объёма (НЕ ТРОГАТЬ) +- `src/projects.py` — реестр работает корректно. +- `.env` / `.env.staging` — конфигурация верна. +- Прод-логика оркестратора. +- Остальные staging-чеки B1–B5 и блок C E2E — зелёные. + +## 5. Бизнес-требования + +| ID | Требование | +|----|------------| +| BR-1 | B6 на staging даёт PASS (`sandbox=YES`, `prod-ET=NO`, `prod-ORCH=NO`), читая реестр из окружения staging-инстанса, а не из локального импорта хост-worktree. | +| BR-2 | B6 по-прежнему детектирует реальное нарушение изоляции: если бы прод-проект реально попал в staging-реестр, B6 обязан выдать FAIL. | +| BR-3 | Остальные staging-чеки не сломаны; `src/projects.py` и `.env*` не изменяются. | +| BR-4 | Существующие unit-тесты остаются зелёными (`pytest tests/ -q`). | +| BR-5 | Документация B6 обновлена в том же PR (golden source). | + +## 6. Допущения и ограничения + +- Решение должно быть минимально инвазивным и не затрагивать прод-логику. +- Скрипт `scripts/staging_check.py` использует только stdlib (нет `requests`/`httpx`) — это конвенция файла, её нужно сохранить. +- Способ запуска suite может варьироваться (с хоста / `docker exec` внутри контейнера) — выбранное решение должно быть корректным для канонического способа запуска деплоером и задокументировано. + +## 7. Критерий успеха (бизнес) + +- staging-прогон `scripts/staging_check.py` → **B6 PASS** при работающей изоляции. +- При искусственно нарушенной изоляции → **B6 FAIL** (проверяется тестом, без реального изменения staging). +- `python -m pytest tests/ -q` — зелёный. + +## 8. Открытые вопросы (для архитектора) + +Бизнес-запрос предлагает три варианта реализации (выбор за архитектором, см. 02-trz §4): +- (а) B6 читает реестр через HTTP-эндпоинт staging-инстанса; +- (б) B6 выполняет проверку через subprocess в окружении staging-контейнера (`docker exec`); +- (в) staging_check запускается ВНУТРИ staging-контейнера и читает собственный process-env (убрать host-path хак). + +Предпочтение бизнес-запроса: минимально инвазивный вариант, не трогающий прод-логику. diff --git a/docs/work-items/ORCH-048/02-trz.md b/docs/work-items/ORCH-048/02-trz.md new file mode 100644 index 0000000..5117ceb --- /dev/null +++ b/docs/work-items/ORCH-048/02-trz.md @@ -0,0 +1,95 @@ +# 02 — Техническое задание (ТЗ / TRZ) + +**Work Item:** ORCH-048 +**Title:** staging B6 check reads registry from host worktree, not staging container +**Stage:** analysis +**Author:** analyst +**Date:** 2026-06-06 + +> Это ТЗ фиксирует требования и инварианты. Выбор одного из трёх архитектурных вариантов (§4) — за архитектором (ADR). Анализ варианты НЕ выбирает. + +--- + +## 1. Задействованные модули + +| Путь | Роль | Характер изменений | +|------|------|--------------------| +| `scripts/staging_check.py` | Suite живых staging-проверок; блок B6 (~строки 263–284) | **Изменяется** — переписать механику получения реестра в B6 | +| `tests/` (новый файл, напр. `tests/test_staging_check_b6.py`) | Unit-тест корректности B6 | **Создаётся** | +| `docs/operations/STAGING_CHECK.md` | Док запуска suite | **Обновляется** (описание B6 + способ запуска) | +| `docs/architecture/README.md` / `CHANGELOG.md` | Golden source | **Обновляется** при необходимости | + +### НЕ изменять (жёсткий инвариант scope) +- `src/projects.py` — реестр работает корректно. +- `.env`, `.env.staging`, `.env.example` — конфиг верен. +- Прод-логику оркестратора (`src/main.py` прод-роуты, `src/webhooks/*`, `src/stage_engine.py`, `src/qg/*`) — кроме случая варианта (а), если архитектор решит добавить read-only эндпоинт (см. §4а, отдельно обоснованный риск). +- Блоки A1–A3, B4, B5 и блок C E2E в `staging_check.py`. + +## 2. Текущее поведение (то, что чиним) + +Блок B6 (`scripts/staging_check.py`): +```python +sys.path.insert(0, "/repos/orchestrator") # хост-worktree path +import importlib +if "src.projects" in sys.modules: + importlib.reload(sys.modules["src.projects"]) # перечитывает env ТЕКУЩЕГО процесса +from src.projects import known_plane_project_ids +known = known_plane_project_ids() +``` +Проблема: реестр строится из `ORCH_PROJECTS_JSON` **process-env того процесса, в котором исполняется скрипт**. При запуске деплоером с хоста (`python3 scripts/staging_check.py --base-url http://localhost:8501`) переменная не задана → `_DEFAULT_PROJECTS` (ET+ORCH) → ложный FAIL. B6 не отражает реестр работающего staging-инстанса. + +## 3. Требуемое поведение (контракт B6) + +| ID | Требование | +|----|------------| +| TR-1 | B6 определяет набор «известных staging-инстансу Plane project id» из источника, который **гарантированно отражает окружение работающего staging-инстанса** (порт 8501 / контейнер `orchestrator-staging`), а не из локального импорта в process-env скрипта. | +| TR-2 | B6 PASS ⟺ `SANDBOX_PROJECT_ID ∈ known` И `PROD_ET_PROJECT_ID ∉ known` И `PROD_ORCH_PROJECT_ID ∉ known`. Идентификаторы — те же константы, что уже в скрипте. | +| TR-3 | B6 сохраняет формат вывода `Results.add(label, passed, detail)` с человекочитаемым detail (`sandbox=…, prod-ET=…, prod-ORCH=…`). | +| TR-4 | При недоступности источника реестра B6 даёт **детерминированный FAIL** с понятным detail (не падает с необработанным исключением, не даёт ложный PASS). | +| TR-5 | Скрипт остаётся на stdlib (без сторонних зависимостей), если выбранный вариант это допускает. | +| TR-6 | Удаляется зависимость B6 от хардкод-пути `/repos/orchestrator` для построения реестра (host-path хак), несовместимого с целью проверки. | + +## 4. Варианты реализации (выбор — архитектор, в ADR) + +Бизнес-запрос предлагает три варианта. Анализ перечисляет их с известными плюсами/минусами; решение и обоснование — в `06-adr/`. + +### (а) HTTP-эндпоинт staging-инстанса +B6 запрашивает реестр у работающего staging-инстанса по HTTP (как делают A/B4/B5/C). +- **Сейчас подходящего эндпоинта НЕТ.** `/health`, `/status`, `/queue` реестр проектов не отдают (`src/main.py`). +- Потребуется добавить read-only эндпоинт (напр. `GET /projects`, отдающий `known_plane_project_ids()` или список репо/prefix). Это касается прод-`main.py` → выходит за «не трогать прод-логику», но изменение read-only и низкорисковое — архитектор взвешивает. +- Плюс: B6 гарантированно читает реестр именно того процесса, что обслуживает webhooks. Единый HTTP-стиль с остальными чеками. + +### (б) Subprocess в окружении staging-контейнера +B6 выполняет `docker exec orchestrator-staging python3 -c "from src.projects import known_plane_project_ids; ..."` и парсит stdout. +- Плюс: не трогает прод-`main.py`; читает env контейнера напрямую. +- Минус: требует доступности docker-CLI и имени контейнера из среды запуска suite; усложняет запуск «изнутри контейнера»; есть нюансы экранирования (см. `docs/history/LESSONS_2026-06-05.md`). + +### (в) Запуск suite внутри контейнера + чтение собственного process-env +Канонизировать запуск `staging_check.py` ВНУТРИ `orchestrator-staging` (`docker exec orchestrator-staging python3 …`), убрать `sys.path.insert(0, "/repos/orchestrator")`, импортировать `src.projects` из кода контейнера (его cwd/PYTHONPATH), env уже staging. +- Плюс: минимально инвазивно, не трогает прод-логику и `src.projects`; согласуется с «рекомендуемым способом запуска» в `STAGING_CHECK.md §Способы запуска.1`. +- Условие: деплоер должен запускать suite через `docker exec` (а не с хоста). Нужно синхронно обновить `.openclaw/agents/deployer.md` и `STAGING_CHECK.md`, иначе host-запуск воспроизведёт баг. +- Нюанс: внутри контейнера код лежит в `/app` (Dockerfile `COPY`), а `/repos/orchestrator` — отдельный mount; импорт должен резолвиться из кода, чьим env реально живёт инстанс. + +## 5. Изменения API + +- Варианты (б) и (в): **нет** изменений API. +- Вариант (а): новый read-only эндпоинт (напр. `GET /projects`) — точная схема ответа определяется архитектором. Если выбран — задокументировать в `docs/architecture/README.md` (таблица API) и `CHANGELOG.md`. + +## 6. Изменения схемы БД +Нет. + +## 7. Требования к новым QG checks +Нет новых QG. Поведение `check_staging_status` (ADR-0003) не меняется — меняется только достоверность одного из чеков suite, чей агрегат пишется в `15-staging-log.md`. + +## 8. Артефакты pipeline, создаваемые/обновляемые +- Код: `scripts/staging_check.py` (B6), новый тест в `tests/`. +- Док: `docs/operations/STAGING_CHECK.md`; при выборе варианта (а) — `docs/architecture/README.md` (API) и `CHANGELOG.md`; при выборе (в) — `.openclaw/agents/deployer.md` (способ запуска) и `STAGING_CHECK.md`. +- ADR: `docs/work-items/ORCH-048/06-adr/ADR-001-*.md` — обоснование выбранного варианта. + +## 9. Тестируемость +- Логика «PASS/FAIL по набору known id» B6 должна быть выделена в чистую, юнит-тестируемую функцию (напр. `_evaluate_b6(known: set[str]) -> tuple[bool, str]`), чтобы тест проверял оба исхода без поднятия staging-инстанса/docker. План — `04-test-plan.yaml`. + +## 10. Definition of Done +- BR-1…BR-5 (01-brd) выполнены. +- staging-прогон → B6 PASS; `pytest tests/ -q` зелёный. +- Док и (при необходимости) ADR обновлены в том же PR. diff --git a/docs/work-items/ORCH-048/03-acceptance-criteria.md b/docs/work-items/ORCH-048/03-acceptance-criteria.md new file mode 100644 index 0000000..1756935 --- /dev/null +++ b/docs/work-items/ORCH-048/03-acceptance-criteria.md @@ -0,0 +1,67 @@ +# 03 — Критерии приёмки (Acceptance Criteria) + +**Work Item:** ORCH-048 +**Title:** staging B6 check reads registry from host worktree, not staging container +**Stage:** analysis +**Author:** analyst +**Date:** 2026-06-06 + +Каждый критерий формулирует чёткое условие PASS/FAIL. Источник — бизнес-запрос ORCH-048 (AC-1…AC-4) + BRD. + +--- + +## AC-1 — B6 PASS на staging, читая реестр из staging-окружения + +**Условие PASS:** +- При staging-прогоне `scripts/staging_check.py` (канонический способ запуска, выбранный архитектором) чек **B6** выдаёт `✓ PASS` c detail `sandbox=YES, prod-ET=NO(good), prod-ORCH=NO(good)`. +- Набор known id, по которому судит B6, получен из окружения работающего staging-инстанса (HTTP-эндпоинт / docker-окружение контейнера / собственный process-env при запуске внутри контейнера), **не** из локального импорта `src.projects` в произвольном process-env с host-path хаком `/repos/orchestrator`. + +**FAIL, если:** B6 даёт ложный FAIL (`prod-ET=YES(BAD!)` / `prod-ORCH=YES(BAD!)`) при фактически исправной изоляции; либо реестр в B6 по-прежнему строится локальным импортом, зависящим от env процесса-запускателя. + +## AC-2 — B6 ловит РЕАЛЬНОЕ нарушение изоляции (оба исхода покрыты тестом) + +**Условие PASS:** +- Существует unit-тест, проверяющий логику вердикта B6 на **двух** входах: + 1. «чистый» staging-реестр (`known = {SANDBOX}`) → B6 вердикт **PASS**; + 2. «загрязнённый» реестр (например `known = {SANDBOX, PROD_ET}` и/или `{SANDBOX, PROD_ORCH}`) → B6 вердикт **FAIL**. +- Тест не требует поднятия живого staging-инстанса/docker (логика вердикта изолирована и тестируема, см. 02-trz §9). + +**FAIL, если:** покрыт только один исход; либо B6 даёт PASS при наличии прод-проекта в реестре (потеря защитной функции). + +## AC-3 — Остальные staging-чеки не сломаны; src/projects.py и .env не тронуты + +**Условие PASS:** +- Блоки A1–A3, B4, B5 и блок C (E2E) в `scripts/staging_check.py` функционально не изменены (формат вывода и логика прежние). +- `git diff` work item НЕ содержит изменений в `src/projects.py`, `.env`, `.env.staging`, `.env.example`. +- Прод-логика оркестратора не затронута. Исключение допускается только если архитектор в ADR выбрал вариант (а) и добавил read-only эндпоинт — тогда изменение ограничено добавлением этого эндпоинта, прод-поведение существующих роутов неизменно. + +**FAIL, если:** изменён `src/projects.py` или любой `.env*`; либо затронута/сломана логика прочих чеков. + +## AC-4 — Существующие unit-тесты зелёные + +**Условие PASS:** +- `python -m pytest tests/ -q` завершается с кодом 0; все ранее зелёные тесты остаются зелёными; новый тест B6 (AC-2) проходит. + +**FAIL, если:** любой тест падает. + +## AC-5 — Документация обновлена в том же PR (golden source) + +**Условие PASS:** +- `docs/operations/STAGING_CHECK.md` отражает исправленную механику B6 и канонический способ запуска suite. +- При выборе варианта (а): обновлены таблица API в `docs/architecture/README.md` и `CHANGELOG.md`. +- При выборе варианта (в): обновлены `.openclaw/agents/deployer.md` (запуск через `docker exec`) и `STAGING_CHECK.md`. +- Заведён ADR `docs/work-items/ORCH-048/06-adr/ADR-001-*.md` с обоснованием выбранного варианта. + +**FAIL, если:** код изменён, а соответствующая док/ADR не обновлены. + +--- + +## Сводная проверка (как мерить приёмку) + +| AC | Команда / действие | Ожидаемый результат | +|----|--------------------|---------------------| +| AC-1 | staging-прогон suite (выбранным способом) | `B6 … ✓ PASS [sandbox=YES, prod-ET=NO(good), prod-ORCH=NO(good)]` | +| AC-2 | `pytest tests/test_staging_check_b6.py -q` | оба кейса (clean→PASS, polluted→FAIL) зелёные | +| AC-3 | `git diff --name-only` по ветке | нет `src/projects.py`, нет `.env*`; чеки A/B4/B5/C не изменены по сути | +| AC-4 | `python -m pytest tests/ -q` | exit 0, все PASS | +| AC-5 | ревью diff документации | STAGING_CHECK.md + ADR-001 присутствуют и согласованы с кодом | diff --git a/docs/work-items/ORCH-048/04-test-plan.yaml b/docs/work-items/ORCH-048/04-test-plan.yaml new file mode 100644 index 0000000..38345fe --- /dev/null +++ b/docs/work-items/ORCH-048/04-test-plan.yaml @@ -0,0 +1,97 @@ +work_item: ORCH-048 +title: staging B6 check reads registry from host worktree, not staging container +stage: analysis +notes: > + B6 в staging_check.py должен оценивать реестр окружения работающего staging-инстанса. + Для тестируемости логика вердикта B6 выделяется в чистую функцию (напр. + _evaluate_b6(known: set[str]) -> tuple[bool, str]); тесты бьют именно её и не + поднимают живой staging-инстанс/docker. Идентификаторы — те же константы из скрипта: + SANDBOX_PROJECT_ID=8c5a3025-4f9d-4190-b79f-fa06276bb27e, + PROD_ET_PROJECT_ID=7a79f0a9-5278-49cd-9007-9a338f238f9c, + PROD_ORCH_PROJECT_ID=8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a. + +tests: + - id: TC-01 + type: unit + description: > + B6-вердикт PASS при чистом staging-реестре: known={SANDBOX} -> + passed=True, detail содержит sandbox=YES, prod-ET=NO, prod-ORCH=NO. (AC-1, AC-2) + module: tests/test_staging_check_b6.py + expected: PASS + + - id: TC-02 + type: unit + description: > + B6-вердикт FAIL при попадании прод-ET в реестр: known={SANDBOX, PROD_ET} -> + passed=False, detail помечает prod-ET как нарушение. (AC-2) + module: tests/test_staging_check_b6.py + expected: PASS + + - id: TC-03 + type: unit + description: > + B6-вердикт FAIL при попадании прод-ORCH в реестр: known={SANDBOX, PROD_ORCH} -> + passed=False, detail помечает prod-ORCH как нарушение. (AC-2) + module: tests/test_staging_check_b6.py + expected: PASS + + - id: TC-04 + type: unit + description: > + B6-вердикт FAIL при отсутствии sandbox в реестре: known=set() (пусто) -> + passed=False (sandbox absent), детерминированно, без исключения. (AC-2, TR-4) + module: tests/test_staging_check_b6.py + expected: PASS + + - id: TC-05 + type: unit + description: > + B6-вердикт FAIL при загрязнении и ET, и ORCH одновременно: + known={SANDBOX, PROD_ET, PROD_ORCH} -> passed=False. (AC-2) + module: tests/test_staging_check_b6.py + expected: PASS + + - id: TC-06 + type: unit + description: > + Источник реестра в B6 больше не зависит от host-path хака + sys.path.insert(0,"/repos/orchestrator"): проверить (статически/через структуру + кода или мок источника), что построение known не делается локальным импортом + src.projects из произвольного process-env. (AC-1, TR-6) + module: tests/test_staging_check_b6.py + expected: PASS + + - id: TC-07 + type: unit + description: > + Деградация источника реестра (HTTP-ошибка / недоступный контейнер / битый ответ) + -> B6 даёт детерминированный FAIL с понятным detail, а не ложный PASS и не + необработанное исключение. (TR-4) + module: tests/test_staging_check_b6.py + expected: PASS + + - id: TC-08 + type: unit + description: > + Регрессия реестра: существующие тесты src/projects.py остаются зелёными, + подтверждая, что src/projects.py не изменён. (AC-3, AC-4) + module: tests/test_projects.py + expected: PASS + + - id: TC-09 + type: integration + description: > + Полный прогон pytest без падений после правок: + `python -m pytest tests/ -q` -> exit 0. (AC-4) + module: tests/ + expected: PASS + + - id: TC-10 + type: integration + description: > + Живой staging-прогон (ручной, вне CI): запустить scripts/staging_check.py + выбранным архитектором способом против orchestrator-staging (8501) -> + B6 == PASS (sandbox=YES, prod-ET=NO, prod-ORCH=NO); блоки A/B4/B5/C не сломаны. + (AC-1, AC-3) Выполняется деплоером на стадии deploy-staging. + module: scripts/staging_check.py + expected: PASS From 7a6c7a0151b5d5ae44c36cae43b7a7b3e46c3ab1 Mon Sep 17 00:00:00 2001 From: stream Date: Sat, 6 Jun 2026 06:56:09 +0000 Subject: [PATCH 3/7] =?UTF-8?q?docs(ORCH-048):=20owner=20decision=20?= =?UTF-8?q?=E2=80=94=20pin=20variant=20(v),=20reject=20(a)=20HTTP-endpoint?= =?UTF-8?q?=20(chicken-egg)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/work-items/ORCH-048/02-trz.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/work-items/ORCH-048/02-trz.md b/docs/work-items/ORCH-048/02-trz.md index 5117ceb..bc4d8b2 100644 --- a/docs/work-items/ORCH-048/02-trz.md +++ b/docs/work-items/ORCH-048/02-trz.md @@ -49,6 +49,29 @@ known = known_plane_project_ids() | TR-5 | Скрипт остаётся на stdlib (без сторонних зависимостей), если выбранный вариант это допускает. | | TR-6 | Удаляется зависимость B6 от хардкод-пути `/repos/orchestrator` для построения реестра (host-path хак), несовместимого с целью проверки. | +## 4. Варианты реализации — РЕШЕНИЕ ВЛАДЕЛЬЦА (обязательно) + +> **РЕШЕНИЕ ПРИНЯТО ВЛАДЕЛЬЦЕМ ПРОЕКТА (Слава, 06.06): выбран ВАРИАНТ (в).** +> Архитектор НЕ выбирает заново — он фиксирует вариант (в) в ADR с обоснованием ниже. +> +> ### Почему (в), а НЕ (а) и НЕ (б) +> - **(а) HTTP-эндпоинт `GET /projects`** — ОТКЛОНЁН. Порождает «курицу-яйцо»: B6 ходит на эндпоинт **работающего** staging-инстанса, а эндпоинт запечён в Docker-образ → на первом прогоне его в живом инстансе ещё нет (404) → ложный FAIL → откат. Требует ручного bootstrap-деплоя. Это ровно тот класс поломки автономности, который мы устраняем. (Подтверждено на проде 06.06: `GET /projects` на 8501 → 404 → deploy-staging FAILED.) +> - **(б) `docker exec` subprocess** — ОТКЛОНЁН. Хрупкое экранирование (см. `docs/history/LESSONS_2026-06-05.md`), зависимость от docker-CLI и имени контейнера. +> - **(в) запуск suite ВНУТРИ staging-контейнера + чтение собственного process-env** — ВЫБРАН. B6 не зависит от того, что отдаёт инстанс по HTTP; `staging_check.py` берётся из mount (свежий код сразу, без ребилда образа); реестр читается из env самого `orchestrator-staging`. **Курицы-яйца нет ни на первом прогоне, ни в будущем.** Автономность не ломается. +> +> ### Что обязан зафиксировать архитектор в ADR (вариант в) +> 1. Убрать из B6 host-path хак `sys.path.insert(0, "/repos/orchestrator")` и `importlib.reload(src.projects)`. +> 2. Канонизировать запуск suite ВНУТРИ контейнера: `docker exec orchestrator-staging python3 <путь к staging_check.py> --base-url http://localhost:8501` (или эквивалент, где cwd/PYTHONPATH и env — staging-контейнера). Код импортируется из кода контейнера, env уже staging. +> 3. **Синхронно** обновить `.openclaw/agents/deployer.md` (способ запуска suite через `docker exec`, НЕ с хоста) и `docs/operations/STAGING_CHECK.md` — иначе host-запуск воспроизведёт баг. +> 4. Логику вердикта B6 вынести в чистую функцию `_evaluate_b6(known: set[str]) -> tuple[bool, str]` (TR-2/§9) для unit-теста на оба исхода (AC-2). +> 5. НЕ добавлять HTTP-эндпоинт `/projects` и НЕ трогать прод-`src/main.py`. НЕ трогать `src/projects.py`, `.env*`, прочие чеки A/B4/B5/C. +> +> ### Нюанс топологии (учесть) +> `Dockerfile` НЕ копирует `scripts/` в образ → `staging_check.py` доступен в контейнере только через mount `/repos/orchestrator/scripts/...`. Архитектор должен указать в ADR корректный путь запуска внутри контейнера, учитывая этот mount (а не `/app/scripts`). + +--- + +## 4-original. Варианты реализации (исходный анализ — справочно) ## 4. Варианты реализации (выбор — архитектор, в ADR) Бизнес-запрос предлагает три варианта. Анализ перечисляет их с известными плюсами/минусами; решение и обоснование — в `06-adr/`. From d6744c3c05f09c02b0a8e0bda9c5a935319d1f9a Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 6 Jun 2026 06:59:56 +0000 Subject: [PATCH 4/7] architect(ET): auto-commit from architect run_id=151 --- docs/operations/STAGING_CHECK.md | 63 +++++--- ...DR-001-b6-registry-via-in-container-run.md | 139 ++++++++++++++++++ 2 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md diff --git a/docs/operations/STAGING_CHECK.md b/docs/operations/STAGING_CHECK.md index 4d1b912..f3e275e 100644 --- a/docs/operations/STAGING_CHECK.md +++ b/docs/operations/STAGING_CHECK.md @@ -36,34 +36,53 @@ Exit code: **0** = все PASS, **non-zero** = есть FAIL. ## Способы запуска -### 1. Внутри контейнера (рекомендуемый) - -```bash -docker exec orchestrator-staging \ - python3 /repos/orchestrator/scripts/staging_check.py --mode stub -``` - -### 2. С хоста (если есть токены в env) - -```bash -export ORCH_STAGING=true -export ORCH_PLANE_API_TOKEN=... -# ... остальные переменные ... - -python3 scripts/staging_check.py \ - --base-url http://localhost:8501 \ - --mode stub -``` - -### 3. Из docker exec с передачей URL +### 1. Внутри контейнера (КАНОНИЧЕСКИЙ — обязателен для деплоера) ```bash docker exec orchestrator-staging \ python3 /repos/orchestrator/scripts/staging_check.py \ - --base-url http://localhost:8501 \ - --mode stub + --base-url http://localhost:8501 --mode stub ``` +Это единственный канонический способ для стадии `deploy-staging` (ORCH-048, ADR-001). +Внутри контейнера env уже staging (`.env.staging`), а чек **B6** строит реестр проектов из +собственного process-env инстанса (см. ниже). Путь к скрипту — `/repos/orchestrator/scripts/…` +(bind-mount); `scripts/` **не** копируется в образ, поэтому `/app/scripts` не существует. + +### 2. С хоста — НЕ рекомендуется + +```bash +# ⚠️ Воспроизводит баг ORCH-048: на хосте ORCH_PROJECTS_JSON не задан → +# B6 строит реестр из дефолта (ET+ORCH) → ложный FAIL. +# Допустимо ТОЛЬКО если env хоста полностью повторяет staging (включая ORCH_PROJECTS_JSON). +export ORCH_STAGING=true +export ORCH_PROJECTS_JSON=... # обязателен, иначе B6 даст ложный FAIL +export ORCH_PLANE_API_TOKEN=... +# ... остальные переменные ... + +python3 scripts/staging_check.py --base-url http://localhost:8501 --mode stub +``` + +--- + +## Механика чека B6 (ORCH-048, ADR-001) + +B6 «Registry: sandbox present, prod ET/ORCH absent» подтверждает изоляцию: в реестре +работающего staging-инстанса есть только sandbox-проект и НЕТ боевых (ET/ORCH). + +- B6 импортирует `known_plane_project_ids()` из `src.projects` **кода контейнера** + (`/app/src` через `PYTHONPATH=/app`), env которого — `.env.staging`. Реестр отражает + именно работающий staging-инстанс. +- Прежний host-path хак (`sys.path.insert(0, "/repos/orchestrator")` + `importlib.reload`) + удалён: он подхватывал env процесса-запускателя и при запуске с хоста давал ложный FAIL. +- Логика вердикта вынесена в чистую функцию `_evaluate_b6(known) -> (passed, detail)`: + `passed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known`. Покрыта юнит-тестами + (`tests/test_staging_check_b6.py`) на оба исхода без поднятия инстанса/docker. +- При недоступности источника реестра B6 даёт детерминированный FAIL (не ложный PASS, + не необработанное исключение). + +**Поэтому B6 достоверен только при каноническом запуске (способ 1).** + --- ## Режимы (`--mode`) diff --git a/docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md b/docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md new file mode 100644 index 0000000..875876d --- /dev/null +++ b/docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md @@ -0,0 +1,139 @@ +# ADR-001: B6 читает реестр через запуск suite ВНУТРИ staging-контейнера + +## Статус +Accepted + +- **Задача:** ORCH-048 +- **Дата:** 2026-06-06 +- **Автор:** architect +- **Решение варианта:** принято Владельцем проекта (Слава, 06.06) — вариант **(в)**. Архитектор фиксирует и обосновывает. + +## Контекст + +Чек **B6 «Registry: sandbox present, prod ET/ORCH absent»** в `scripts/staging_check.py` +(блок B, ~строки 263–284) — страховка изоляции staging: подтверждает, что в реестре +проектов работающего staging-инстанса есть только sandbox-проект и НЕТ боевых +(enduro-trails / orchestrator). + +B6 даёт **ложный FAIL** (`prod-ET=YES(BAD!)`, `prod-ORCH=YES(BAD!)`), хотя изоляция +реестра в staging исправна. Root cause (подтверждён прямым запуском, 06.06): + +```python +sys.path.insert(0, "/repos/orchestrator") # host-worktree path +import importlib +if "src.projects" in sys.modules: + importlib.reload(sys.modules["src.projects"]) # перечитывает env ТЕКУЩЕГО процесса +from src.projects import known_plane_project_ids +known = known_plane_project_ids() +``` + +B6 — единственный чек, который не ходит к инстансу по HTTP, а импортирует Python-код +локально и строит реестр из `ORCH_PROJECTS_JSON` **process-env того процесса, в котором +исполняется скрипт**. Деплоер фактически запускает suite **с хоста** +(`python3 scripts/staging_check.py --base-url http://localhost:8501`), где +`ORCH_PROJECTS_JSON` не задан → `src.projects` грузит встроенный `_DEFAULT_PROJECTS` +(ET + ORCH) → ложный FAIL. B6 проверяет реестр НЕ того окружения, реестр которого +реально использует staging-инстанс. + +### Топология (ключевой факт для решения) + +- Контейнер `orchestrator-staging`: `WORKDIR /app`, `ENV PYTHONPATH=/app`; код приложения + **скопирован** в образ (`Dockerfile: COPY src/ ./src/`) → живёт в `/app/src/`. +- `.env.staging` (env_file контейнера) задаёт `ORCH_PROJECTS_JSON` = только sandbox. +- `Dockerfile` **НЕ копирует** `scripts/` в образ. Скрипт доступен в контейнере только + через bind-mount `/home/slin/repos:/repos` → `/repos/orchestrator/scripts/staging_check.py`. + +Из этого следует: при запуске `docker exec orchestrator-staging python3 +/repos/orchestrator/scripts/staging_check.py` интерпретатор добавляет в `sys.path[0]` +каталог скрипта (`/repos/orchestrator/scripts`), а `import src.projects` резолвится через +`PYTHONPATH=/app` → `/app/src/projects.py` (собственный код контейнера) с env из +`.env.staging`. Это ровно реестр работающего staging-инстанса — без HTTP, без host-path хака. + +## Решение + +Принят **вариант (в): канонизировать запуск suite ВНУТРИ `orchestrator-staging` и читать +собственный process-env контейнера.** + +Архитектурно фиксируется (детальная реализация — стадия development): + +1. **Убрать из B6 host-path хак:** удалить `sys.path.insert(0, "/repos/orchestrator")` и + `importlib.reload(sys.modules["src.projects"])`. Импорт `from src.projects import + known_plane_project_ids` остаётся, но резолвится из кода контейнера (`/app/src` через + `PYTHONPATH=/app`), env которого — staging (`.env.staging`). + +2. **Канонизировать запуск suite внутри контейнера** (а не с хоста): + ```bash + docker exec orchestrator-staging \ + python3 /repos/orchestrator/scripts/staging_check.py \ + --base-url http://localhost:8501 --mode stub + ``` + `--base-url http://localhost:8501` корректен изнутри контейнера: сеть `network_mode: host`. + Путь к скрипту — `/repos/orchestrator/scripts/...` (mount), а НЕ `/app/scripts` (в образе + scripts отсутствует). + +3. **Синхронно обновить документацию запуска** (этот же PR), иначе host-запуск воспроизведёт + баг: + - `.openclaw/agents/deployer.md` — команда стадии `deploy-staging` через `docker exec`. + - `docs/operations/STAGING_CHECK.md` — канонический способ запуска и описание механики B6. + +4. **Логику вердикта B6 вынести в чистую функцию** `_evaluate_b6(known: set[str]) -> + tuple[bool, str]`, инвариант (TR-2): `passed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ + PROD_ORCH ∉ known`; `detail` сохраняет формат `sandbox=…, prod-ET=…, prod-ORCH=…` (TR-3). + Функция юнит-тестируема без поднятия инстанса/docker (TC-01…TC-07). + +5. **Детерминированная деградация (TR-4):** при недоступности источника реестра (ошибка + импорта/построения `known`) B6 даёт FAIL с понятным detail, без необработанного исключения + и без ложного PASS. + +### Границы (scope guards — обязательны) + +- **НЕ** добавлять HTTP-эндпоинт `GET /projects`; **НЕ** трогать прод-`src/main.py`, + `src/webhooks/*`, `src/stage_engine.py`, `src/qg/*`. +- **НЕ** изменять `src/projects.py`, `.env`, `.env.staging`, `.env.example`. +- **НЕ** менять блоки A1–A3, B4, B5 и блок C (E2E): формат вывода и логика прежние. +- Реестр QG и стадий не меняется; ADR-0003 (`check_staging_status`) в силе — меняется только + достоверность одного чека внутри suite, чей агрегат пишется в `15-staging-log.md`. + +## Альтернативы (отклонены) + +### (а) HTTP-эндпоинт `GET /projects` работающего staging-инстанса — ОТКЛОНЁН +Порождает «курицу-яйцо»: B6 ходит на эндпоинт **работающего** инстанса, а эндпоинт запечён +в Docker-образ → на первом прогоне его в живом инстансе ещё нет (404) → ложный FAIL → откат. +Требует ручного bootstrap-деплоя. Это ровно тот класс поломки автономности, который мы +устраняем. Подтверждено на проде 06.06: `GET /projects` на 8501 → 404 → deploy-staging FAILED. +(Предыдущая итерация архитектора выбрала (а); решение отклонено Владельцем, код и ADR(а) +удалены, ветка откатана к analyst-артефактам.) + +### (б) `docker exec` subprocess + парсинг stdout — ОТКЛОНЁН +`docker exec orchestrator-staging python3 -c "..."` из процесса suite. Хрупкое экранирование +(`docs/history/LESSONS_2026-06-05.md`), зависимость от наличия docker-CLI и имени контейнера +в среде запуска, усложняет запуск «изнутри контейнера». + +### (в) Запуск suite внутри контейнера + собственный process-env — ВЫБРАН +B6 не зависит от того, что отдаёт инстанс по HTTP; `staging_check.py` берётся из mount (свежий +код сразу, без ребилда образа); реестр читается из env самого `orchestrator-staging`. Курицы-яйца +нет ни на первом прогоне, ни в будущем. Минимально инвазивно, прод-логика и `src/projects.py` не +тронуты. Согласуется с «рекомендуемым способом запуска» (`STAGING_CHECK.md §Способы запуска.1`). + +## Последствия + +**Плюсы** +- B6 достоверно отражает реестр работающего staging-инстанса; ложные FAIL/откаты устранены. +- Автономность self-hosting не ломается: нет bootstrap-зависимости от запечённого в образ кода. +- Свежий `staging_check.py` подхватывается из mount без ребилда образа. +- Защитная функция B6 сохранена и покрыта юнит-тестами на оба исхода (PASS/FAIL). + +**Минусы / ограничения** +- Запуск suite **обязан** идти через `docker exec` внутри `orchestrator-staging`. Запуск с + хоста воспроизведёт исходный баг (host-env без `ORCH_PROJECTS_JSON`). Это закреплено в + `deployer.md` и `STAGING_CHECK.md`; способ «с хоста» остаётся возможен, только если env + хоста корректно повторяет staging (не рекомендуется, помечено). +- Деплоер должен иметь доступ к docker-CLI/сокету (есть: `/var/run/docker.sock` смонтирован в + контейнер оркестратора, у которого deployer-агент исполняется; `deployer.md` tools: Bash docker). + +## Связи +- ADR-0003 (`docs/architecture/adr/adr-0003-staging-gate.md`) — staging-гейт, который этот чек + обслуживает. +- ORCH-6 / `src/projects.py` — реестр проектов (источник `known_plane_project_ids()`), + НЕ изменяется. +- `docs/history/LESSONS_2026-06-05.md` — обоснование отказа от варианта (б). From 28d019a1e2fec08c67db1b5a345336ad2bfad8f5 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 6 Jun 2026 07:03:31 +0000 Subject: [PATCH 5/7] fix(staging_check): B6 reads registry from running staging instance env B6 false-FAILed because it built the project registry from the launcher process-env via a host-path hack (sys.path.insert + importlib.reload), not from the running staging instance. Run from the host, ORCH_PROJECTS_JSON is unset -> default ET+ORCH registry -> false FAIL -> spurious deploy-staging -> development rollback. Variant (v) per ADR-001: remove the host-path hack; canonically run the suite INSIDE orchestrator-staging via docker exec so src.projects resolves from /app (PYTHONPATH) with .env.staging. Verdict logic extracted into pure _evaluate_b6(known) -> (passed, detail) + _known_project_ids_from_registry() / _run_b6() with deterministic FAIL on source unavailability. deployer.md and STAGING_CHECK.md updated to the docker exec command. src/projects.py, .env* and checks A/B4/B5/C untouched. Refs: ORCH-048 Co-Authored-By: Claude Opus 4.7 --- .openclaw/agents/deployer.md | 14 ++- CHANGELOG.md | 1 + scripts/staging_check.py | 90 ++++++++++++++------ tests/test_staging_check_b6.py | 151 +++++++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 tests/test_staging_check_b6.py diff --git a/.openclaw/agents/deployer.md b/.openclaw/agents/deployer.md index e668931..53611cb 100644 --- a/.openclaw/agents/deployer.md +++ b/.openclaw/agents/deployer.md @@ -21,10 +21,20 @@ On stage `deploy-staging` your job is to run the staging test suite and write a ### Steps: -1. Run the staging test suite against the live staging environment: +1. Run the staging test suite against the live staging environment. + **CANONICAL: run INSIDE the `orchestrator-staging` container via `docker exec`** + (ORCH-048, ADR-001) — NOT from the host: ```bash - python3 scripts/staging_check.py --base-url http://localhost:8501 --mode stub + docker exec orchestrator-staging \ + python3 /repos/orchestrator/scripts/staging_check.py \ + --base-url http://localhost:8501 --mode stub ``` + Why: the B6 registry-isolation check reads the registry from the running + instance's own process-env (`.env.staging`). Running from the host leaves + `ORCH_PROJECTS_JSON` unset → B6 falls back to the default (ET+ORCH) registry + → false FAIL → spurious rollback. The script path is `/repos/orchestrator/scripts/…` + (bind-mount); `scripts/` is NOT copied into the image, so `/app/scripts` does + not exist. Details: `docs/operations/STAGING_CHECK.md`. 2. Check the exit code: - Exit code **0** = all tests PASS → `staging_status: SUCCESS` diff --git a/CHANGELOG.md b/CHANGELOG.md index c008b1f..2e3cbfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Цепочка стадий: `... testing → deploy-staging → deploy → done` (была без `deploy-staging`). ### Fixed +- **Staging-чек B6 читает реестр из окружения работающего staging-инстанса** (ORCH-048): блок B6 «Registry: sandbox present, prod ET/ORCH absent» в `scripts/staging_check.py` давал **ложный FAIL** (`prod-ET=YES(BAD!)`, `prod-ORCH=YES(BAD!)`) при фактически исправной изоляции — единственный чек suite, который не ходил к инстансу по HTTP, а импортировал `src.projects` локально через host-path хак `sys.path.insert(0, "/repos/orchestrator")` + `importlib.reload`, строя реестр из `ORCH_PROJECTS_JSON` **process-env запускающего процесса**. При фактическом запуске деплоером с хоста переменная не задана → дефолт `_DEFAULT_PROJECTS` (ET+ORCH) → ложный FAIL → лишний откат `deploy-staging → development`. Решение (вариант «в», ADR-001): host-path хак удалён; suite канонически запускается ВНУТРИ контейнера `orchestrator-staging` через `docker exec … python3 /repos/orchestrator/scripts/staging_check.py` (`scripts/` доступен только через bind-mount, `import src.projects` резолвится через `PYTHONPATH=/app` из кода контейнера, env — `.env.staging`) → B6 читает реестр именно работающего инстанса, без HTTP-bootstrap и «курицы-яйца». Логика вердикта вынесена в чистую `_evaluate_b6(known) -> (passed, detail)` (инвариант `passed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known`, формат detail сохранён) + `_known_project_ids_from_registry()` / `_run_b6()` с детерминированным FAIL при недоступности источника (не ложный PASS, не необработанное исключение). Синхронно обновлены `.openclaw/agents/deployer.md` (команда стадии через `docker exec`) и `docs/operations/STAGING_CHECK.md`. `src/projects.py`, `.env*` и прочие чеки A/B4/B5/C не тронуты; реестр `QG_CHECKS` и `check_staging_status` (ADR-0003) не менялись. ADR `docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md`. Тесты: `tests/test_staging_check_b6.py`. - **Testing-гейт `check_tests_passed` читает `result:` наравне с `verdict:`/`status:`** (ORCH-047): парсер `_parse_tests_verdict` (`src/qg/checks.py`) теперь принимает три равноправных машиночитаемых поля frontmatter `13-test-report.md` — `result:` (канон промпта тестера `.openclaw/agents/tester.md`, `result: PASS|FAIL`), плюс легаси `verdict:` и `status:` (enduro-trails ET-001..ET-014); достаточно любого одного непустого. Устраняет рассинхрон контракта: тестер честно эмитил `result: PASS` без `verdict:`/`status:`, парсер попадал в ветку «нет машинного вердикта» → откат `testing → development` в петлю до исчерпания `MAX_DEVELOPER_RETRIES` (наблюдалось на ORCH-17; ORCH-016 прошёл лишь из-за избыточного дублирования полей). Семантика приоритетов сохранена и распространена на все три поля через объединённую строку: negative-токен в любом поле авторитетен (перебивает positive), наборы токенов заморожены (обратная совместимость). Сигнатура гейта, имя и реестр `QG_CHECKS` не менялись. ADR `docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md`. Тесты: `tests/test_qg.py::TestCheckTestsPassed`. - БАГ-8: провал deploy/deploy-staging → корректный откат на `development`. - Изоляция тестов от живого Plane API (PR #27): autouse-фикстура сброса settings. diff --git a/scripts/staging_check.py b/scripts/staging_check.py index 87edf59..75ba892 100644 --- a/scripts/staging_check.py +++ b/scripts/staging_check.py @@ -8,8 +8,14 @@ Checks: Block C — E2E (create task in SANDBOX → trigger pipeline via /webhook/plane → verify branch + job enqueued → CLEANUP in finally) -Usage (inside the container or with correct env set): - python3 scripts/staging_check.py [--base-url http://localhost:8501] [--mode stub|full-real] +Usage — CANONICAL: run INSIDE the orchestrator-staging container (ORCH-048, ADR-001) +so B6 reads the registry from the running instance's own env (.env.staging): + docker exec orchestrator-staging \ + python3 /repos/orchestrator/scripts/staging_check.py \ + --base-url http://localhost:8501 [--mode stub|full-real] + +Running from the host leaves ORCH_PROJECTS_JSON unset → B6 falls back to the +default (ET+ORCH) registry → false FAIL. See docs/operations/STAGING_CHECK.md. Exit code: 0 = all PASS, non-zero = at least one FAIL. @@ -214,6 +220,59 @@ SANDBOX_PROJECT_ID = "8c5a3025-4f9d-4190-b79f-fa06276bb27e" PROD_ET_PROJECT_ID = "7a79f0a9-5278-49cd-9007-9a338f238f9c" PROD_ORCH_PROJECT_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a" +B6_LABEL = "B6 Registry: sandbox present, prod ET/ORCH absent" + + +def _evaluate_b6(known: set[str]) -> tuple[bool, str]: + """Pure verdict logic for the B6 registry-isolation check (ORCH-048). + + PASS ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known (TR-2). + ``detail`` keeps the human-readable ``sandbox=…, prod-ET=…, prod-ORCH=…`` + format (TR-3). Isolated from any I/O so both outcomes are unit-testable + without a live staging instance or docker (02-trz §9, ADR-001). + """ + sandbox_present = SANDBOX_PROJECT_ID in known + et_absent = PROD_ET_PROJECT_ID not in known + orch_absent = PROD_ORCH_PROJECT_ID not in known + passed = sandbox_present and et_absent and orch_absent + detail = ( + f"sandbox={'YES' if sandbox_present else 'NO'}, " + f"prod-ET={'NO(good)' if et_absent else 'YES(BAD!)'}, " + f"prod-ORCH={'NO(good)' if orch_absent else 'YES(BAD!)'}" + ) + return passed, detail + + +def _known_project_ids_from_registry() -> set[str]: + """Registry of the *running staging instance* — its own process-env (ORCH-048). + + The suite is canonically run INSIDE ``orchestrator-staging`` via + ``docker exec`` (ADR-001), so ``src.projects`` resolves through the + container's ``PYTHONPATH=/app`` to ``/app/src/projects.py`` and reads + ``ORCH_PROJECTS_JSON`` from ``.env.staging``. This reflects exactly the + registry the live instance serves webhooks with — no host-path hack, no HTTP + bootstrap dependency. + """ + from src.projects import known_plane_project_ids + return known_plane_project_ids() + + +def _run_b6(results: Results) -> None: + """Run the B6 registry-isolation check and record its verdict. + + Builds the known-id set from the running instance's registry and applies + ``_evaluate_b6``. Any failure to obtain the registry yields a deterministic + FAIL with a clear detail (TR-4) — never an unhandled exception and never a + false PASS. + """ + try: + known = _known_project_ids_from_registry() + except Exception as e: + results.add(B6_LABEL, False, f"registry source unavailable: {e}") + return + passed, detail = _evaluate_b6(known) + results.add(B6_LABEL, passed, detail) + def block_b(results: Results): print(f"\n{_BOLD}[Block B] ACCESS{_RESET}") @@ -260,28 +319,11 @@ def block_b(results: Results): except Exception as e: results.add("B5 Gitea: orchestrator-sandbox accessible, push=true", False, str(e)) - # B6 — Registry: sandbox in known IDs, prod ET/ORCH NOT in known IDs - try: - # Import from inside the container (script runs in /repos/orchestrator context) - sys.path.insert(0, "/repos/orchestrator") - # Force reload to pick up container env - import importlib - if "src.projects" in sys.modules: - importlib.reload(sys.modules["src.projects"]) - from src.projects import known_plane_project_ids - known = known_plane_project_ids() - sandbox_present = SANDBOX_PROJECT_ID in known - et_absent = PROD_ET_PROJECT_ID not in known - orch_absent = PROD_ORCH_PROJECT_ID not in known - ok = sandbox_present and et_absent and orch_absent - detail = ( - f"sandbox={'YES' if sandbox_present else 'NO'}, " - f"prod-ET={'NO(good)' if et_absent else 'YES(BAD!)'}, " - f"prod-ORCH={'NO(good)' if orch_absent else 'YES(BAD!)'}" - ) - results.add("B6 Registry: sandbox present, prod ET/ORCH absent", ok, detail) - except Exception as e: - results.add("B6 Registry: sandbox present, prod ET/ORCH absent", False, str(e)) + # B6 — Registry: sandbox in known IDs, prod ET/ORCH NOT in known IDs (ORCH-048). + # Reads the registry of the running staging instance from its own process-env + # (canonical: docker exec inside orchestrator-staging — ADR-001). No host-path + # hack; deterministic FAIL if the registry source is unavailable (TR-4). + _run_b6(results) # --------------------------------------------------------------------------- diff --git a/tests/test_staging_check_b6.py b/tests/test_staging_check_b6.py new file mode 100644 index 0000000..0eb8940 --- /dev/null +++ b/tests/test_staging_check_b6.py @@ -0,0 +1,151 @@ +"""ORCH-048: unit tests for the B6 registry-isolation verdict in staging_check.py. + +B6 «Registry: sandbox present, prod ET/ORCH absent» is the staging-isolation +safety check. Its verdict logic is isolated into the pure function +``_evaluate_b6(known) -> (passed, detail)`` so both outcomes (clean staging +registry → PASS, polluted registry → FAIL) can be tested without standing up a +live staging instance or docker (02-trz §9, ADR-001). + +These tests target that pure function plus the deterministic-degradation path +(``_run_b6``) and statically assert the host-path hack is gone (TR-6 / TC-06). +""" + +import importlib.util +import pathlib + +import pytest + +# --------------------------------------------------------------------------- +# Load scripts/staging_check.py by path (scripts/ is not an importable package). +# --------------------------------------------------------------------------- +_SCRIPT_PATH = ( + pathlib.Path(__file__).resolve().parent.parent / "scripts" / "staging_check.py" +) + + +def _load_module(): + spec = importlib.util.spec_from_file_location("staging_check", _SCRIPT_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +sc = _load_module() + +SANDBOX = sc.SANDBOX_PROJECT_ID +PROD_ET = sc.PROD_ET_PROJECT_ID +PROD_ORCH = sc.PROD_ORCH_PROJECT_ID + + +# --------------------------------------------------------------------------- +# TC-01 — clean staging registry → PASS +# --------------------------------------------------------------------------- +def test_tc01_clean_registry_passes(): + passed, detail = sc._evaluate_b6({SANDBOX}) + assert passed is True + assert "sandbox=YES" in detail + assert "prod-ET=NO(good)" in detail + assert "prod-ORCH=NO(good)" in detail + + +# --------------------------------------------------------------------------- +# TC-02 — prod-ET leaked into registry → FAIL +# --------------------------------------------------------------------------- +def test_tc02_prod_et_present_fails(): + passed, detail = sc._evaluate_b6({SANDBOX, PROD_ET}) + assert passed is False + assert "sandbox=YES" in detail + assert "prod-ET=YES(BAD!)" in detail + assert "prod-ORCH=NO(good)" in detail + + +# --------------------------------------------------------------------------- +# TC-03 — prod-ORCH leaked into registry → FAIL +# --------------------------------------------------------------------------- +def test_tc03_prod_orch_present_fails(): + passed, detail = sc._evaluate_b6({SANDBOX, PROD_ORCH}) + assert passed is False + assert "sandbox=YES" in detail + assert "prod-ET=NO(good)" in detail + assert "prod-ORCH=YES(BAD!)" in detail + + +# --------------------------------------------------------------------------- +# TC-04 — sandbox absent (empty registry) → deterministic FAIL, no exception +# --------------------------------------------------------------------------- +def test_tc04_empty_registry_fails_without_sandbox(): + passed, detail = sc._evaluate_b6(set()) + assert passed is False + assert "sandbox=NO" in detail + + +# --------------------------------------------------------------------------- +# TC-05 — both prod projects leaked → FAIL +# --------------------------------------------------------------------------- +def test_tc05_both_prod_present_fails(): + passed, detail = sc._evaluate_b6({SANDBOX, PROD_ET, PROD_ORCH}) + assert passed is False + assert "prod-ET=YES(BAD!)" in detail + assert "prod-ORCH=YES(BAD!)" in detail + + +# --------------------------------------------------------------------------- +# TC-06 — registry source no longer depends on the host-path hack (TR-6) +# --------------------------------------------------------------------------- +def test_tc06_no_host_path_hack_in_source(): + source = _SCRIPT_PATH.read_text(encoding="utf-8") + # The host-worktree path injection and the env-of-the-launcher reload that + # caused the false FAIL must be gone from the B6 mechanics. + assert 'sys.path.insert(0, "/repos/orchestrator")' not in source + assert "importlib.reload" not in source + + +def test_tc06_registry_loader_uses_src_projects(): + # The verdict input is built from src.projects.known_plane_project_ids() + # resolved via the running instance's own PYTHONPATH/env — not from a + # host-path-injected import. We verify the loader delegates to that function. + import src.projects as projects_mod + + sentinel = {"sentinel-id"} + original = projects_mod.known_plane_project_ids + projects_mod.known_plane_project_ids = lambda: sentinel + try: + known = sc._known_project_ids_from_registry() + finally: + projects_mod.known_plane_project_ids = original + assert known == sentinel + + +# --------------------------------------------------------------------------- +# TC-07 — degraded registry source → deterministic FAIL (not false PASS, not raise) +# --------------------------------------------------------------------------- +def test_tc07_source_failure_is_deterministic_fail(monkeypatch): + def _boom(): + raise RuntimeError("registry import blew up") + + monkeypatch.setattr(sc, "_known_project_ids_from_registry", _boom) + + results = sc.Results() + # Must not raise. + sc._run_b6(results) + + assert len(results._items) == 1 + label, passed, detail = results._items[0] + assert passed is False + assert "registry source unavailable" in detail + assert "registry import blew up" in detail + + +# --------------------------------------------------------------------------- +# _run_b6 happy path wiring (clean registry → PASS result recorded) +# --------------------------------------------------------------------------- +def test_run_b6_records_pass_for_clean_registry(monkeypatch): + monkeypatch.setattr( + sc, "_known_project_ids_from_registry", lambda: {SANDBOX} + ) + results = sc.Results() + sc._run_b6(results) + assert len(results._items) == 1 + _label, passed, detail = results._items[0] + assert passed is True + assert "sandbox=YES" in detail From 2424f9aaad8e985d85dd0dd8b204169f17589214 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 6 Jun 2026 07:05:47 +0000 Subject: [PATCH 6/7] reviewer(ET): auto-commit from reviewer run_id=153 --- docs/work-items/ORCH-048/12-review.md | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/work-items/ORCH-048/12-review.md diff --git a/docs/work-items/ORCH-048/12-review.md b/docs/work-items/ORCH-048/12-review.md new file mode 100644 index 0000000..5eb597e --- /dev/null +++ b/docs/work-items/ORCH-048/12-review.md @@ -0,0 +1,69 @@ +--- +type: review +work_item_id: ORCH-048 +verdict: APPROVED +version: 1 +--- + +# Review ORCH-048 + +## Summary + +PR чинит ложный FAIL чека **B6** в `scripts/staging_check.py`: реестр проектов теперь +читается из окружения работающего staging-инстанса (вариант «в», выбранный Владельцем и +зафиксированный в ADR-001), host-path хак `sys.path.insert(0, "/repos/orchestrator")` + +`importlib.reload` удалён. Реализация соответствует ТЗ, ADR-001 и всем критериям приёмки. +Документация обновлена синхронно. `pytest tests/ -q` — **470 passed**. + +Соответствие осям проверки: + +- **ТЗ (02-trz):** TR-1…TR-6 выполнены. TR-1/TR-6 — реестр строится из process-env + инстанса, host-path хак удалён. TR-2 — инвариант `passed ⟺ SANDBOX ∈ known ∧ PROD_ET ∉ + known ∧ PROD_ORCH ∉ known` в `_evaluate_b6`. TR-3 — формат detail сохранён. TR-4 — + детерминированный FAIL при недоступности источника (`_run_b6` ловит `Exception`, нет + ложного PASS, нет необработанного исключения). TR-5 — stdlib. §9 — логика вердикта + вынесена в чистую `_evaluate_b6` для unit-теста. +- **ADR-001:** реализация дословно следует пунктам 1–5 решения и scope-guards. + HTTP-эндпоинт не добавлен, прод-`src/main.py` не тронут. +- **AC-1…AC-5:** AC-1 — механика читает реестр инстанса; AC-2 — оба исхода покрыты + (TC-01 clean→PASS, TC-02/03/05 polluted→FAIL); AC-3 — `git diff` не содержит + `src/projects.py`/`.env*`, блоки A1–A3/B4/B5/C не тронуты; AC-4 — 470 passed; AC-5 — + STAGING_CHECK.md, deployer.md, CHANGELOG, ADR-001 обновлены в этом же PR. +- **Качество кода:** чистые функции, докстринги на всех новых функциях, defensive-обработка, + `sys` остаётся используемым (`sys.exit`), без мёртвых импортов. Тесты содержательные + (7 TC + happy-path wiring + статическая проверка отсутствия хака). + +## Findings + +### P0 — Blocker +- нет + +### P1 — Must fix +- нет + +### P2 — Should fix +- нет + +### P3 — Nice-to-have +- [ ] `test_tc06_no_host_path_hack_in_source` и `test_tc06_registry_loader_uses_src_projects` + носят одинаковый префикс `tc06` — формально это два разных кейса; имена можно было бы + развести для читаемости отчёта pytest. Косметика, на приёмку не влияет. + +## Документация + +Полностью обновлена в том же PR (golden source соблюдён): + +- `docs/operations/STAGING_CHECK.md` — канонический способ запуска (способ 1 через + `docker exec`), способ «с хоста» помечен как невалидный/воспроизводящий баг, добавлена + секция «Механика чека B6». +- `.openclaw/agents/deployer.md` — команда стадии `deploy-staging` переведена на + `docker exec orchestrator-staging python3 /repos/orchestrator/scripts/staging_check.py …` + с пояснением, почему host-запуск ломает B6. +- `CHANGELOG.md` — запись в разделе Fixed с полным описанием root cause и решения. +- ADR `docs/work-items/ORCH-048/06-adr/ADR-001-b6-registry-via-in-container-run.md` — + обоснование варианта (в), отклонённые (а)/(б), scope-guards. + +`docs/architecture/README.md` обновлять не требовалось: API, реестр стадий и `QG_CHECKS` +не менялись (изменение касается только достоверности одного чека внутри suite). + +**Вердикт: APPROVED** — P0/P1 отсутствуют. From 913c185232631dd77ea3db52e10e628da94f49d2 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 6 Jun 2026 07:07:53 +0000 Subject: [PATCH 7/7] tester(ET): auto-commit from tester run_id=154 --- docs/work-items/ORCH-048/13-test-report.md | 79 ++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/work-items/ORCH-048/13-test-report.md diff --git a/docs/work-items/ORCH-048/13-test-report.md b/docs/work-items/ORCH-048/13-test-report.md new file mode 100644 index 0000000..469e7e2 --- /dev/null +++ b/docs/work-items/ORCH-048/13-test-report.md @@ -0,0 +1,79 @@ +--- +type: test-report +work_item_id: ORCH-048 +result: PASS +--- + +# Test Report — ORCH-048 + +**Title:** staging B6 check reads registry from host worktree, not staging container +**Stage:** testing +**Branch:** feature/ORCH-048-staging-b6-check-reads-registr + +## Окружение +- Python: 3.12.13 +- pytest: 8.3.3 +- Дата: 2026-06-06T07:06Z +- Prod API (8500): `/health` 200 ok, `/status` 200 (ORCH-048 в stage=testing), `/queue` 200 (breaker closed, preflight ok) + +## Результаты + +| TC ID | Тип | Описание | Результат | +|-------|-----|----------|-----------| +| TC-01 | unit | `known={SANDBOX}` → B6 PASS, detail sandbox=YES/prod-ET=NO/prod-ORCH=NO | PASS | +| TC-02 | unit | `known={SANDBOX,PROD_ET}` → B6 FAIL, prod-ET помечен нарушением | PASS | +| TC-03 | unit | `known={SANDBOX,PROD_ORCH}` → B6 FAIL, prod-ORCH помечен нарушением | PASS | +| TC-04 | unit | `known=set()` (нет sandbox) → детерминированный FAIL без исключения | PASS | +| TC-05 | unit | `known={SANDBOX,PROD_ET,PROD_ORCH}` → B6 FAIL | PASS | +| TC-06 | unit | Нет host-path хака `/repos/orchestrator`; реестр строится не локальным импортом в произвольном process-env | PASS | +| TC-07 | unit | Деградация источника реестра → детерминированный FAIL с понятным detail (не ложный PASS, не необработанное исключение) | PASS | +| TC-08 | unit | Регрессия `src/projects.py` (16 тестов) зелёные — реестр не изменён | PASS | +| TC-09 | integration | `python -m pytest tests/ -q` → exit 0 | PASS | +| TC-10 | integration | Живой staging-прогон B6 на 8501 | DEFERRED — выполняется деплоером на стадии deploy-staging (см. 04-test-plan TC-10) | + +Доп. покрытие: `test_run_b6_records_pass_for_clean_registry` (happy-path wiring `_run_b6`). + +## Покрытие критериев приёмки + +| AC | Подтверждение | Статус | +|----|---------------|--------| +| AC-1 | B6 PASS на чистом реестре (TC-01), источник — окружение инстанса, host-path хак удалён (TC-06) | PASS | +| AC-2 | Оба исхода покрыты: clean→PASS (TC-01), polluted→FAIL (TC-02/03/05), без sandbox→FAIL (TC-04) | PASS | +| AC-3 | `git diff origin/main...HEAD` НЕ содержит `src/projects.py` / `.env*`; блоки A/B4/B5/C не тронуты | PASS | +| AC-4 | `pytest tests/ -q` → exit 0, 470 passed | PASS | +| AC-5 | STAGING_CHECK.md, deployer.md, CHANGELOG.md, ADR-001 обновлены в том же PR (подтверждено review) | PASS | + +## Проверка scope (AC-3) +Изменённые файлы ветки vs origin/main: +``` +.openclaw/agents/deployer.md +CHANGELOG.md +docs/operations/STAGING_CHECK.md +docs/work-items/ORCH-048/* (артефакты задачи) +scripts/staging_check.py +tests/test_staging_check_b6.py +``` +Forbidden-path check: OK — `src/projects.py` и `.env*` НЕ затронуты. + +## Вывод pytest + +Полный прогон: +``` +470 passed, 1 warning in 9.07s +pytest exit code: 0 +``` +(warning — PydanticDeprecatedSince20 в `src/config.py`, предсуществующий, к ORCH-048 не относится.) + +B6 suite: +``` +tests/test_staging_check_b6.py ......... [9 passed] +9 passed, 1 warning in 0.29s +``` + +projects-регрессия (TC-08): +``` +16 passed, 1 warning in 0.31s +``` + +## Итог +**PASS** — все unit/integration тесты зелёные (470 passed, exit 0), smoke API prod исправен, scope-инвариант AC-3 соблюдён. Живой staging-прогон B6 (TC-10) выполняется деплоером на стадии deploy-staging.