architect(ET): auto-commit from architect run_id=146
All checks were successful
CI / test (push) Successful in 12s
All checks were successful
CI / test (push) Successful in 12s
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# ADR-001: B6 читает реестр staging через HTTP-эндпоинт `GET /projects` живого инстанса
|
||||
|
||||
## Статус
|
||||
Accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
`scripts/staging_check.py` — suite живых проверок staging-стенда orchestrator (порт 8501, ADR-0003). Деплоер гоняет её на стадии `deploy-staging`; агрегат пишется в `15-staging-log.md` (`staging_status:`), FAIL любого чека = откат на `development`.
|
||||
|
||||
Чек **B6 «Registry: sandbox present, prod ET/ORCH absent»** — единственный в suite, который не ходит по HTTP к работающему инстансу, а **импортирует Python-код локально** и перечитывает реестр из process-env скрипта:
|
||||
|
||||
```python
|
||||
sys.path.insert(0, "/repos/orchestrator") # host-worktree path
|
||||
importlib.reload(sys.modules["src.projects"]) # подхватывает env ТЕКУЩЕГО процесса
|
||||
known = known_plane_project_ids()
|
||||
```
|
||||
|
||||
Подтверждённый root cause (01-brd §1, прямой запуск 06.06):
|
||||
|
||||
- В контейнере `orchestrator-staging` `known_plane_project_ids()` корректно отдаёт только sandbox (`.env.staging → ORCH_PROJECTS_JSON`). **Изоляция реестра исправна.** Чеки A1–A3, B4, B5, блок C — зелёные (все по HTTP).
|
||||
- Деплоер канонически запускает suite **с хоста** (`.openclaw/agents/deployer.md:26`: `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.
|
||||
|
||||
Топология, подтверждённая в этой задаче (`docker-compose.yml`, `Dockerfile`):
|
||||
|
||||
- Контейнер `orchestrator-staging` исполняет **собранный код из `/app/src`** (Dockerfile `COPY src/ ./src/`) с env из `.env.staging`.
|
||||
- `Dockerfile` **не** копирует `scripts/` — `staging_check.py` доступен только через mount `/home/slin/repos:/repos` по пути `/repos/orchestrator/scripts/...`.
|
||||
- Значит host-path хак `/repos/orchestrator` импортирует **другую копию кода** (mounted worktree), а не ту, что реально обслуживает webhooks (`/app`). Реестр совпадает лишь при удачном process-env.
|
||||
|
||||
Вывод: B6 проверяет реестр НЕ того окружения, реестр которого реально использует staging-инстанс. Нужен источник, который **гарантированно** отражает реестр работающего процесса (порт 8501), независимо от способа запуска suite (хост / `docker exec`).
|
||||
|
||||
Анализ предложил три варианта (02-trz §4): (а) HTTP-эндпоинт инстанса; (б) `docker exec` subprocess; (в) канонизировать запуск внутри контейнера + убрать host-path хак.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбран **вариант (а): B6 получает реестр у работающего staging-инстанса по HTTP**, через новый read-only эндпоинт.
|
||||
|
||||
### 1. Новый эндпоинт `GET /projects` (прод `src/main.py`)
|
||||
|
||||
Read-only, additive. Возвращает реестр **именно того процесса**, что обслуживает webhooks:
|
||||
|
||||
```
|
||||
GET /projects → 200
|
||||
{
|
||||
"known_plane_project_ids": ["<uuid>", ...],
|
||||
"projects": [
|
||||
{"plane_project_id": "<uuid>", "repo": "...", "work_item_prefix": "...", "name": "..."}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- Источник — существующая `src.projects.known_plane_project_ids()` / `PROJECTS` (никакой новой логики реестра).
|
||||
- Данные неконфиденциальны: project uuid / repo / prefix / name. Секреты (токены, webhook-secret) **не** отдаются.
|
||||
- Существующие роуты (`/health`, `/status`, `/queue`, `/webhook/*`) не меняются — поведение прода неизменно.
|
||||
|
||||
### 2. B6 переписывается на HTTP
|
||||
|
||||
- B6 делает `GET {base}/projects` тем же stdlib-хелпером `_get(...)`, что и A/B4/B5/C (TR-5: без сторонних зависимостей).
|
||||
- Удаляются `sys.path.insert(0, "/repos/orchestrator")` и `importlib.reload` (TR-6).
|
||||
- Логика вердикта выносится в **чистую функцию** `_evaluate_b6(known: set[str]) -> tuple[bool, str]` (02-trz §9), которую покрывает unit-тест на оба исхода (AC-2), без поднятия инстанса/docker.
|
||||
- Контракт вердикта неизменен (TR-2): PASS ⟺ `SANDBOX ∈ known ∧ PROD_ET ∉ known ∧ PROD_ORCH ∉ known`. Те же константы `SANDBOX_PROJECT_ID` / `PROD_ET_PROJECT_ID` / `PROD_ORCH_PROJECT_ID`.
|
||||
- Формат `Results.add(label, passed, detail)` сохраняется (TR-3): `sandbox=…, prod-ET=…, prod-ORCH=…`.
|
||||
- При недоступности эндпоинта (не-200 / отсутствует ключ `known_plane_project_ids` / сетевой сбой) → **детерминированный FAIL** с понятным detail (TR-4), без необработанного исключения и без ложного PASS.
|
||||
|
||||
### Почему (а), а не (б)/(в)
|
||||
|
||||
- **Семантическая корректность.** Только HTTP-ответ живого процесса отражает реестр, по которому инстанс реально фильтрует webhooks. (б) и (в) **переимпортируют** `src.projects` в стороннем процессе и переоценивают реестр из его env — это та же анти-схема «local import → process-env», от которой предостерегает TR-1; корректны лишь «по совпадению» нужного env/копии кода.
|
||||
- **Инвариантность к способу запуска.** (а) одинаково работает при запуске с хоста (канонический способ деплоера) и из `docker exec` — оба бьют в `http://localhost:8501`. **Менять способ запуска деплоера не требуется** → меньше self-hosting-churn и нет риска «тихого» воспроизведения бага при host-запуске (главная слабость варианта (в)).
|
||||
- **Согласованность с suite.** Вся остальная suite — живые HTTP-вызовы к инстансу (docstring: «Live … against running services»). B6 был единственным выбросом; (а) устраняет архитектурную неоднородность.
|
||||
- **(б)** требует доступного docker-CLI и имени контейнера в среде запуска, ломается «изнутри контейнера», имеет нюансы экранирования (`docs/history/LESSONS_2026-06-05.md`) — отклонён как хрупкий для проверки-предохранителя.
|
||||
- **(в)** минимально инвазивен, но корректен только при запуске через `docker exec`; host-запуск воспроизводит ровно тот баг, что чиним. Для **isolation safety-check** недопустимо полагаться на конвенцию запуска.
|
||||
|
||||
Цена (а) — один additive read-only эндпоинт в прод-`main.py`. Это явно санкционировано анализом (02-trz §1 «исключение для варианта (а)», AC-3): изменение ограничено добавлением эндпоинта, прод-поведение существующих роутов неизменно.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы**
|
||||
- B6 достоверен: PASS при исправной изоляции (BR-1), FAIL при реальном попадании прод-проекта в staging-реестр (BR-2, покрыт тестом AC-2).
|
||||
- Способ запуска деплоера (`--base-url http://localhost:8501` с хоста) не меняется.
|
||||
- Эндпоинт `/projects` — полезный read-only diagnostics для всех инстансов (наблюдаемость реестра), переиспользуем за пределами B6.
|
||||
- `src/projects.py` и `.env*` не тронуты (BR-3, AC-3).
|
||||
|
||||
**Минусы / ограничения**
|
||||
- Затронут прод-`main.py` (additive route) — единственное отступление от «не трогать прод-логику», заранее одобренное анализом. Доставка эндпоинта в прод идёт штатно через staging-гейт (ADR-0003), без внепланового рестарта прод-контейнера.
|
||||
- B6 теперь зависит от доступности `/projects` на инстансе. Митигируется детерминированным FAIL (TR-4) — отказ эндпоинта не даёт ложный PASS.
|
||||
- Реестр в ответе не должен расширяться чувствительными полями — зафиксировано: только id/repo/prefix/name.
|
||||
|
||||
**Обязательства по документации (golden source, тот же PR)**
|
||||
- `docs/architecture/README.md` — добавить строку `GET /projects` в таблицу API; обновить описание механики B6 в секции про staging-гейт.
|
||||
- `CHANGELOG.md` — запись о новом read-only эндпоинте и фиксе B6.
|
||||
- `docs/operations/STAGING_CHECK.md` — обновить описание B6 (HTTP вместо локального импорта) и строку «Изолированность от прода» (B6 читает `known_plane_project_ids` через `GET /projects`).
|
||||
- `.openclaw/agents/deployer.md` — **не требует** изменения способа запуска (host-вызов остаётся валиден); при желании добавить пояснение, что B6 теперь HTTP.
|
||||
|
||||
## Связи
|
||||
- ADR-0003 (staging-гейт `check_staging_status`) — поведение гейта не меняется, повышается достоверность одного из чеков suite.
|
||||
- ADR-0001 (реестр проектов) — источник данных эндпоинта `known_plane_project_ids()`.
|
||||
- 01-brd ORCH-048 (BR-1…BR-5), 02-trz (TR-1…TR-6, §4а), 03-acceptance-criteria (AC-1…AC-5).
|
||||
19
docs/work-items/ORCH-048/10-tech-risks.md
Normal file
19
docs/work-items/ORCH-048/10-tech-risks.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 10 — Технические риски
|
||||
|
||||
**Work Item:** ORCH-048
|
||||
**Stage:** architecture
|
||||
**Решение:** ADR-001 — B6 читает реестр через HTTP-эндпоинт `GET /projects` живого staging-инстанса.
|
||||
|
||||
| ID | Риск | Вероятн. | Влияние | Митигация |
|
||||
|----|------|----------|---------|-----------|
|
||||
| R-1 | Эндпоинт `GET /projects` затрагивает прод-`main.py` (self-hosting) — добавление роута в инструмент, обслуживающий все проекты | низкая | высокое | Эндпоинт additive и read-only; существующие роуты не меняются. Доставка в прод — штатно через staging-гейт (ADR-0003), без внепланового рестарта прод-контейнера. |
|
||||
| R-2 | Утечка чувствительных данных через `/projects` | низкая | среднее | Отдавать только id / repo / prefix / name. Токены, webhook-secret, пути — НЕ включать. Зафиксировано контрактом в ADR-001. |
|
||||
| R-3 | Недоступность `/projects` даёт ложный PASS B6 (потеря защитной функции) | низкая | высокое | TR-4: при не-200 / отсутствии ключа / сетевом сбое — детерминированный FAIL с понятным detail; никогда не PASS «по умолчанию». |
|
||||
| R-4 | Логика вердикта B6 не выделена → невозможно покрыть оба исхода без живого инстанса | средняя | среднее | Вынести вердикт в чистую `_evaluate_b6(known: set[str]) -> tuple[bool, str]`; unit-тест на clean→PASS и polluted→FAIL (AC-2), без docker/HTTP. |
|
||||
| R-5 | Регрессия других чеков / случайная правка `src/projects.py` или `.env*` | низкая | высокое | Менять только блок B6 в `staging_check.py` + добавить эндпоинт. `git diff` не должен содержать `src/projects.py`, `.env*` (AC-3). A1–A3/B4/B5/C — без изменений. |
|
||||
| R-6 | Документация (golden source) разойдётся с кодом | средняя | среднее | В том же PR обновить README (API-таблица + B6), CHANGELOG, STAGING_CHECK.md. Reviewer обязан завернуть при отсутствии (CLAUDE.md правило 6). |
|
||||
| R-7 | Нарушение конвенции «stdlib-only» в `scripts/staging_check.py` | низкая | низкое | B6 использует существующий `_get(...)` (urllib) — сторонние зависимости не добавляются (TR-5). |
|
||||
| R-8 | Эндпоинт не сериализуется (ProjectConfig — frozen dataclass с dict-полями) | низкая | низкое | Возвращать явный dict с нужными полями, не сам dataclass; `agent_models`/`agent_efforts` в ответ не включать. |
|
||||
|
||||
## Открытых архитектурных вопросов нет
|
||||
ТЗ удовлетворяется без нарушения принципов (SQLite не затронут, без новых зависимостей, без новой стадии/QG, без рестарта прода вне staging-гейта). Эскалация не требуется.
|
||||
Reference in New Issue
Block a user