--- work_item: ORCH-123 stage: architecture author_agent: architect status: proposed created_at: 2026-06-16 model_used: claude-opus-4-8 --- # adr-0049: Граница исполнения docker — все docker-операции host-side, не изнутри app-контейнера > **Сквозной (cross-cutting) ADR.** Кодифицирует инвариант **«docker-операции оркестратора > исполняются host-side через доверенный ssh-канал, никогда изнутри прод-контейнера»**, охватывающий > компоненты ORCH-036/058/115/123/101, и **амендит** execution-strategy-решение > [adr-0048](adr-0048-deterministic-staging-runner.md) (D3/D5). Поводом стала задача ORCH-123 (баг: > staging-runner отклонился от инварианта). Локальная детализация (D1–D9) — > `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`. ## Статус Proposed ## Контекст Прод-контейнер `orchestrator` (8500) **не содержит docker CLI** (`Dockerfile:11`: `openssh-client git curl ca-certificates` + pinned gitleaks; `python:3.12-slim` docker не несёт). `/var/run/docker.sock` смонтирован rw + `group_add 999` (ORCH-040 «МИНА 1»), но **клиента, который бы им воспользовался, нет** — сознательно: добавление CLI/SDK активировало бы root-эквивалентный путь исполнения для всего, что бежит в контейнере (вкл. LLM-агентов). Поэтому в оркестраторе сложился **инвариант исполнения**, ранее не выделенный в отдельный ADR: - **ORCH-036** (`self_deploy.build_deploy_command`, [adr-0007](adr-0007-executable-self-deploy.md)) — прод-деплой исполняется host-side через `ssh + setsid bash --deploy` на `127.0.0.1`. - **ORCH-058** (`image_freshness`, [adr-0008](adr-0008-staging-image-provenance.md)) — ребилд staging-образа (`ssh … bash --build-staging`) и инспекция revision (`image_revision(ssh_target=…)`) — host-side; модуль прямо документирует: *«docker lives on the HOST (the container ships only openssh-client git)»*. - **ORCH-101** ([adr-0036](adr-0036-replication-foundation-host-parametrization.md)) — host-параметры канала (`deploy_ssh_*`, `deploy_host_repo_path`, `repos_dir`/`host_repos_dir`) расхардкожены. **ORCH-115** ([adr-0048](adr-0048-deterministic-staging-runner.md)), заменяя LLM-деплойера детерминированным `staging_runner`, **отклонился** от инварианта: зашил `docker exec` **изнутри** прод-контейнера через `proc_group → Popen` → `FileNotFoundError: docker` → постоянный environment-дефект, ложно маршрутизированный как транзиентная инфра → DEFER → fail-closed FAILED → **откат `deploy-staging → development`** (винит код задачи за дефект окружения раннера). Инцидент ORCH-116/ORCH-123. ## Решение **Кодифицировать инвариант (нормативно):** docker-операции оркестратора (`docker`/`docker compose`/ `docker exec`/`docker inspect`/`docker tag`) исполняются **host-side** через доверенный ssh-канал (`deploy_ssh_host=127.0.0.1`, ключ смонтирован, `openssh-client` в образе) — **никогда** изнутри прод-контейнера, который docker CLI не несёт. `/var/run/docker.sock` **не используется** изнутри контейнера; docker CLI/SDK в образ **не добавляется** (любое исключение — отдельный явный security-review: socket-из-контейнера = root-эквивалент на хосте, обслуживающем все проекты). **ORCH-123 приводит `staging_runner` в соответствие** (амендит adr-0048 D3/D5): - **D3 (амендмент adr-0048):** `staging_runner.build_staging_command` теперь обёртывает `docker exec orchestrator-staging python3 staging_check.py …` в `ssh @ '<…>'` (зеркало `image_freshness.image_revision(ssh_target=…)`). Внутренняя команда сюиты и exit-код-контракт — те же; меняется лишь **инициатор/канал**. - **D5 (амендмент adr-0048 двухуровневого исхода):** введён **третий** класс исхода `permanent-env` (зеркало `merge_gate.classify_retest_failure`, ORCH-110); корневой инвариант — **«сюита не исполнилась» (environment ИЛИ транзиентная инфра) НИКОГДА не оканчивается код-фейл-откатом и не жжёт developer-retry**; откат — только для реально исполнившейся сюиты с `exit≠0`. Терминал исчерпания DEFER изменён с fail-closed-FAILED+advance на **infra-HOLD + alert** (как ORCH-110 D3). Кросс-каттинговые инварианты (сохранены **байт-в-байт**, как adr-0048): - `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_staging_status`/`_parse_staging_status` / machine-verdict-ключи (`staging_status:`/`deploy_status:`/…) / **схема БД** — не тронуты (замена *стратегии исполнения продюсера*, не гейта/стадии). - Единственный транспорт LLM-консультации (`launcher._spawn`/S0, [adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)) — соблюдён (раннер LLM не зовёт). - Сквозной бюджет времени ORCH-065/109/110 (`reaper_max_running_s` > Σ(работ на ребре) + grace) — не растёт (host-side ssh заменяет in-container call, окно ≤ `staging_runner_timeout_s`). - Граница transition-lease ORCH-114 — берётся внутри `advance_stage`; раннер не трогает. Скоуп — **self-hosting only** (`staging_runner_repos=""` → `is_self_hosting_repo`); под флагами `staging_runner_enabled` (→ LLM-путь) и **новым** `staging_runner_exec_host_side` (дефолт `True` → фикс; `False` → прежний in-container call). never-raise во всех публичных функциях. ## Последствия - **+** Инвариант «docker host-side» выделен и задокументирован → будущие компоненты не повторят отклонение ORCH-115; reviewer ловит in-container docker как регресс инварианта. - **+** staging-сюита реально исполняется в проде; инфра/environment ≠ код-фейл на staging-ребре (закрыт RCA-класс ORCH-110 на этом ребре полностью); анти-over-tolerance цел. - **+** Без расширения привилегий (нет docker CLI/SDK в контейнере, сокет не используется); согласовано с ORCH-036/058. - **−** Remote tree-kill ограничен локальным ssh-клиентом (как `image_freshness.rebuild_staging_image`); backstop — bounded таймаут внутри `staging_check.py`. - **−** Permanent-env/исчерпавшая-DEFER задача держится на `deploy-staging` (блокирует serial-gate репо до починки оператором) — принятый tradeoff (зеркало ORCH-110), self-hosting only. - **Откат:** `ORCH_STAGING_RUNNER_ENABLED=false` (→ LLM) или `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false` (→ in-container call). ## Ссылки - Локальный ADR: `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md` - Амендит: [adr-0048](adr-0048-deterministic-staging-runner.md) (D3/D5 ORCH-115) - Опирается на: [adr-0007](adr-0007-executable-self-deploy.md) (ORCH-036 self-deploy ssh), [adr-0008](adr-0008-staging-image-provenance.md) (ORCH-058 image-freshness host-side docker), [adr-0042](adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md) (ORCH-110 proc_group + classify + infra-tolerance), [adr-0036](adr-0036-replication-foundation-host-parametrization.md) (ORCH-101 host-параметризация) - Сверено по коду: `src/staging_runner.py`, `src/self_deploy.py:220`, `src/image_freshness.py:185/246`, `scripts/orchestrator-deploy-hook.sh:166/197`, `Dockerfile:11`, `docker-compose.yml`