fix(staging): host-side ssh execution + env classification for staging-runner (ORCH-123)
All checks were successful
CI / test (push) Successful in 1m8s
CI / test (pull_request) Successful in 1m8s

The ORCH-115 deterministic staging-runner ran `docker exec` FROM INSIDE the prod
`orchestrator` container, which ships only `openssh-client git curl` — no `docker`
CLI (Dockerfile:11). `Popen(["docker", ...])` hit FileNotFoundError -> a PERMANENT
environment defect that was mis-routed as a code-fail rollback
`deploy-staging -> development` (burning developer-retries). Incident ORCH-116:
every self-hosting task reaching deploy-staging was doomed to a false rollback.

Fix (adr-0049, additive, flag-gated, never-raise, self-hosting scope; the gate /
artifact contract / STAGE_TRANSITIONS / DB schema are byte-for-byte unchanged):

- D1: build_staging_command() wraps the SAME `docker exec ... staging_check.py
  ... --mode stub` in `ssh <user@host> '<...>'` so it runs HOST-SIDE over the
  existing trusted ssh channel (mirror self_deploy / image_freshness). New flag
  staging_runner_exec_host_side (default True). No docker CLI/SDK added to the
  image, docker.sock not used in-container (D2 security).
- D3: three-way classify_staging_outcome (suite-ran / permanent-env /
  transient-infra), disambiguating the exit=1 collision by scanning stderr.
- D4: invariant "infra != code-fail" — permanent-env / exhausted transient-infra
  end in an infra-HOLD (no rollback, no developer-retry), NOT a false FAILED
  rollback (supersedes ORCH-115 D5). A really-executed failing suite still rolls
  back (anti-over-tolerance). R-2 verified: a held deploy-staging task is not
  rolled back by the reconciler.
- D5: prod-like preflight() of the host-side channel at startup (main.lifespan,
  best-effort, never blocks).
- D8: snapshot adds permanent_env / exec_host_side / preflight.

Docs (golden source, same PR): INFRA.md execution-boundary section,
architecture/README.md, CLAUDE.md, CHANGELOG.md, .env.example.

Tests: tests/test_orch123_staging_runner_exec.py (TC-01 mandatory regression
red->green; TC-02..TC-14 + R-2). ORCH-115 anti-drift green (3 tests updated for
the D1/D4/D8 supersession). Full suite: 2131 passed.

Refs: ORCH-123

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 08:42:36 +03:00
parent e1872e3d94
commit cc41dd849c
9 changed files with 917 additions and 52 deletions

File diff suppressed because one or more lines are too long

View File

