--- work_item: ORCH-123 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-16 model_used: claude-opus-4-8 escalate: full-cycle --- # 01 — BRD / Bug-report: ORCH-123 — staging-runner assumes Docker CLI inside the orchestrator container and false-fails deploy-staging Work Item: **ORCH-123** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug → эскалация в full-cycle** > ⚠️ **`escalate: full-cycle` (ADR-001 D5 ORCH-019).** Метка задачи — `Bug`, но по сути это > **архитектурный + safety-critical (self-hosting)** дефект: требуется **решение о стратегии > исполнения** staging-сюиты из прод-окружения (host-side ssh / Docker SDK поверх смонтированного > сокета / docker CLI в образе / выделенный hook-режим), и любой вариант с доступом к > `docker.sock`/CLI из контейнера требует **явного security-review** (доступ к сокету = root-эквивалент > на хосте). Это не «однострочная» правка кода — нужен ADR. Поэтому выпускается **полный** > analysis-пакет (а не облегчённый bug-пакет). Оператор снимает багфикс-трек: > `POST /bug-fast-track/escalate?work_item=ORCH-123` → задача пойдёт через стадию `architecture` > (architect выпустит ADR для стратегии исполнения staging-runner). --- ## 1. Бизнес-контекст и проблема ### Симптом (наблюдаемое — установленный факт из прод-логов) Задача **ORCH-116** (поверх раската ORCH-115) дошла до стадии `deploy-staging` с **зелёными reviewer и tester**, но **детерминированный staging-runner упал ещё до запуска staging-сюиты**. Запись из прод-логов: ``` proc_group: spawn error for [docker, exec, orchestrator-staging, python3, /repos/orchestrator/scripts/staging_check.py, ...]: [Errno 2] No such file or directory: docker ``` Далее runner отработал свой инфра-DEFER **дважды**, исчерпал `staging_runner_infra_max_retries=2`, записал `15-staging-log.md` с `staging_status: FAILED` и `stage_engine` **откатил ORCH-116 на `development`** — как будто это дефект кода/тестов задачи, чем оно не является. ### Причина симптома (установленный факт — верифицировано по коду и инфраструктуре) ORCH-115 (`src/staging_runner.py`) исполняет staging-сюиту командой `docker exec orchestrator-staging python3 .../staging_check.py …` **изнутри прод-контейнера `orchestrator`** через `proc_group.run_in_process_group`. Прод-контейнер **не содержит docker CLI**: - **`Dockerfile:11`** ставит `openssh-client git curl ca-certificates` — **бинаря `docker` нет**; `python:3.12-slim` его тоже не содержит. Поэтому `subprocess.Popen(["docker", …])` падает `FileNotFoundError: [Errno 2] No such file or directory: docker`. - **`docker-compose.yml:40`** монтирует `/var/run/docker.sock` (rw) в сервис `orchestrator` и добавляет `group_add: ${ORCH_DOCKER_GID}`. То есть **сокет в контейнере есть, а CLI-бинаря, который бы им воспользовался, — нет**. (Это важное уточнение корня: проблема не в доступе к Docker, а в **отсутствии исполняемого клиента** в образе.) Цепочка отказа в коде: `build_staging_command()` (`staging_runner.py:154`) → `["docker","exec",…]` → `run_staging_suite()` → `proc_group.run_in_process_group` → `Popen` ⇒ `OSError` ⇒ `ProcResult(returncode=None, timed_out=False)` ⇒ в `run_staging_gate` (`staging_runner.py:528`) `suite_ran = (returncode is not None) and (not timed_out)` = **False** ⇒ ветка **tool-error** ⇒ `_handle_tool_error` (`staging_runner.py:415`): инфра-DEFER ×2 → бюджет исчерпан → `write_staging_log(..., "FAILED", tool_error=True)` + `_advance(...)` ⇒ существующий откат `deploy-staging → development`. ### Корневая классификационная ошибка Двухуровневый исход ORCH-115 (D5) различает только **«сюита исполнилась» vs «сюита не исполнилась (транзиентная инфра)»**. Отсутствие docker CLI — это **детерминированный, постоянный дефект окружения**, а не транзиентная икота: ретраи бессмысленны (каждая попытка падает идентично), а **терминальный исход — откат на `development` с расходом developer-retry** — вводит в заблуждение (винит код/разработчика задачи за environment-проблему раннера). Так до фикса **любая** self-hosting задача, доходящая до `deploy-staging`, обречена на ложный откат. ### Локализация (анализ — куда смотреть архитектору/разработчику) **Установленный факт о топологии исполнения (CLAUDE.md / `docs/operations/INFRA.md` / код):** прод-деплой (ORCH-036, `src/self_deploy.py`) **уже решил ровно эту проблему** и НЕ запускает docker изнутри контейнера. Он исполняет хост-хук **на хосте** через `ssh + setsid` (detached): `build_deploy_command` (`self_deploy.py:220`) формирует `ssh @ 'setsid bash -c "… bash --deploy …"'`, где `deploy_ssh_host=127.0.0.1`, ssh-ключ смонтирован, `openssh-client` стоит в образе (`Dockerfile:11`). Хост-хук `scripts/orchestrator-deploy-hook.sh` уже выполняет `docker compose …` / `docker tag` / **`docker exec "$STAGING_CONTAINER" python3 staging_check.py`** (`--build-staging`, строки 197/261) — **на хосте**, где docker CLI есть. **Вывод:** ORCH-115 при замене LLM-деплойера детерминированным раннером **отклонился** от уже установленного паттерна «docker-операции исполняются host-side, не внутри app-контейнера». Дефект — в **стратегии исполнения** `staging_runner` (где/как запускается staging-сюита), а не в гейте `check_staging_status` и не в контракте `15-staging-log.md`. Поэтому фикс должен **восстановить работоспособную стратегию исполнения** staging-сюиты в проде, не завися от недоступного внутри контейнера docker CLI, и **перестать классифицировать постоянный environment-дефект как код-фейл**. > 🔎 **Точка для проверки на стадии architecture (не факт, требует верификации):** как > staging-сюита исполнялась **до** ORCH-115 LLM-деплойером — действительно через `docker exec` из > контейнера (тогда путь всегда был сломан и LLM-гейт был «бумажным»), или иным механизмом. Это > определяет, «сломал ли ORCH-115 рабочий путь» или «сделал давний дефект детерминированным и > видимым». На фикс выбор стратегии это не меняет, но влияет на формулировку регресса. ## 2. Объём (scope) ### В объёме - Исправить **стратегию исполнения staging-runner** так, чтобы `deploy-staging` для self-hosting `orchestrator` **проходил в проде**, не завися от недоступного внутри прод-контейнера docker CLI. - Гарантировать, что **tool-error / environment-дефект** (в частности отсутствие исполняемого `docker`/невозможность запустить сюиту по причине окружения) **НЕ приводит к вводящему в заблуждение откату `deploy-staging → development`** как код-фейлу и **не жжёт** developer-retry; постоянный environment-дефект должен быть отличим от транзиентной инфры и от настоящего код-фейла. - Добавить **prod-like регресс/preflight**, который ловит «нет исполняемого/стратегия неработоспособна» **до раската** (а не постфактум, ложным откатом реальной задачи). - Задокументировать **границу исполнения** (`docs/operations/INFRA.md` + `docs/architecture/README.md`): где и как staging-сюита исполняется относительно прод-контейнера, какие исполняемые/сокеты доступны. ### Вне объёма - ❌ Изменение гейта `check_staging_status` / `_parse_staging_status`, контракта `15-staging-log.md` (`staging_status:`), `STAGE_TRANSITIONS`, machine-verdict ключей, схемы БД — **байт-в-байт прежние** (ORCH-115 NFR-1: меняется **продюсер/стратегия исполнения** артефакта, не гейт). - ❌ Изменение содержимого/логики самой `scripts/staging_check.py` (тулинг сюиты корректен; меняется лишь **как/откуда** её запускают). - ❌ Откат ORCH-115 (детерминизация staging корректна по замыслу; чиним способ исполнения). - ❌ Выбор конкретного механизма (ssh host-side / Docker SDK поверх сокета / docker CLI в образе / выделенный hook-режим) — это **зона архитектора** (`06-adr/`), здесь — только требования и ограничения. - ❌ Изменение прод-деплой-пути (ORCH-036 self_deploy) сверх возможного переиспользования его ssh-механизма; happy-path прод-деплоя не трогается. ## 3. Заинтересованные стороны - **Заказчик/оператор (Слава)** — страдает от ложных откатов и ручного разруливания залипших задач; принимает результат. - **Self-hosting конвейер `orchestrator`** — прямой потребитель: без фикса **ни одна** self-hosting задача не проходит `deploy-staging`. - **Все проекты на общем инстансе (enduro-trails)** — косвенно: застрявшие/откатываемые self-hosting задачи занимают слоты и внимание оператора на общей очереди. ## 4. Бизнес-требования (BR) - **BR-1** — Для self-hosting `orchestrator` стадия `deploy-staging` (детерминированный staging-runner) **исполняет staging-сюиту и проходит в проде** без зависимости от docker CLI, отсутствующего внутри прод-контейнера. Задача уровня ORCH-116 (reviewer/tester зелёные, код корректен) **доходит до `deploy`**, а не откатывается. - **BR-2** — **Tool-error / environment-дефект ≠ код-фейл.** Невозможность исполнить сюиту по причине окружения (нет исполняемого, недоступна стратегия) **не должна** завершаться откатом `deploy-staging → development` как кодовой ошибкой и **не должна** инкрементировать developer-retry; такой исход должен быть **отдельным, отличимым** (хольд/алерт оператору об инфра/environment-сбое). - **BR-3** — **Настоящий код-фейл сохраняется (анти-over-tolerance).** Если сюита **реально исполнилась** и упала (exit≠0), поведение — **прежний** откат `deploy-staging → development` + developer-retry (BR-2 не должно маскировать настоящие провалы; ср. ORCH-110 BR-6). - **BR-4** — **Prod-like preflight/регресс.** Должен существовать механизм, ловящий «исполняемое отсутствует / стратегия неработоспособна» **до раската** (preflight на старте/в smoke-проверке) и **тест**, воспроизводящий «docker CLI отсутствует в контейнере» (красный до фикса, зелёный после). - **BR-5** — **Граница исполнения задокументирована.** `docs/operations/INFRA.md` (и `docs/architecture/README.md`) явно описывают, где/как исполняется staging-сюита относительно прод-контейнера, какие исполняемые/сокеты доступны, и почему docker-операции идут так, а не «изнутри app-контейнера». - **BR-6** — **Наблюдаемость.** Различие «environment/tool-error» vs «код-фейл» видно в логе (структурная запись), Telegram-алерте (кликабельный номер) и read-only блоке `staging_runner` в `GET /queue`. ## 5. Нефункциональные требования (NFR) - **NFR-1 (нулевая регрессия конвейера)** — `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / имена и семантика `check_*` (в т.ч. `check_staging_status`/`_parse_staging_status`) / machine-verdict ключи (`staging_status:`/`deploy_status:`/…) / схема БД — **байт-в-байт не тронуты** (фикс — стратегия исполнения продюсера, не гейт и не стадия). - **NFR-2 (self-hosting safety)** — путь исполнения staging-runner **никогда**: не рестартит прод `orchestrator` (8500), не делает `docker compose up orchestrator`/`--build` прода, не force-push, не пишет в `main`, не редактирует `.env`. Только запускает staging-сюиту (8501) и пишет лог (инвариант ORCH-115 BR-7/AC-8 сохраняется). - **NFR-3 (security — если выбран socket/CLI-в-контейнере)** — любой вариант с прямым использованием `docker.sock`/CLI из контейнера = root-эквивалент на хосте → **обязателен явный security-review** в ADR (поверхность атаки, ограничение прав, :ro где возможно). Вариант host-side ssh должен переиспользовать уже существующий доверенный механизм (ORCH-036) без расширения привилегий. - **NFR-4 (обратимость / kill-switch)** — поведение под существующим `staging_runner_enabled` (+ при необходимости — новым флагом выбора стратегии); выключенный kill-switch → прежний LLM-деплойер через `_spawn` **байт-в-байт** (ORCH-115 fail-safe сохраняется). - **NFR-5 (надёжность)** — never-raise / fail-safe / restart-safe (по образцу leaf'ов `staging_runner`/`self_deploy`/`proc_group`); очередь репо **никогда не клинится**; тайм-аут сюиты не растёт сверх бюджета, держащего сквозной инвариант reaper (ORCH-065/109/110). - **NFR-6 (область)** — изменение скоупится на self-hosting (`orchestrator`, единственный с staging 8501); поведение для прочих репо/синхронного LLM-деплоя — не ухудшается (`applies(repo)` первым). ## 6. Допущения и ограничения - Прод и staging контейнеры запущены на **одном хосте**; `/var/run/docker.sock` доступен на хосте, где docker CLI установлен; ssh на `127.0.0.1` под смонтированным ключом — рабочий канал (его уже использует ORCH-036 self-deploy). - `staging_check.py` исполняется **внутри контейнера `orchestrator-staging`** (там есть python3 и приложение 8501) — это контракт сюиты; меняется только то, **кто инициирует** `docker exec` (хост vs прод-контейнер) или **как** (CLI vs SDK поверх сокета). - Источник истины «применять ли детерминированный runner» — `staging_runner.applies(repo)` (ORCH-115); фикс не меняет его семантику. - Конкретный механизм исполнения (host-side ssh / Docker SDK поверх сокета / docker CLI в образе / выделенный режим хука) — **открытый вопрос для архитектуры**, решается в `06-adr/` с security-review. ## 7. Критерии успеха Self-hosting задача уровня ORCH-116 проходит `deploy-staging` в проде (staging-сюита реально исполняется, вердикт маппится из exit-кода); environment/tool-error **не** даёт ложного код-фейл-отката и не жжёт developer-retry; настоящий код-фейл по-прежнему откатывает на `development`; существует prod-like preflight + обязательный регресс-тест «нет docker CLI в контейнере» (**красный до фикса, зелёный после**); граница исполнения задокументирована; гейт/контракт/`STAGE_TRANSITIONS`/схема БД — без регресса; полный `pytest tests/ -q` зелёный. Детальные PASS/FAIL — `03-acceptance-criteria.md`. ## 8. Риски - **Security (socket/CLI в контейнере):** прямой доступ к `docker.sock` из app-контейнера = эскалация до root на хосте → ADR обязан взвесить поверхность атаки против host-side ssh-варианта. - **Over-tolerance:** слишком широкая трактовка «environment-дефекта» может замаскировать настоящие код-фейлы/реальные сбои staging-стенда (митигация — BR-3; ср. инцидент ORCH-110). - **Кросс-каттинг:** ORCH-115 (staging-runner, прямой объект), ORCH-036 (self_deploy ssh-механизм — потенциально переиспользуемый), ORCH-110 (proc_group tree-kill + infra-tolerance паттерн), ORCH-058 (`--build-staging` host-side docker exec — прецедент), ORCH-101 (host-параметризация). Правки маркированных блоков сверять с их `06-adr/` (CLAUDE.md §9). Детали/митигации — `10-tech-risks.md` (заполняет архитектор).