--- work_item: ORCH-057 stage: architecture author_agent: architect status: proposed created_at: 2026-06-10 model_used: claude-opus-4-8 --- # adr-0031: Нормализация legacy root-owned файлов при миграции uid — детект-leaf + actionable worktree-ошибка - **Статус:** proposed - **Дата:** 2026-06-10 - **Задача:** ORCH-057 (follow-up ORCH-040) - **Детальный ADR:** `docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md` ## Контекст ORCH-040 перевёл контейнеры на `user: "1000:1000"`, изменив только `docker-compose.yml`. Владельца уже существующих `root:root` файлов в bind-mount `/repos` это не меняет. Под uid 1000 `src/git_worktree.py::ensure_worktree` (`os.makedirs` стр. 78 / `git worktree add` стр. 81/85) не может создать worktree рядом с root-owned `/repos/_wt/` → `fatal: could not create leading directories … Permission denied`, который сейчас пробрасывается сырым. Конвейер приходит сюда из `launcher._spawn`/`_materialize_deferred_branch` (ORCH-088) — **агент не стартует** (launch-time инфра-сбой, не код задачи). Инцидент 06.06 на проде (первый запуск ORCH-043); workaround Стрима (`chown -R 1000:1000`) наложен вручную. ADR-040 описал нормализацию абстрактно («вне объёма кода») и не дал процедуры → баг воспроизводим на чистой среде / новом репо / после исторического запуска под root. Контейнер бежит **без root** → код физически не может `chown` чужие файлы; ему доступны лишь детект + диагностика. ## Решение Три аддитивных, обратимых kill-switch'ем слоя — паттерн условного leaf-гейта (`coverage_gate`/ `serial_gate`) + best-effort startup-хук (`main.lifespan`, как lease-reclaim). `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи (`verdict:`/`result:`/`deploy_status:`/ `staging_status:`/`security_status:`/`coverage_status:`) / схема БД — **байт-в-байт прежние**. - **Actionable worktree-ошибка (D1):** `ensure_worktree` классифицирует класс «нет прав» (маркеры `Permission denied`/`could not create leading directories`/`insufficient permission`/`EACCES`/ `EPERM`) и поднимает `RuntimeError` с причиной (legacy root-файлы после миграции uid) + лечащей командой + ссылкой на INFRA.md. Не-прав-ошибки сохраняют прежний текст/смысл (никакой подмены). Меняется лишь **формулировка**, не факт сбоя. - **Детект-leaf `src/fs_normalize.py` (D2):** чистый, never-raise, TTL-кэш (паттерн `preflight`). `scan_ownership(roots, target_uid)` обходит `/repos/_wt`, `/.git/objects`, `/.git/worktrees`, `data/runs`; ранний выход при первом `st_uid != target_uid` (`target_uid=os.getuid()` по умолчанию). `applies(repo)` (kill-switch + scope; пусто → `is_self_hosting_repo`) проверяется ПЕРВЫМ → дорогой обход только при applies. Идемпотентно; ошибка обхода → WARNING + консервативный `mismatch=False`. - **Интеграция = наблюдаемость, без блокировки claim (D3):** best-effort `scan_ownership()` на старте `main.lifespan` → WARNING + Telegram при mismatch. Claim НЕ гейтится: внятный ранний отказ даёт D1 в точке launch (знает repo, агент ещё не тратил токены). Блокирующий preflight-гейт отвергнут — preflight не знает repo, заблокировал бы и enduro-trails на общем `/repos`. - **Опц. `normalize()` (D4):** chown только при `CAP_CHOWN`/root (под uid 1000 — no-op + лог), флаг `fs_normalize_auto` (дефолт `False`). Init-контейнер/root-entrypoint отвергнут: реинтродукция root-контекста (анти-цель ORCH-040) + правка compose = self-deploy/групповой риск. Реальную нормализацию несёт операторская процедура. - **Процедура (D5):** `INFRA.md` получает раздел «Миграция uid: обязательная нормализация legacy root-файлов» (точные команды по всем корням) как обязательный шаг миграции; forward-breadcrumb из ADR-040. - **Флаги:** `fs_normalize_enabled` (kill-switch, дефолт `True`), `fs_normalize_repos` (CSV, пусто → self-hosting only), `fs_target_uid` (1000), `fs_normalize_auto` (`False`), `fs_scan_roots`, `fs_scan_cache_ttl_s` (300). Наблюдаемость — блок `fs_ownership` в `GET /queue`; опц. `POST /fs-normalize/check`. ## Альтернативы - **Init-контейнер/root-entrypoint** — реинтродукция root (анти-цель ORCH-040), self-deploy compose, групповой риск ради разовой операции. Отвергнуто; носитель нормализации — операторская процедура. - **Блокирующий claim-гейт (preflight)** — preflight не знает repo → регресс enduro на общем `/repos`. Отвергнуто. - **Блокирующий claim-гейт (queue_worker/claim)** — дорогой FS-обход в hot-path + «молчаливое зависание» вместо диагноза D1. Отвергнуто. - **Авто-chown из app по умолчанию** — под uid 1000 невозможен; ложное ожидание самолечения. Отвергнуто (оставлен opt-in `fs_normalize_auto`). - **Hard-fail старта при mismatch** — нарушает never-raise, стопорит сервис всех проектов. Отвергнуто. ## Последствия - Класс «сырой git-fatal на launch после миграции uid» закрыт внятным диагнозом (D1) + проактивным startup-сигналом (D3); пробел процедуры ADR-040 закрыт (INFRA.md). - Нулевая регрессия enduro-trails (scope first); инварианты конвейера/схема БД — байт-в-байт. - Никакого root-контекста/рестарта прода/касания `main`/force-push/прод-образа (NFR-1). - Плата: фактический `chown` остаётся ручным операторским шагом (но теперь внятным, с инструкцией); +1 best-effort startup-хук и leaf-модуль; `fs_normalize_auto=True` под root реинтродуцирует chown-контекст (дефолт `False`, не для прод-self). - Аддитивно/обратимо: **не** `arch:major-change` (нет новой стадии/QG/таблицы/смены топологии) — leaf + startup-хук + docs. - **Откат:** `fs_normalize_enabled=False` → полный no-op (мгновенный обратимый kill-switch). ## Связи adr-0005 (контейнер под host-uid — порождающее решение ORCH-040, чей пробел закрываем), adr-0029/adr-0012 (coverage/security-гейт — паттерн условного leaf `applies`/scope/never-raise/ fail-open), adr-0017 (serial-gate — leaf never-raise + отложенный срез ветки `_materialize_deferred_ branch`, чья точка падает в `ensure_worktree`), adr-0011 (job-reaper — образец best-effort startup-хука в `lifespan`), adr-0024 (disk-watchdog — образец «только читать/уведомлять, не трогать хост/прод»).