@@ -244,10 +244,39 @@ watchdog'а: **watchdog сигналит, pruner убирает**.
- **Свежесть staging-образа (ORCH-058):** на ребре `deploy-staging → deploy` (ПОСЛЕ merge-gate, ДО Phase A) QG-под-чек `check_staging_image_fresh` пересобирает staging-образ из валидированного коммита и пересоздаёт 8501 (Strategy A), а хук перед build-once retag fail-closed сверяет OCI-лейбл `revision` с `EXPECTED_REVISION` (Strategy B). Гарантирует: в прод промоутится РОВНО провалидированный артефакт (инцидент LESSONS_ORCH-036 п.4 — тихий промоут устаревшего образа). Сборки/recreate — ТОЛЬКО staging (8501); FAIL → откат на `development`. Условный: реален только для self-hosting.
- **Гигиена shared deploy-базы (ORCH-112):** self-deploy `git pull origin main` устойчив к грязному рабочему дереву deploy-базы (модифицированные tracked + untracked-остатки failed/cancelled/брошенных задач). Хук `--deploy` перед pull приводит базу к чистому `origin/main` (resilient-pull: `git fetch` + `git reset --hard origin/main` + `git clean -fd`), **строго сохраняя** rollback-снимки `.deploy-prev-image-*`, `deploy-hook.log`, gitignored `.env`/`data/`/`*.db` (НИКОГДА `-x`!), sibling `.deploy-state-*`/`.merge-lease-*.json`, `.git/worktrees/*`. Гейт — kill-switch `ORCH_CHECKOUT_HYGIENE_ENABLED` (дефолт `True`; off → голый pull 1:1); скоуп `ORCH_CHECKOUT_HYGIENE_REPOS` (пусто → self-hosting only). Грязь базы детектируется → лог + Telegram-алерт (Phase-C finalizer). Решает инцидент ORCH-111 (грязь ORCH-104 заблокировала `git pull`). Детально — `docs/work-items/ORCH-112/06-adr/ADR-001`, сквозной adr-0044.
### Граница исполнения docker-операций — host-side, НЕ изнутри прод-контейнера (ORCH-123)
**Инвариант (нормативно, adr-0049):** прод-контейнер `orchestrator` несёт **только**
`openssh-client git curl` (+ pinned gitleaks) — **бинаря `docker` в образе НЕТ** (`Dockerfile:11`,
`python:3.12-slim` его не несёт). `/var/run/docker.sock` **смонтирован** (`docker-compose.yml`,
rw + `group_add 999`, «МИНА 1» ORCH-040), но **сознательно НЕ используется изнутри** контейнера: нет
docker-клиента, и добавлять его (CLI/SDK) **нельзя** — это активировало бы дремлющий
root-эквивалентный путь для всего, что бежит в контейнере, включая LLM-агентов (security-разбор —
ADR-001 D2 / adr-0049 / R-1).
Поэтому **все** docker-операции self-hosting исполняются **host-side** через **ssh на `127.0.0.1`**
(`ORCH_DEPLOY_SSH_HOST`/`ORCH_DEPLOY_SSH_USER`, ssh-ключ смонтирован `:ro`), где docker CLI есть:
- **прод-деплой** (ORCH-036, `self_deploy.build_deploy_command`) — `ssh … setsid bash hook --deploy`;
- **image-freshness** (ORCH-058, `image_freshness.image_revision`/`rebuild_staging_image`) — `ssh … docker …`;
- **staging-runner** (ORCH-123, `staging_runner.build_staging_command`) — `ssh … docker exec orchestrator-staging python3 … staging_check.py … --mode stub`.
Сама staging-сюита (`scripts/staging_check.py`) **по-прежнему** исполняется **внутри** контейнера
`orchestrator-staging` (8501) — меняется лишь **кто инициирует** `docker exec` (хост через ssh, а не
прод-контейнер). До ORCH-123 staging-runner (ORCH-115) вызывал `docker exec` **изнутри**
прод-контейнера → `FileNotFoundError` (нет `docker`) → постоянный environment-дефект ложно
маршрутизировался как код-фейл-откат `deploy-staging → development` (инцидент ORCH-116). Фикс:
host-side ssh-обёртка (флаг `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=true`, дефолт) + трёхсторонняя
классификация (suite-ran / permanent-env / transient-infra), где environment/инфра **никогда** не
оканчивается код-фейл-откатом (infra-HOLD + алерт «инфра, НЕ дефект кода»), и prod-like preflight
канала на старте сервиса. Откат — `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=false` (прежний in-container
вызов — валиден лишь там, где docker CLI запечён в образ) или `ORCH_STAGING_RUNNER_ENABLED=false`
(LLM-deployer 1:1). Детали — `docs/work-items/ORCH-123/06-adr/ADR-001`, сквозной adr-0049.
**Правила для агентов при задачах ORCH:**
1. НЕ перезапускать / не ронять прод-контейнер `orchestrator` в рамках задачи.
2. Все проверки деплоя — на staging (8501), боевой 8500 не трогать.
3. Деплой self — только через хук с health-check + авто-rollback (`DEPLOY_HOOK.md`).
4. docker-операции исполняются **host-side через ssh** — в контейнере `docker` CLI нет (ORCH-123).
## Эксплуатация (быстрые команды)
```bash