---
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 <user>@<host> 'setsid bash -c "… bash
<hook> --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`
  (заполняет архитектор).
