# INFRA.md — инфраструктура и эксплуатация оркестратора > RUNBOOK. Топология, контейнеры, порты, переменные окружения, границы. > **Секреты тут НЕ хранятся** — только дескрипторы. Реальные значения — в `.env` на хосте. ## Топология ``` host: mva154 (slin@82.22.50.71), network_mode: host ┌──────────────────────────────────────────────────────────────────────┐ │ orchestrator (PROD) :8500 env_file .env │ │ БД: ./data/orchestrator.db (обслуживает ВСЕ прод-проекты) │ │ │ │ orchestrator-staging (STAGING) :8501 env_file .env.staging │ │ БД: ./data/staging/orchestrator.db (изолирована, только sandbox) │ │ profile: staging — НЕ стартует обычным `docker compose up` │ └──────────────────────────────────────────────────────────────────────┘ │ webhooks │ git ▼ ▼ Plane (ag_proj) Gitea (localhost:3000) /repos/ ← общий каталог репозиториев (host: /home/slin/repos) ``` ## Контейнеры | Контейнер | Роль | Порт | env_file | БД (хост) | Старт | |-----------|------|------|----------|-----------|-------| | `orchestrator` | прод | 8500 | `.env` | `./data/orchestrator.db` | `docker compose up -d` | | `orchestrator-staging` | staging / песочница | 8501 | `.env.staging` | `./data/staging/orchestrator.db` | `docker compose --profile staging up -d orchestrator-staging` | Оба: `network_mode: host`, `init: true` (tini как PID 1 — reaping зомби, B-2), `restart: unless-stopped`. ### Рантайм-uid (ORCH-040) Оба сервиса бегут под `user: "1000:1000"` (slin), **не** root. Артефакты конвейера (git worktree `/repos/_wt/...`, коммиты в `docs/work-items/...`) создаются как `slin:slin`, поэтому `git pull` / `git reset` на хосте под slin работают без ручного `chown`. Доступ к docker.sock сохранён через `group_add: ["999"]` (gid docker, **не** через root — НЕ удалять). При переносе на другой хост uid пересматривается. См. ADR `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md` и глобальный `docs/architecture/adr/adr-0005-container-runs-as-host-uid.md`. **Host-prerequisites (обязательная процедура Owner, в git не коммитятся):** - **P-1 (блокер):** uid 1000 читает claude creds — `chown -R 1000:1000 /home/slin/.claude`; проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json`. Без этого preflight (ORCH-044) заворачивает весь конвейер. - **P-2:** ssh-ключи в `/home/slin/.orchestrator-ssh` читаемы uid 1000 (маунт ведёт в `/home/slin/.ssh`). - **P-3:** `id slin` → `1000:1000`; `/repos`, `/app/data` уже `1000:1000`. - **P-4:** прод-рестарт self — только в окно тишины (`GET /status` без активных задач): общий инстанс с enduro-trails. - **P-5 (блокер миграции uid, ORCH-057):** нормализация **всех** legacy `root:root` файлов в `/repos` — см. подраздел «Миграция uid: обязательная нормализация legacy root-файлов» ниже. Без неё первый job падает на launch при создании worktree (инцидент 06.06, ORCH-043). ### Миграция uid: обязательная нормализация legacy root-файлов (ORCH-057) ORCH-040 сменил `user:` контейнера, но **не** владельца уже существующих файлов в bind-mount `/repos`, созданных прежним root-контейнером. Под uid 1000 `src/git_worktree.py::ensure_worktree` не может создать worktree рядом с `root:root` каталогом `/repos/_wt/` → `fatal: could not create leading directories … Permission denied` (агент даже не стартует). С ORCH-057 эта ошибка распознаётся и выдаётся **внятно** (с лечащей командой) + детектится на старте сервиса (WARNING/Telegram, блок `fs_ownership` в `GET /queue`), но **фактический `chown` обязан выполнить оператор под root на хосте** (контейнер бежит без root и chown'ить чужие файлы не может). **Обязательный разовый шаг при миграции uid / на новой среде (под root на mva154, ПЕРЕД стартом app):** ```bash # 1) worktree-корень (все ветки всех проектов режутся здесь) sudo chown -R 1000:1000 /home/slin/repos/_wt # 2) .git обоих репо (objects / worktrees-административные записи) sudo chown -R 1000:1000 /home/slin/repos/orchestrator/.git \ /home/slin/repos/enduro-trails/.git # 3) корень orchestrator целиком (включая data/runs/*.log — 37 root-логов в инциденте) sudo chown -R 1000:1000 /home/slin/repos/orchestrator # Проверка (пусто = ок): find /home/slin/repos/_wt ! -uid 1000 -print -quit ``` Процедура **идемпотентна** (повтор на корректной среде — no-op) и входит в **чеклист деплоя/миграции self**. Область охвата: `_wt`, оба `.git` (`objects`+`worktrees`), `data/runs`. См. `docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md` и сквозной `docs/architecture/adr/adr-0031-legacy-ownership-normalization.md`. ### Тома (volumes) - `./data` → `/app/data` (БД; у staging — `./data/staging`) - `/home/slin/repos` → `/repos` (рабочие репозитории проектов) - `/var/run/docker.sock` (для docker-операций деплоя) - claude-code, node, `~/.claude*` (CLI агентов, ro) - `~/.orchestrator-ssh` → `/home/slin/.ssh` (ro, деплой по ssh; target в HOME агента, согласован с `HOME=/home/slin` из launcher — ORCH-040, ранее `/root/.ssh`) ### Disk-watchdog: мониторинг заполнения диска mva154 (ORCH-063) 07.06.2026 диск хоста mva154 тихо дорос до 100% и положил **весь конвейер всех проектов** (один прод-инстанс `orchestrator` на общей БД/очереди). Чтобы такой инцидент сигнализировался **заранее**, работает фоновый daemon-поток `src/disk_watchdog.py` (каркас `reconciler`/`job_reaper`): - **Что мониторится:** заполнение **хост-разделов** по смонтированным bind-путям (`/repos` → host `/home/slin/repos`, `/app/data` → host `./data`) через stdlib `shutil.disk_usage` — НЕ overlay `/` контейнера (иначе замер ложно-низкий). Пути с одним физическим устройством (`st_dev`) дедуплицируются → один алерт, не два. - **Порог и период:** при заполнении **≥ 85%** (`ORCH_DISK_MONITOR_THRESHOLD_PCT`) шлётся Telegram-алерт оператору; замер — раз в 300с (`ORCH_DISK_MONITOR_INTERVAL_S`). Пока диск выше порога, повтор — не чаще раза в ~6ч (`ORCH_DISK_MONITOR_REALERT_S`, анти-спам). При возврате ниже порога — однократное recovery-сообщение. - **Как отключить:** `ORCH_DISK_MONITOR_ENABLED=false` (демон не стартует; `GET /queue` → `disk_monitor.enabled=false`; поведение 1:1 как сейчас). Наблюдаемость — блок `disk_monitor` в `GET /queue` (последний замер: `used_pct`/`free_gb`/`alerting`/`last_alert_at` по каждому пути). - **Что делать при алерте:** watchdog **только сигнализирует** — он не трогает диск/контейнер и не рестартит прод (self-hosting безопасность). Освобождение **docker build cache** автоматизировано отдельным демоном (ORCH-062, см. ниже); прочие «пожиратели» — старые worktree-каталоги `/home/slin/repos/_wt/*` завершённых задач, логи, dangling-образы (`docker image prune`) — по-прежнему **ручная** операция оператора (авто-уборка этих категорий — вне объёма ORCH-062/063). ### Build-cache-pruner: авто-prune docker build cache на mva154 (ORCH-062) Доминирующий «пожиратель» в инциденте 07.06.2026 — **docker build cache** (≈11 ГБ от частых пересборок прод/staging-образов). Чтобы он не мог снова заполнить диск **без оператора**, работает фоновый daemon-поток `src/build_cache_pruner.py` (каркас `disk_watchdog`) — «вторая половина» watchdog'а: **watchdog сигналит, pruner убирает**. - **Что делает:** каждые `ORCH_BUILD_CACHE_PRUNE_INTERVAL_S` (дефолт 21600с = 6ч) выполняет **строго `docker builder prune -f --filter until=`** (BuildKit GC; дефолт `until=24h` — удаляется build cache старше суток, тёплый свежий кэш сохраняется). Команда затрагивает **только build cache** — НЕ образы/контейнеры запущенных сервисов; рестарт docker daemon/прода НЕ выполняется (self-hosting безопасность). - **Как исполняется:** в контейнере нет `docker` CLI (образ несёт только `openssh-client git`), поэтому уборка идёт **на хосте через ssh** тем же каналом `ORCH_DEPLOY_SSH_USER@_HOST`, что деплой/`image_freshness`. **Пустой `ORCH_DEPLOY_SSH_HOST` → тик no-op** (фича активна только на self-host, где ssh настроен). - **Как отключить:** `ORCH_BUILD_CACHE_PRUNE_ENABLED=false` (демон не стартует; поведение 1:1 как до ORCH-062). Наблюдаемость — блок `build_cache_prune` в `GET /queue` (`enabled`/`interval_s`/ `until`/`last_run_ts`/`last_reclaimed`/`last_error`); never-raise; in-memory учёт (без миграции). - **Ручной fallback** (если ssh-канал недоступен) — host-cron на mva154: `0 */6 * * * docker builder prune -f --filter until=24h` (off-git, процедура Owner). ## Переменные окружения (карта; значения — в `.env`) | Переменная | Назначение | |-----------|-----------| | `ORCH_PLANE_API_URL` / `_TOKEN` / `_WORKSPACE_SLUG` | доступ к Plane API | | `ORCH_PLANE_WEB_URL` | внешний (браузерный) web-URL Plane для кликабельных ссылок на issue в уведомлениях (ORCH-017); пусто → фолбэк на `ORCH_PLANE_API_URL`, loopback-фолбэк → ссылка опускается | | `ORCH_PLANE_WEBHOOK_SECRET` | HMAC-проверка вебхуков Plane | | `ORCH_GITEA_URL` / `_TOKEN` / `_WEBHOOK_SECRET` | доступ к Gitea + HMAC | | `ORCH_CLAUDE_BIN` | путь к claude CLI | | `ORCH_REPOS_DIR` / `ORCH_HOST_REPOS_DIR` | каталог репозиториев (в контейнере / на хосте) | | `ORCH_DB_PATH` | путь к SQLite БД | | `ORCH_PROJECTS_JSON` | реестр проектов (Plane id → repo + prefix); пусто → дефолт из `src/projects.py` | | `ORCH_AGENT_MODEL_DEFAULT` | LLM-модель агентов по умолчанию (ORCH-41); дефолт `claude-opus-4-8` | | `ORCH_AGENT_MODEL_` | per-agent модель (ANALYST/ARCHITECT/DEVELOPER/REVIEWER/TESTER/DEPLOYER); пусто → default | | `ORCH_AGENT_EFFORT_DEFAULT` | режим работы `--effort` по умолчанию (ORCH-41): low\|medium\|high\|xhigh\|max; дефолт `high` | | `ORCH_AGENT_EFFORT_` | per-agent effort; дефолт: думающие → high, tester/deployer → medium | | `ORCH_AGENT_FALLBACK_MODEL` | опц. фолбэк-модель при overloaded (`--fallback-model`); пусто → без флага | | `ORCH_SELF_DEPLOY_ENABLED` | ORCH-036 kill-switch исполняемого самодеплоя (true); false → legacy-путь для всех | | `ORCH_SELF_DEPLOY_REPOS` | CSV репозиториев с реальным самодеплоем; пусто → только self-hosting `orchestrator` | | `ORCH_DEPLOY_REQUIRE_MANUAL_APPROVE` | требовать человеческий Plane «Approved» для прод-деплоя (true, безопасно) | | `ORCH_DEPLOY_FINALIZE_DELAY_S` / `_MAX_ATTEMPTS` | задержка и бюджет defer'ов finalizer'а (Фаза C; 90 / 10) | | `ORCH_DEPLOY_SSH_USER` / `_SSH_HOST` | куда запускается detached хост-деплой (Фаза B, `ssh user@host`) | | `ORCH_DEPLOY_HOOK_SCRIPT` / `_HOST_REPO_PATH` | путь к хук-скрипту (отн. репо) и чекаут orchestrator на хосте | | `ORCH_DEPLOY_PROD_SOURCE_IMAGE` | staging-образ для build-once retag на прод-тег (без rebuild) | | `ORCH_DEPLOY_PROD_TARGET_SERVICE` / `_TARGET_PORT` / `_TARGET_IMAGE` / `_COMPOSE_PROFILE` / `_PREV_IMAGE_FILE` | прод-цель хука + снапшот для авто-rollback | | `ORCH_IMAGE_FRESHNESS_ENABLED` | ORCH-058 единый kill-switch провенанса staging-образа (A+B как целое); дефолт `true`, false → legacy build-once без проверки свежести | | `ORCH_IMAGE_FRESHNESS_REPOS` | CSV репозиториев с реальным гейтом свежести; пусто → только self-hosting `orchestrator` | | `ORCH_RECONCILE_ENABLED` | kill-switch sweeper потерянных webhook (ORCH-053); дефолт `true`. **При инциденте/раскатке** — `false` глушит весь фоновый reconciler | | `ORCH_RECONCILE_PLANE_ENABLED` | отдельный флаг F-2 (опрос Plane API); `false` гасит только plane-ветку, F-1 продолжает работать; дефолт `true` | | `ORCH_RECONCILE_INTERVAL_S` | период фонового прохода reconciler, сек; дефолт `120` | | `ORCH_RECONCILE_GRACE_DEFAULT_S` | порог «застряла» по `tasks.updated_at`, сек; дефолт `600` | | `ORCH_RECONCILE_GRACE_OVERRIDES_JSON` | per-stage пороги, напр. `{"development":300}`; невалидный JSON → дефолт | | `ORCH_RECONCILE_NOTIFY_UNBLOCK` | слать Telegram при разблокировке застрявшей задачи; дефолт `true` | | `ORCH_DISK_MONITOR_ENABLED` | kill-switch disk-watchdog (ORCH-063); дефолт `true`. `false` → демон не стартует, поведение 1:1 как сейчас | | `ORCH_DISK_MONITOR_INTERVAL_S` | период heartbeat-замера заполнения диска, сек; дефолт `300` | | `ORCH_DISK_MONITOR_THRESHOLD_PCT` | порог заполнения для алерта, %; дефолт `85` (валидация 1..100, иначе → дефолт) | | `ORCH_DISK_MONITOR_REALERT_S` | cooldown повторного алерта, пока выше порога, сек; дефолт `21600` (~6 ч) | | `ORCH_DISK_MONITOR_PATHS` | CSV отслеживаемых **хост**-bind-путей; пусто → `/repos,/app/data` | | `ORCH_BUILD_CACHE_PRUNE_ENABLED` | kill-switch build-cache-pruner (ORCH-062); дефолт `true`. `false` → демон не стартует, поведение 1:1 как до задачи | | `ORCH_BUILD_CACHE_PRUNE_INTERVAL_S` | период тика авто-prune, сек; дефолт `21600` (~6 ч); валидация >0, иначе → дефолт | | `ORCH_BUILD_CACHE_PRUNE_UNTIL` | возраст удержания тёплого кэша (`docker builder prune --filter until=`); дефолт `24h`; валидация `^\d+[smhdw]?$`, иначе → `24h` | | `ORCH_BUILD_CACHE_PRUNE_ALL` | добавить `-a` к prune (только в паре с `until`); дефолт `false` | | `ORCH_BUILD_CACHE_PRUNE_TIMEOUT_S` | таймаут ssh-команды prune, сек; дефолт `120` | | `ORCH_BUILD_CACHE_PRUNE_NOTIFY_MIN_GB` | Telegram при освобождении ≥ N ГБ; дефолт `0` (тихо) | | `DEPLOY_SSH_USER` / `_HOST` / `DEPLOY_HOOK_SCRIPT` | параметры деплой-хука | **Секреты — только в `.env` / `.env.staging` на хосте, в гит НЕ коммитятся.** Канон — `.env.example`, `.env.staging.example`. ## Реестр проектов (`src/projects.py`, ORCH-6) Связывает Plane project id → gitea repo + work-item prefix. Источник: `ORCH_PROJECTS_JSON`, fallback — встроенный дефолт. Прод видит: `enduro-trails` (ET), `orchestrator` (ORCH). Staging видит ТОЛЬКО `orchestrator-sandbox` (SANDBOX) — изоляция. ## Модель и effort агентов (`src/config.py` + `src/agents/launcher.py`, ORCH-41) Модель LLM и режим работы (`--effort`) каждого агента **конфигурируемы** — глобально per-agent (env) и per-project (через `ORCH_PROJECTS_JSON`). **Приоритет резолвинга** (`resolve_agent_model` / `resolve_agent_effort`): 1. per-project override — `agent_models` / `agent_efforts` в записи `ORCH_PROJECTS_JSON`; 2. per-agent env — `ORCH_AGENT_MODEL_` / `ORCH_AGENT_EFFORT_` (если непусто); 3. глобальный дефолт — `ORCH_AGENT_MODEL_DEFAULT` (`claude-opus-4-8`) / `ORCH_AGENT_EFFORT_DEFAULT` (`high`); 4. пусто → флаг не передаётся, действует дефолт CLI. **Значения effort:** `low` < `medium` < `high` < `xhigh` < `max` — рычаг «качество vs стоимость/время». Дефолтная раскладка: думающие агенты (analyst/architect/developer/reviewer) → `high`, механические (tester/deployer) → `medium`. Невалидное значение → лог-warning, флаг опускается. **Per-project override в `ORCH_PROJECTS_JSON`** (поля `agent_models` / `agent_efforts` опциональны, старые записи работают): ```json {"plane_project_id":"...","repo":"orchestrator","work_item_prefix":"ORCH", "agent_models":{"developer":"claude-opus-4-8","reviewer":"claude-sonnet-4-6"}, "agent_efforts":{"developer":"xhigh","tester":"low"}} ``` > ⚠️ Бюджет (ORCH-38): `claude-opus-4-8` дефолт в коде; реальное переключение прод-env делается отдельно после согласования. ## ⚠️ Self-hosting — оркестратор дорабатывает САМ СЕБЯ **Факт:** прод-инстанс `orchestrator` (8500) — ОДИН на ВСЕ прод-проекты (enduro-trails + orchestrator), с ОБЩЕЙ БД `./data/orchestrator.db` и общей очередью задач (ORCH-1). **Следствие — групповой риск:** когда орк выполняет задачу из проекта ORCH (дорабатывает себя), он бежит в том же инстансе, что обслуживает enduro-trails. - Рестарт / падение прод-контейнера орк-задачей → конвейер ВСЕХ проектов встаёт. - Кривой self-деплой (ORCH-36, Вариант B) → лежат все проекты сразу. - Общая очередь → орк-задача занимает concurrency-слоты других проектов. **Что изолировано (безопасно):** - Staging (8501) — отдельная БД (`./data/staging`), отдельный реестр (`ORCH_PROJECTS_JSON` = только sandbox). Прод-проекты не видит. - Репозитории разделены, изоляция веток через git worktree (ORCH-2). **Страховки:** - Стадия `deploy-staging` (порт 8501) — обязательный гейт перед прод-деплоем орка. Прод-деплой недостижим, пока staging-гейт не зелёный (см. `STAGING.md`, ORCH-35). Гейт условный: реален только для self-hosting (repo=orchestrator), для остальных проектов — no-op. - **Свежесть 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. **Правила для агентов при задачах ORCH:** 1. НЕ перезапускать / не ронять прод-контейнер `orchestrator` в рамках задачи. 2. Все проверки деплоя — на staging (8501), боевой 8500 не трогать. 3. Деплой self — только через хук с health-check + авто-rollback (`DEPLOY_HOOK.md`). ## Эксплуатация (быстрые команды) ```bash # статус docker ps --filter name=orchestrator curl -s http://localhost:8500/health curl -s http://localhost:8500/status # активные задачи curl -s http://localhost:8500/queue # очередь # поднять staging-песочницу docker compose --profile staging up -d orchestrator-staging curl -s http://localhost:8501/health # логи docker logs --tail 100 orchestrator ``` --- *RUNBOOK 2026-06-05. Обновлять при изменении топологии/портов/переменных. См. CONTRIBUTING.md §8.*