architect(ET): auto-commit from architect run_id=748
All checks were successful
CI / test (push) Successful in 1m8s
All checks were successful
CI / test (push) Successful in 1m8s
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
---
|
||||
work_item: ORCH-123
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-16
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# ADR-001: Стратегия исполнения staging-раннера — host-side ssh + классификация environment-дефекта
|
||||
|
||||
Work Item: **ORCH-123** — staging-runner (ORCH-115) исполняет `docker exec` **изнутри**
|
||||
прод-контейнера, где нет docker CLI → постоянный environment-дефект ложно маршрутизируется как
|
||||
код-фейл (откат `deploy-staging → development`). Стадия: **architecture** (задача-`Bug`,
|
||||
эскалирована `escalate: full-cycle` — `01-brd.md`, D5 ORCH-019).
|
||||
Сквозная регистрация: **`docs/architecture/adr/adr-0049-host-side-docker-execution-boundary.md`**
|
||||
(решение кросс-каттинговое — кодифицирует инвариант «docker-операции исполняются host-side»,
|
||||
охватывающий ORCH-036/058/115/123/101, и амендит execution-strategy-решение adr-0048).
|
||||
|
||||
## Статус
|
||||
Proposed
|
||||
|
||||
## Контекст
|
||||
|
||||
**Установленный факт (сверено по коду и инфраструктуре, BRD §1):** ORCH-115
|
||||
(`src/staging_runner.py:154`, `build_staging_command`) исполняет staging-сюиту командой
|
||||
`["docker", "exec", "orchestrator-staging", "python3", ".../staging_check.py", …]` **изнутри
|
||||
прод-контейнера `orchestrator`** через `proc_group.run_in_process_group` → `subprocess.Popen`.
|
||||
Прод-контейнер docker CLI **не содержит**:
|
||||
|
||||
- `Dockerfile:11` ставит `openssh-client git curl ca-certificates` (+ pinned gitleaks) — бинаря
|
||||
`docker` нет; `python:3.12-slim` его тоже не несёт → `Popen(["docker", …])` падает
|
||||
`FileNotFoundError: [Errno 2] No such file or directory: docker`.
|
||||
- `docker-compose.yml:40` монтирует `/var/run/docker.sock` (rw) в сервис `orchestrator` и
|
||||
добавляет `group_add 999` (ORCH-040 «МИНА 1»). **Сокет в контейнере есть, а CLI-клиента,
|
||||
который бы им воспользовался, — нет.** Проблема не в доступе к Docker, а в **отсутствии
|
||||
исполняемого клиента** в образе.
|
||||
|
||||
Цепочка отказа в проде (job ORCH-116): `Popen` → `OSError` → `proc_group` деградирует в
|
||||
`ProcResult(returncode=None, timed_out=False)` → в `run_staging_gate` (`:528`)
|
||||
`suite_ran = (returncode is not None) and (not timed_out)` = **False** → ветка tool-error →
|
||||
`_handle_tool_error` (`:415`) → инфра-DEFER ×2 → бюджет исчерпан →
|
||||
`write_staging_log(..., "FAILED", tool_error=True)` + `_advance` → **существующий откат
|
||||
`deploy-staging → development`** (как код-фейл задачи). Так до фикса **любая** self-hosting задача,
|
||||
доходящая до `deploy-staging`, обречена на ложный откат.
|
||||
|
||||
**Корневая классификационная ошибка (BRD §1):** двухуровневый исход ORCH-115 (D5) различает только
|
||||
«сюита исполнилась» vs «сюита не исполнилась (транзиентная инфра → DEFER → fail-closed FAILED)».
|
||||
Отсутствие docker CLI — **детерминированный, постоянный environment-дефект**, а не транзиентная
|
||||
икота: ретраи бессмысленны (каждая попытка падает идентично), а терминальный
|
||||
fail-closed-FAILED-откат винит код/разработчика за дефект окружения раннера.
|
||||
|
||||
**Установленный паттерн, от которого ORCH-115 отклонился (сверено по коду):** прод-деплой ORCH-036
|
||||
(`src/self_deploy.py:220`, `build_deploy_command`) и image-freshness ORCH-058
|
||||
(`src/image_freshness.py`) **уже** исполняют все docker-операции **host-side** через
|
||||
`ssh + setsid`/`ssh` на `deploy_ssh_host=127.0.0.1` (ssh-ключ смонтирован, `openssh-client` в
|
||||
образе). `image_freshness.image_revision` (`:185`) прямо документирует инвариант:
|
||||
> *«`docker` lives on the HOST (the container ships only `openssh-client git`)»*
|
||||
и при `ssh_target` обёртывает `docker image inspect` в ssh. Хост-хук
|
||||
`scripts/orchestrator-deploy-hook.sh --build-staging` (`:197`) уже делает host-side
|
||||
`docker exec "$STAGING_CONTAINER" python3 staging_check.py` — на хосте, где docker CLI есть.
|
||||
|
||||
**Вывод:** ORCH-115 при замене LLM-деплойера детерминированным раннером **отклонился** от уже
|
||||
установленного инварианта «docker-операции — host-side, не изнутри app-контейнера». Дефект — в
|
||||
**стратегии исполнения** раннера, **не** в гейте `check_staging_status` и **не** в контракте
|
||||
`15-staging-log.md` (BRD §«Вне объёма»; NFR-1).
|
||||
|
||||
> 🔎 **Проверка точки BRD (как сюита исполнялась до ORCH-115 LLM-деплойером):** до ORCH-115
|
||||
> staging-сюиту исполнял LLM-агент `deployer` (`.openclaw/agents/deployer.md` шаг 1), которому
|
||||
> формулировка шага позволяла исполнить шаги на хосте/в нужном контексте; ORCH-115 жёстко зашил
|
||||
> `docker exec` именно **изнутри** прод-контейнера через `proc_group`. То есть ORCH-115 сделал
|
||||
> ранее-гибкий путь жёстко-неработоспособным в проде. На выбор стратегии фикса это не влияет
|
||||
> (host-side восстанавливает рабочий инвариант независимо от истории).
|
||||
|
||||
## Решение
|
||||
|
||||
Восстановить **host-side стратегию исполнения** staging-сюиты через уже доверенный ssh-канал
|
||||
(ORCH-036/058) и ввести **трёхстороннюю классификацию исхода**, чтобы постоянный
|
||||
environment-дефект и транзиентная инфра **никогда** не оканчивались код-фейл-откатом, а только
|
||||
реально исполнившаяся упавшая сюита — откатывала (анти-over-tolerance). Гейт / контракт артефакта /
|
||||
`STAGE_TRANSITIONS` / схема БД — **байт-в-байт неизменны** (замена *стратегии исполнения продюсера*,
|
||||
не гейта). Аддитивно, под флагами, never-raise, скоуп self-hosting.
|
||||
|
||||
### D1 — Стратегия исполнения: host-side ssh, обёртывающий `docker exec` (BR-1 / FR-1 / AC-1, AC-2)
|
||||
|
||||
`staging_runner.build_staging_command()` теперь строит **ssh-обёрнутую** команду (зеркало
|
||||
`self_deploy.build_deploy_command` / `image_freshness.image_revision(ssh_target=…)`):
|
||||
|
||||
```
|
||||
ssh -o StrictHostKeyChecking=no <deploy_ssh_user>@<deploy_ssh_host> \
|
||||
'docker exec <STAGING_SERVICE> python3 <repos_dir>/<SELF_HOSTING_REPO>/scripts/staging_check.py \
|
||||
--base-url http://localhost:<staging_port> --mode stub'
|
||||
```
|
||||
|
||||
- Внутренняя команда (`docker exec … staging_check.py … --mode stub`) — **та же**, что прежде
|
||||
(контракт сюиты и exit-кода не меняется, BRD §«Вне объёма»); собирается из config (ORCH-101, без
|
||||
host-хардкодов), `shlex.quote`-ится в inner-строку ssh.
|
||||
- Канал — существующий: `deploy_ssh_user`/`deploy_ssh_host` (compose:
|
||||
`ORCH_DEPLOY_SSH_HOST=127.0.0.1`, `ORCH_DEPLOY_SSH_USER=slin`); ssh-ключ смонтирован
|
||||
(`${ORCH_HOST_SSH_DIR}:…/.ssh:ro`), `openssh-client` в образе (`Dockerfile:11`). **Новых
|
||||
секретов/привилегий не вводится** (NFR-3).
|
||||
- Запуск по-прежнему через `proc_group.run_in_process_group` (bounded local timeout + tree-kill
|
||||
локального ssh-клиента, ORCH-110). **Ограничение (документируется, D-Последствия):** tree-kill
|
||||
убивает локальный ssh-клиент, но не гарантирует убийство удалённого `docker exec`-дерева на
|
||||
хосте; backstop — bounded таймаут самой `staging_check.py` внутри сюиты. Это **то же** допущение,
|
||||
что у уже принятого `image_freshness.rebuild_staging_image` (синхронный ssh без remote tree-kill).
|
||||
- **Self-hosting safety (NFR-2 / AC-8):** argv содержит **только** `docker exec <staging-service>
|
||||
python3 staging_check.py … --mode stub` — **никаких** литералов рестарта прод-8500,
|
||||
`docker compose up orchestrator`/`--build` прода, force-push, записи в `main`, правки `.env`.
|
||||
Структурный анти-литерал-тест (ORCH-115 TC-12, расширяется TC-08 ORCH-123) держит инвариант на
|
||||
ssh-обёрнутой команде.
|
||||
|
||||
### D2 — Почему host-side ssh, а НЕ socket/CLI-в-контейнере — security-review (NFR-3 / AC-9) — **ключевое решение безопасности**
|
||||
|
||||
BRD/TRZ перечисляют четыре кандидата (host-side ssh / Docker SDK поверх `docker.sock` / docker CLI
|
||||
в образе / выделенный hook-режим). Любой вариант с **прямым использованием `docker.sock`/CLI
|
||||
изнутри контейнера** = **root-эквивалент на хосте** и требует явного security-обоснования.
|
||||
|
||||
**Разбор поверхности атаки:**
|
||||
- `/var/run/docker.sock` смонтирован rw + `group_add 999` **уже** (ORCH-040, «МИНА 1»). Это
|
||||
**дремлющая** возможность: контейнер может *достучаться* до сокета, но **не имеет клиента**, чтобы
|
||||
им воспользоваться. Добавление docker CLI в образ **или** Docker SDK (`pip install docker`)
|
||||
превратило бы дремлющую возможность в **активный root-эквивалентный путь исполнения**, доступный
|
||||
не только worker-коду, но и **любому subprocess'у в том же контейнере — включая LLM-агентов**,
|
||||
которых оркестратор спавнит (`launcher._spawn`). Это **расширение поверхности атаки** (компрометация
|
||||
агента/зависимости → полный контроль docker → root на хосте, обслуживающем ВСЕ проекты).
|
||||
- Host-side ssh держит docker-операции **на хосте** под **существующим** доверенным key-gated
|
||||
каналом (ORCH-036/058), **не наделяя** in-container код docker-возможностями. Это **строго
|
||||
безопаснее** и согласуется с задокументированным инвариантом `image_freshness`.
|
||||
|
||||
**Решение:** выбран **host-side ssh** (D1). Docker CLI/SDK в контейнер **не добавляется**;
|
||||
использование `docker.sock` **не расширяется**. ORCH-123 поверхность ORCH-040 (смонтированный сокет)
|
||||
**не трогает и не расширяет** (правка compose вне объёма — BRD §«Вне объёма», happy-path
|
||||
прод-деплоя/ORCH-040 не трогаем). Если будущая задача потребует socket/CLI-в-контейнере — это
|
||||
**отдельный** явный security-review; этот ADR его не выдаёт (AC-9 удовлетворён выбором ssh-варианта,
|
||||
переиспользующего доверенный ORCH-036 механизм без расширения привилегий).
|
||||
|
||||
### D3 — Трёхсторонняя классификация исхода (FR-2 / FR-3 / AC-3, AC-4, AC-6) — **ключевое решение**
|
||||
|
||||
Чистая функция `classify_staging_outcome(result, ssh_configured) -> str` (зеркало
|
||||
`merge_gate.classify_retest_failure`, ORCH-110 D2; pure, never-raise) различает **три** класса:
|
||||
|
||||
| Класс | Детерминированный сигнал | Семантика |
|
||||
|-------|--------------------------|-----------|
|
||||
| `permanent-env` | `returncode is None and not timed_out` (spawn-error локального бинаря) **ИЛИ** ssh-target не сконфигурирован **ИЛИ** `returncode in (126, 127)` **ИЛИ** stderr содержит маркеры `command not found` / `executable file not found` / `No such container` / `is not running` / `Cannot connect to the Docker daemon` / `Is the docker daemon running` | Постоянный дефект окружения (нет docker/ssh, контейнер не поднят, демон недоступен). Ретрай бессмыслен. |
|
||||
| `transient-infra` | `timed_out` **ИЛИ** `returncode == 255` (ssh transport/connection) | Транзиентная инфра (сеть/handshake/стенд на миг недоступен). Ретрай осмыслен. |
|
||||
| `suite-ran` | распознанный исполнительный exit-код (любой иной int) **и НЕТ** env-маркера в stderr | Сюита **реально исполнилась** — доверяем коду (`0→SUCCESS`, `≠0→FAILED`). |
|
||||
|
||||
- **Дизамбигуация «сюита exit=1 vs `docker exec` No such container exit=1»** — по **скану stderr на
|
||||
env-маркеры** (а не по голому exit-коду): env-маркер → `permanent-env`; нет маркера → `suite-ran`
|
||||
(`staging_check.py` пишет PASS/FAIL в stdout, её коды — 0/≠0 без 126/127/255). Маркер-сет
|
||||
финализирует разработчик; набор выше — стартовый, по образцу scope-guard `merge_gate`.
|
||||
- **Анти-over-tolerance (BR-3 / AC-4, зеркало ORCH-110 BR-6):** распознанный `suite-ran` exit-код
|
||||
**никогда** не реклассифицируется в инфру → реальный фейл сюиты (`exit≠0`) идёт прежним путём.
|
||||
- **Fail-safe при неоднозначности:** неизвестный сигнал по умолчанию → `transient-infra` (DEFER), а
|
||||
**не** `suite-ran` — mis-route environment→код-фейл есть именно тот дефект, что чиним; но
|
||||
распознанный suite-exit без env-маркера всегда `suite-ran` (BR-3 не ослаблен).
|
||||
|
||||
### D4 — Маршрутизация исходов (BR-2 / BR-3 / FR-2 / FR-3 / AC-3, AC-4) — **инвариант: инфра ≠ код-фейл**
|
||||
|
||||
| Класс | Действие |
|
||||
|-------|----------|
|
||||
| `suite-ran` | **Без изменений** (ORCH-115): `write_staging_log` + `_advance` → `SUCCESS`→под-гейты+Phase A; `FAILED`→**прежний** откат `deploy-staging → development` + developer-retry (`stage_engine.py:932`). BR-3 байт-в-байт. |
|
||||
| `permanent-env` | **Немедленный отличимый infra-исход:** DEFER-цикл **пропускается** (ретрай постоянного дефекта бессмыслен — FR-3); записывается структурный `permanent-env`-лог + операторский алерт («инфра/окружение, **НЕ дефект кода**», кликабельный номер); **НЕ** `_advance`, **НЕ** откат, **НЕ** developer-retry. Задача **остаётся на `deploy-staging`** (infra-HOLD). Счётчик `permanent_env`++. |
|
||||
| `transient-infra` | **Существующий** bounded DEFER (re-queue `deployer`-джоба + delay + restart-safe маркер `_INFRA_RETRY_MARKER`, cap `staging_runner_infra_max_retries`). |
|
||||
|
||||
**Изменение терминала исчерпания DEFER (супершид ORCH-115 ADR D5):** при исчерпании бюджета DEFER
|
||||
исход — **infra-HOLD + алерт** (как `permanent-env`), а **НЕ** прежний fail-closed
|
||||
`write_staging_log("FAILED") + _advance` (= откат). Прежний терминал нарушал BR-2 (транзиентная инфра,
|
||||
которая не прояснилась, всё равно ложно откатывалась как код-фейл). Новый терминал выравнивает
|
||||
staging-раннер с моделью `merge_gate` infra-tolerance (ORCH-110 D3: исчерпание инфра → алерт «НЕ
|
||||
дефект кода», задача **не** уходит в `development`).
|
||||
|
||||
**Корневой инвариант (нормативно):** «сюита **не исполнилась**» (environment ИЛИ транзиентная инфра)
|
||||
**никогда** не оканчивается код-фейл-откатом `deploy-staging → development` и **никогда** не жжёт
|
||||
developer-retry. Откат — **только** для реально исполнившейся сюиты с `exit≠0` (`suite-ran`/`FAILED`).
|
||||
Это закрывает RCA-класс ORCH-110/111…117 на staging-ребре.
|
||||
|
||||
**Re-drive / отсутствие вечного залипания (NFR-5 / AC-11):** infra-HOLD оставляет задачу на
|
||||
`deploy-staging` в восстановимом состоянии (worker не падает, тугого ретрай-цикла нет, исход виден в
|
||||
алерте + `GET /queue` + структурном логе). После починки окружения оператором задача переоткатывается
|
||||
**существующим** механизмом (оператор / reconciler ORCH-053), без нового движка. **Граница (риск
|
||||
R-2):** reconciler/reaper **должен** трактовать infra-held `deploy-staging`-задачу как удержанную
|
||||
(пере-выдать `deployer`-джоб), **не** откатывать её в `development` — иначе реинтродукция дефекта;
|
||||
developer верифицирует (`10-tech-risks.md`).
|
||||
|
||||
### D5 — Prod-like preflight (BR-4 / FR-4 / AC-5)
|
||||
|
||||
`staging_runner.preflight() -> tuple[bool, str]` (bounded, never-raise, self-hosting-скоуп —
|
||||
`applies(repo)` первым):
|
||||
- Пробит **канал исполнения** host-side коротким bounded ssh-зондом:
|
||||
`ssh <target> 'command -v docker >/dev/null && docker inspect -f "{{.State.Running}}"
|
||||
<STAGING_SERVICE>'` → распознаёт «нет docker на хосте» / «staging-контейнер не поднят» / «ssh
|
||||
недоступен» **до** того, как реальная задача дойдёт до ложного исхода.
|
||||
- Вызывается на **старте сервиса** в `main.lifespan` (best-effort, после
|
||||
`requeue_running_jobs`/`recover_on_startup`, **никогда не блокирует старт**) → условие инцидента
|
||||
всплывает на рестарте/деплое самого орка, а не постфактум на реальной задаче. Эмитит структурный
|
||||
лог + Telegram-алерт при неудаче + поле в `snapshot()`.
|
||||
- **Чисто наблюдательный:** preflight **не** гейтит/блокирует конвейер, **не** трогает стадии/QG,
|
||||
never-raise (FR-4). Регресс-тест TC-01 моделирует «нет docker CLI в контейнере» (монки-патч
|
||||
argv/spawn) — **красный до фикса** (in-container `docker exec` → spawn-error → tool-error → ложный
|
||||
откат), **зелёный после** (host-side стратегия + `permanent-env`-классификация → нет код-фейл-отката).
|
||||
|
||||
### D6 — Условность / kill-switch / обратимость (FR-5 / NFR-4 / NFR-6 / AC-10)
|
||||
|
||||
`config.py` (паттерн `staging_runner_*` / ORCH-101 «дефолт = боевое»):
|
||||
- **Новый:** `staging_runner_exec_host_side: bool = True` (env `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE`).
|
||||
`True` (дефолт) → host-side ssh-стратегия (D1, фикс). `False` → прежний in-container `docker exec`
|
||||
(валиден **только** там, где docker CLI запечён в образ; в текущем проде — нет). Добавить в
|
||||
`.env.example`.
|
||||
- **Переиспользуются:** `staging_runner_enabled` (kill-switch; `False` → прежний LLM-`deployer` через
|
||||
`_spawn` **байт-в-байт**, ORCH-115 fail-safe), `staging_runner_repos` (CSV; пусто → self-hosting
|
||||
only через `is_self_hosting_repo`), `staging_runner_timeout_s`, `staging_runner_infra_max_retries`/
|
||||
`_retry_delay_s` (DEFER-бюджет транзиентной инфры), `deploy_ssh_user`/`deploy_ssh_host`/
|
||||
`deploy_host_repo_path` (ssh-канал).
|
||||
- `applies(repo)` (локально, без сети) — **первым** в `should_intercept`: выключенный флаг = нулевой
|
||||
оверхед, на `deploy-staging` штатно `_spawn`. **Откат:** `ORCH_STAGING_RUNNER_ENABLED=false`
|
||||
(→ LLM-путь) **или** `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false` (→ прежний in-container call).
|
||||
Для прочих репо `applies==False` → no-op (нулевая регрессия enduro). NFR-6.
|
||||
|
||||
### D7 — Сохранность контракта (NFR-1 / AC-7) — границы O1
|
||||
|
||||
**Байт-в-байт неизменны:** `STAGE_TRANSITIONS` (`src/stages.py`), реестр и имена `QG_CHECKS`/
|
||||
`check_staging_status`/`_parse_staging_status`, machine-verdict-ключи (`staging_status:`/
|
||||
`deploy_status:`/…), **схема БД** (restart-safe счётчик DEFER — маркер в `task_content`, без
|
||||
миграции). `15-staging-log.md` сохраняет `staging_status:`-frontmatter (UPPERCASE) + 52c-схему
|
||||
(`author_agent: staging-runner` / `model_used: n/a`); вердикт читается ТОЛЬКО из frontmatter через
|
||||
`src/frontmatter.py`. Это замена *стратегии исполнения продюсера*, не гейта/стадии (зеркало
|
||||
инварианта ORCH-115 NFR-1). transition-lease ORCH-114 берётся **внутри** `advance_stage` — раннер
|
||||
его не трогает (граница O1, как ORCH-115 D7).
|
||||
|
||||
### D8 — Наблюдаемость (BR-6 / AC-12-набл.)
|
||||
|
||||
`_STAGING_RUNNER_COUNTERS` расширяется ключом `permanent_env` (+ существующие
|
||||
`runs`/`success`/`failed`/`tool_error`/`deferred`). `snapshot()` / read-only блок `staging_runner` в
|
||||
`GET /queue` различает **три** не-успешных класса: `code-fail` (исполнившаяся сюита `FAILED`) vs
|
||||
`transient-infra` (`deferred`) vs `permanent-env` (новый). Один структурный лог-вердикт на прогон
|
||||
(`work_item`/`repo`/`exit_code`/`status`/`duration_s`/`outcome` ∈ {`code-pass`,`code-fail`,
|
||||
`transient-infra`,`permanent-env`}) + Telegram-алерт на `permanent-env`/infra-исчерпание с
|
||||
кликабельным номером и явной формулировкой «инфра/окружение, НЕ дефект кода».
|
||||
|
||||
### D9 — Норматив сопровождения docs (BR-5 / FR-6 / AC-12) — спека для development
|
||||
|
||||
Документация границы исполнения и витрина **коуплены к состоянию кода** (описывают post-fix
|
||||
поведение; структурный анти-дрейф TC-13 проверяет наличие разделов). По прецеденту **ORCH-115 D11 /
|
||||
adr-0048** (и маршрутизации analyst'а — `02-trz.md` §2) эти правки применяет **developer атомарно с
|
||||
кодом** в том же PR, **не** architecture-стадия (иначе доки/тесты описывают не-существующий ещё код).
|
||||
Точная спека (developer применяет дословно):
|
||||
|
||||
- **`docs/operations/INFRA.md`** — в раздел `## ⚠️ Self-hosting` (или новый под-раздел «Граница
|
||||
исполнения docker-операций») зафиксировать: контейнер `orchestrator` несёт **только**
|
||||
`openssh-client git curl` (docker CLI **нет**); `/var/run/docker.sock` смонтирован, но **не
|
||||
используется изнутри** (нет клиента, сознательно — см. D2); **все** docker-операции
|
||||
(прод-деплой ORCH-036, image-freshness ORCH-058, **staging-runner ORCH-123**) исполняются
|
||||
**host-side** через ssh на `127.0.0.1` под смонтированным ключом; staging-сюита (`staging_check.py`)
|
||||
по-прежнему исполняется **внутри** `orchestrator-staging` (8501) — меняется лишь **кто инициирует**
|
||||
`docker exec` (host vs прод-контейнер).
|
||||
- **`docs/architecture/README.md`** — в секции про staging-гейт/ORCH-115 (или соседней) описать
|
||||
host-side стратегию исполнения staging-раннера и трёхстороннюю классификацию (env ≠ код-фейл),
|
||||
сослаться на `adr-0049`.
|
||||
- **`CLAUDE.md`** — раздел ORCH-115 дополнить фиксом ORCH-123 (host-side exec + классификация
|
||||
environment-дефекта); **`CHANGELOG.md`** — запись `fix:`; **`docs/overview/`** — если затронут
|
||||
машинно-проверяемый факт (`tests/test_system_docs.py`).
|
||||
- **Анти-дрейф ORCH-115 (TC-14):** существующие `tests/test_orch115_staging_runner.py` и
|
||||
LLM-determinization-тесты остаются зелёными (фикс не ломает инварианты ORCH-115: контракт/гейт/
|
||||
один транспорт LLM `_spawn`/«ровно один first_slice» целы).
|
||||
|
||||
## Альтернативы
|
||||
|
||||
- **Docker CLI в образ (`Dockerfile`) + `docker exec` через смонтированный сокет** — отвергнуто
|
||||
(D2): активирует дремлющий root-эквивалентный путь для всего, что исполняется в контейнере (вкл.
|
||||
LLM-агентов) — расширение поверхности атаки. Требовало бы отдельного security-review; host-side ssh
|
||||
достигает цели без расширения привилегий.
|
||||
- **Docker SDK (`pip install docker`) поверх `docker.sock`** — отвергнуто: та же security-проблема,
|
||||
что выше, + новая зависимость + не переиспользует доказанный канал.
|
||||
- **Выделенный режим хука (`--run-staging-check`) через ssh** — отвергнуто как избыточное: прецедент
|
||||
`image_freshness.image_revision(ssh_target=…)` уже исполняет docker-команду через **прямой** ssh
|
||||
(без хука); `--build-staging` хука перегружен ребилдом образа (это работа ORCH-058, не раннера) —
|
||||
переиспользовать его нельзя, а заводить новый hook-режим тяжелее прямого ssh-wrap. Прямой
|
||||
ssh-обёртки (D1) достаточно.
|
||||
- **tool-error → немедленный `FAILED`-откат на `development`** (исходный путь до ORCH-115 D5) —
|
||||
отвергнуто: это в точности дефект ORCH-123 (и анти-паттерн ORCH-110).
|
||||
- **Сохранить ORCH-115 D5 терминал (исчерпание DEFER → fail-closed `FAILED` + `_advance`)** —
|
||||
отвергнуто: нарушает BR-2 (транзиентная инфра, не прояснившаяся за N попыток, всё равно ложно
|
||||
откатывается как код-фейл). Заменён на infra-HOLD + алерт (D4), как ORCH-110 D3.
|
||||
- **Двухсторонняя классификация (как ORCH-115)** — отвергнуто: не отличает постоянный
|
||||
environment-дефект (ретрай бессмыслен, нужен немедленный HOLD) от транзиентной инфры (ретрай
|
||||
осмыслен). Трёхсторонняя (D3) различает (FR-3).
|
||||
|
||||
## Последствия
|
||||
|
||||
- **+** На `deploy-staging` для `orchestrator` staging-сюита **реально исполняется в проде**
|
||||
(host-side через доверенный ssh-канал); задача уровня ORCH-116 доходит до `deploy`, а не
|
||||
откатывается (BR-1 / AC-2).
|
||||
- **+** **Инфра/environment ≠ код-фейл** на staging-ребре: ни environment-дефект, ни транзиентная
|
||||
инфра не дают ложного отката `→ development` и не жгут developer-retry (BR-2). Закрыт RCA-класс
|
||||
ORCH-110 на staging-ребре полностью (не только частично, как ORCH-115 D5).
|
||||
- **+** Анти-over-tolerance цел: реально исполнившаяся упавшая сюита по-прежнему откатывает (BR-3).
|
||||
- **+** Переиспользует существующий доверенный ssh-канал (ORCH-036/058) — **без** расширения
|
||||
привилегий, **без** docker CLI/SDK в контейнере (NFR-3); согласовано с задокументированным
|
||||
инвариантом `image_freshness`.
|
||||
- **+** Полная обратимость двумя флагами (`staging_runner_enabled` → LLM;
|
||||
`staging_runner_exec_host_side` → in-container); контракт/гейт/ребро/схема БД неизменны.
|
||||
- **−** Remote tree-kill ограничен локальным ssh-клиентом (удалённое `docker exec`-дерево не
|
||||
гарантированно убивается tree-kill'ом) — **то же** допущение, что у принятого
|
||||
`image_freshness.rebuild_staging_image`; backstop — bounded таймаут внутри `staging_check.py`.
|
||||
- **−** Permanent-env / исчерпавшая-DEFER задача **держится** на `deploy-staging` до починки
|
||||
окружения оператором → блокирует serial-gate репо (ORCH-088) для **более поздних** задач.
|
||||
Принятый tradeoff (зеркало ORCH-110 infra-HOLD): environment-проблема, требующая оператора,
|
||||
**должна** остановить репо, а не молча ложно-откатывать; скоуп self-hosting only.
|
||||
- **−** Классификация по stderr-маркерам имеет остаточную неоднозначность. Митигейшн: fail-safe
|
||||
default → `transient-infra` (не `suite-ran`); распознанный suite-exit без env-маркера всегда
|
||||
доверяется (BR-3); набор маркеров — по образцу `merge_gate` scope-guard, под покрытием TC-06.
|
||||
- **Откат:** `ORCH_STAGING_RUNNER_ENABLED=false` → прежний LLM-`deployer` на `deploy-staging`
|
||||
байт-в-байт; либо `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false` → прежний in-container `docker exec`.
|
||||
|
||||
## Ссылки
|
||||
- BRD: `docs/work-items/ORCH-123/01-brd.md`; ТЗ: `02-trz.md`; Acceptance: `03-acceptance-criteria.md`;
|
||||
Test-plan: `04-test-plan.yaml`
|
||||
- Инфра: `docs/work-items/ORCH-123/07-infra-requirements.md`; Риски: `10-tech-risks.md`
|
||||
(данные — `08` N/A: схема БД не меняется, TRZ §5)
|
||||
- Сквозной ADR: `docs/architecture/adr/adr-0049-host-side-docker-execution-boundary.md`
|
||||
- Амендит / опирается на: ORCH-115 (`docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`,
|
||||
`adr-0048`) — стратегия исполнения D3 и терминал D5; ORCH-036 (`adr-0007-executable-self-deploy.md` /
|
||||
`self_deploy` ssh-механизм), ORCH-058 (`adr-0008-staging-image-provenance.md` / `image_freshness`
|
||||
host-side docker), ORCH-110 (`adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md` / proc_group
|
||||
+ infra-tolerance + classify), ORCH-101 (`adr-0036-replication-foundation-host-parametrization.md` /
|
||||
host-параметризация)
|
||||
- Сверено по коду: `src/staging_runner.py:154/415/487/528`, `src/self_deploy.py:220/287`,
|
||||
`src/image_freshness.py:185/246`, `scripts/orchestrator-deploy-hook.sh:166/197`,
|
||||
`src/proc_group.py`, `src/merge_gate.py:229`, `src/agents/launcher.py:406/438`,
|
||||
`src/main.py:16/58/73`, `Dockerfile:11`, `docker-compose.yml` (docker.sock + ssh mounts),
|
||||
`src/config.py:301-304/455-459`
|
||||
85
docs/work-items/ORCH-123/07-infra-requirements.md
Normal file
85
docs/work-items/ORCH-123/07-infra-requirements.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
work_item: ORCH-123
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-16
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 07 — Инфра-требования: ORCH-123 — host-side исполнение staging-раннера
|
||||
|
||||
Work Item: **ORCH-123** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
> When-applicable. **Топология не меняется** (новых контейнеров/портов/томов/compose-правок нет).
|
||||
> Файл фиксирует **границу исполнения** (BR-5/FR-6/AC-12) и **предусловия канала**, на которые
|
||||
> раннер теперь опирается программно после фикса (раньше тот же `docker exec` исполнялся изнутри
|
||||
> контейнера — и поэтому всегда падал в проде).
|
||||
|
||||
## I-1. Граница исполнения docker-операций (нормативно — BR-5 / FR-6)
|
||||
|
||||
**Прод-контейнер `orchestrator` (8500) НЕ содержит docker CLI.** `Dockerfile:11` ставит
|
||||
`openssh-client git curl ca-certificates` (+ pinned gitleaks); `python:3.12-slim` docker не несёт.
|
||||
`/var/run/docker.sock` **смонтирован** (`docker-compose.yml`, rw + `group_add 999`, ORCH-040 «МИНА 1»),
|
||||
но **изнутри контейнера не используется** (нет клиента — сознательно, см. ADR-001 D2: добавление
|
||||
клиента/SDK = активация root-эквивалентного пути для всего, что исполняется в контейнере).
|
||||
|
||||
**Все docker-операции исполняются host-side через ssh на `127.0.0.1`** (доверенный канал, ключ
|
||||
смонтирован `${ORCH_HOST_SSH_DIR}:…/.ssh:ro`, `openssh-client` в образе):
|
||||
|
||||
| Операция | Компонент | Канал (host-side) |
|
||||
|----------|-----------|-------------------|
|
||||
| Прод-деплой (рестарт 8500) | `self_deploy.build_deploy_command` (ORCH-036) | `ssh + setsid bash <hook> --deploy` |
|
||||
| Ребилд staging-образа из validated commit | `image_freshness.rebuild_staging_image` (ORCH-058) | `ssh … bash <hook> --build-staging` |
|
||||
| Инспекция revision-label образа | `image_freshness.image_revision(ssh_target=…)` (ORCH-058) | `ssh … docker image inspect …` |
|
||||
| **Staging-сюита на `deploy-staging`** | **`staging_runner.build_staging_command` (ORCH-123)** | **`ssh … docker exec orchestrator-staging python3 staging_check.py …`** |
|
||||
|
||||
`scripts/staging_check.py` по-прежнему исполняется **внутри** `orchestrator-staging` (8501) — это
|
||||
контракт сюиты (там python3 + приложение 8501, bind-mount `/repos/orchestrator/scripts/…`, ORCH-048).
|
||||
**Меняется только инициатор `docker exec`** (host-side ssh вместо изнутри прод-контейнера), не сама
|
||||
сюита и не её exit-код-контракт.
|
||||
|
||||
## I-2. Предусловия канала исполнения
|
||||
|
||||
Раннер после фикса программно опирается на (preflight их пробит на старте сервиса — ADR-001 D5):
|
||||
- ssh-доступность `deploy_ssh_host` (`127.0.0.1`) под смонтированным ключом (та же, что использует
|
||||
прод-деплой ORCH-036 / image-freshness ORCH-058 — уже в проде);
|
||||
- наличие `docker` CLI **на хосте** (есть; контейнер его не несёт);
|
||||
- поднятый и доступный staging-контейнер `orchestrator-staging` (8501) (Допущение А1 ORCH-115).
|
||||
|
||||
Недоступность любого предусловия классифицируется **детерминированно** (ADR-001 D3): постоянный
|
||||
дефект (`docker`/контейнер/ssh недоступны как класс) → `permanent-env` → **infra-HOLD + алерт**
|
||||
(никогда тихий advance / ложный green / откат как код-фейл); транзиентная икота (timeout/ssh-255) →
|
||||
bounded DEFER.
|
||||
|
||||
## I-3. Переменные окружения / секреты
|
||||
|
||||
**Новый env-ключ** (config, дефолт = боевое; добавить в `.env.example`):
|
||||
- `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE` (`staging_runner_exec_host_side`, дефолт `True`) — выбор
|
||||
стратегии исполнения. `True` → host-side ssh (фикс). `False` → прежний in-container `docker exec`
|
||||
(валиден только там, где docker CLI запечён в образ). Откат фикса — без LLM-пути.
|
||||
|
||||
**Переиспользуются (без изменений):** `ORCH_STAGING_RUNNER_ENABLED`/`_REPOS`/`_TIMEOUT_S`/
|
||||
`_INFRA_MAX_RETRIES`/`_INFRA_RETRY_DELAY_S` (ORCH-115); `ORCH_DEPLOY_SSH_USER`/`ORCH_DEPLOY_SSH_HOST`/
|
||||
`ORCH_DEPLOY_HOST_REPO_PATH` (ssh-канал ORCH-036); `ORCH_STAGING_PORT`/`ORCH_REPOS_DIR` (ORCH-101).
|
||||
**Секретов не вводит** — переиспользует существующий смонтированный ssh-ключ; команда строится из
|
||||
существующих host-параметров (анти-дрейф `tests/test_no_host_hardcodes.py`).
|
||||
|
||||
## I-4. Деплой / рестарт (self-hosting инвариант, NFR-2 / AC-8)
|
||||
|
||||
Раннер на `deploy-staging` **никогда** не рестартит прод-8500, не выполняет
|
||||
`docker compose up orchestrator`/`--build` прода, не пушит force в `main`, не правит `.env`/
|
||||
`docker-compose.yml`/`Dockerfile` (BR-7 ORCH-115 / AC-8; структурный анти-литерал-тест на
|
||||
ssh-обёрнутой команде — TC-08). Argv содержит только `ssh … docker exec orchestrator-staging python3
|
||||
staging_check.py … --mode stub`. ORCH-040 (смонтированный сокет) **не трогается и не расширяется**.
|
||||
Прод-выкат самого ORCH-123 — штатно через staging-гейт (8501) → `Confirm Deploy` (ORCH-059); это
|
||||
**docs/код+config**-изменение, активируется на следующем worktree от `main`, включается флагом
|
||||
(дефолт `True`, обратимо двумя флагами — ADR-001 D6).
|
||||
|
||||
## I-5. CI/CD
|
||||
|
||||
**Без изменений `.gitea/workflows/`.** Новый/расширенный `tests/test_orch123_staging_runner_exec.py`
|
||||
исполняется существующим `pytest tests/ -q` (моки subprocess/ssh-spawn и `advance_stage`; живой
|
||||
Claude CLI / docker / ssh / сеть не требуются — окружение «нет docker CLI» моделируется монки-патчем
|
||||
argv/spawn, `04-test-plan.yaml` notes). Существующие `tests/test_orch115_staging_runner.py` остаются
|
||||
зелёными (TC-14).
|
||||
93
docs/work-items/ORCH-123/10-tech-risks.md
Normal file
93
docs/work-items/ORCH-123/10-tech-risks.md
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
work_item: ORCH-123
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-16
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 10 — Технические риски: ORCH-123 — host-side исполнение staging-раннера
|
||||
|
||||
Work Item: **ORCH-123** · Repo: **orchestrator** · Стадия: architecture
|
||||
|
||||
Формат: риск → вероятность/влияние → митигация (привязка к решению ADR-001 / AC).
|
||||
|
||||
---
|
||||
|
||||
## R-1 — Security: расширение поверхности атаки docker (NFR-3 / AC-9)
|
||||
**Влияние:** критичное (root-эквивалент на общем хосте). **Вероятность (при выбранной стратегии):**
|
||||
низкая.
|
||||
Любой вариант с docker CLI/SDK **в контейнере** поверх смонтированного `docker.sock` (rw + group_add
|
||||
999, ORCH-040) превращает дремлющую возможность в активный root-эквивалентный путь, доступный
|
||||
worker-коду **и любому subprocess'у** (вкл. LLM-агентов).
|
||||
**Митигация:** выбран **host-side ssh** (ADR-001 D1/D2) — docker-операции на хосте под существующим
|
||||
доверенным key-gated каналом (ORCH-036/058), **без** docker CLI/SDK в контейнере и **без** расширения
|
||||
использования сокета. ORCH-123 поверхность ORCH-040 не трогает. Любой будущий socket/CLI-в-контейнере
|
||||
вариант — отдельный явный security-review.
|
||||
|
||||
## R-2 — Реконсилятор/reaper откатывает infra-held задачу как код-фейл (FR-2 / AC-3 / AC-11)
|
||||
**Влияние:** высокое (реинтродукция дефекта ORCH-123). **Вероятность:** средняя (зависит от
|
||||
поведения reconciler ORCH-053 на застрявшем `deploy-staging`).
|
||||
`permanent-env` / исчерпавшая-DEFER задача держится на `deploy-staging` (ADR-001 D4). Если reconciler/
|
||||
reaper трактует «застрял на deploy-staging без running-job» как повод откатить на `development` —
|
||||
дефект вернётся.
|
||||
**Митигация:** developer **обязан** верифицировать, что reconciler/reaper трактует infra-held
|
||||
deploy-staging-задачу как **удержанную** (пере-выдать `deployer`-джоб для re-drive после починки
|
||||
окружения), а **не** откатывает в `development`. Регресс-покрытие — поведенческий тест на «held не
|
||||
становится rollback». Наблюдаемость (D8) делает held видимым (alert + `GET /queue` + лог).
|
||||
|
||||
## R-3 — Over-tolerance: реальный код-фейл сюиты замаскирован как «инфра» (BR-3 / AC-4)
|
||||
**Влияние:** высокое (сломанный код проходит staging-гейт). **Вероятность:** низкая.
|
||||
Трёхсторонняя классификация (D3) может ошибочно отнести реальный `exit≠0` сюиты к
|
||||
`permanent-env`/`transient-infra` (→ нет отката) при коллизии exit-кодов
|
||||
(`docker exec` No such container = 1, и `staging_check` fail = 1).
|
||||
**Митигация (D3):** дизамбигуация по **скану stderr на env-маркеры**, не по голому exit-коду:
|
||||
распознанный suite-exit **без** env-маркера всегда `suite-ran` (никогда не реклассифицируется в
|
||||
инфру — зеркало ORCH-110 BR-6). Fail-safe default при неоднозначности → `transient-infra` (DEFER), не
|
||||
тихий проход. Маркер-сет покрыт TC-06; reviewer проверяет анти-over-tolerance (≥P1, как ORCH-110).
|
||||
|
||||
## R-4 — Remote tree-kill: удалённое `docker exec`-дерево переживает таймаут (NFR-5)
|
||||
**Влияние:** среднее (осиротевший pytest на хосте — класс ORCH-109/110). **Вероятность:** низкая.
|
||||
`proc_group` tree-kill убивает **локальный** ssh-клиент при таймауте, но не гарантирует убийство
|
||||
удалённого `docker exec`-дерева на хосте (процессы — дети docker-демона, не ssh-сессии).
|
||||
**Митигация:** backstop — bounded таймаут внутри самой `staging_check.py`; это **то же** принятое
|
||||
допущение, что у `image_freshness.rebuild_staging_image` (синхронный ssh без remote tree-kill).
|
||||
Документируется как known-limitation (ADR-001 D-Последствия); не блокер.
|
||||
|
||||
## R-5 — Held-задача клинит serial-gate репо (NFR-5 / AC-11)
|
||||
**Влияние:** среднее (более поздние задачи репо ждут). **Вероятность:** средняя (при реальном
|
||||
environment-сбое).
|
||||
infra-HOLD держит задачу на `deploy-staging` → ORCH-088 serial-gate блокирует **более поздние** задачи
|
||||
того же репо до снятия.
|
||||
**Митигация:** принятый tradeoff (зеркало ORCH-110 infra-HOLD): environment-проблема, требующая
|
||||
оператора, **должна** остановить репо, а не молча ложно-откатывать (что было бы хуже — порча
|
||||
done-критериев + расход developer-retry). Скоуп **self-hosting only** (enduro не затронут). Громкий
|
||||
alert «инфра, НЕ дефект кода» (D8) → быстрая операторская реакция; `GET /queue` показывает held.
|
||||
|
||||
## R-6 — ssh-target не сконфигурирован / пустой `deploy_ssh_host` (FR-1 / D6)
|
||||
**Влияние:** среднее. **Вероятность:** низкая (в проде `ORCH_DEPLOY_SSH_HOST=127.0.0.1` задан compose).
|
||||
Config-дефолт `deploy_ssh_host=""`; на нестандартном хосте host-side стратегия без ssh-target
|
||||
неработоспособна.
|
||||
**Митигация:** пустой ssh-target → классификация `permanent-env` (D3) → infra-HOLD + alert (никогда
|
||||
ложный откат); preflight на старте (D5) ловит это до реальной задачи. `applies(repo)` self-hosting-only
|
||||
→ прочие репо не затронуты.
|
||||
|
||||
## R-7 — Кросс-каттинг: правка маркированных блоков (CLAUDE.md §9 / ORCH-078)
|
||||
**Влияние:** среднее. **Вероятность:** низкая.
|
||||
Затрагиваются блоки с маркерами ORCH-115 (staging_runner, прямой объект), ORCH-036 (`self_deploy`
|
||||
ssh), ORCH-058 (`image_freshness` host-side docker), ORCH-110 (`proc_group`/infra-tolerance/classify),
|
||||
ORCH-101 (host-параметризация), ORCH-040 (docker.sock mount), ORCH-114 (transition-lease граница).
|
||||
**Митигация:** developer **перед** изменением сверяет инварианты с их `06-adr/` (CLAUDE.md §9). Блок с
|
||||
≥3 маркерами агрегируется **сводным сквозным ADR** `adr-0049` (ADR-001 Сквозная регистрация). Не
|
||||
ломать: один транспорт LLM `_spawn` (`llm-usage-policy.md` §5), `STAGE_TRANSITIONS`/`QG_CHECKS`/
|
||||
machine-verdict (NFR-1), transition-lease берётся внутри `advance_stage` (граница O1), INV-4 (мерж
|
||||
только через PR-merge, никогда push в `main`).
|
||||
|
||||
## R-8 — Регрессия конвейера / гейта (NFR-1 / AC-7)
|
||||
**Влияние:** критичное. **Вероятность:** очень низкая.
|
||||
Случайное изменение `check_staging_status`/`_parse_staging_status`/`staging_status:`/`STAGE_TRANSITIONS`/
|
||||
схемы БД.
|
||||
**Митигация:** фикс — замена **стратегии исполнения продюсера**, не гейта (зеркало ORCH-115 NFR-1).
|
||||
Структурный анти-регресс-тест (TC-11) держит имена/семантику/ключи байт-в-байт; миграции БД нет
|
||||
(restart-safe DEFER-счётчик — маркер в `task_content`, TRZ §5). Анти-дрейф ORCH-115 (TC-14) зелёный.
|
||||
Reference in New Issue
Block a user