Files
orchestrator/docs/architecture/adr/adr-0031-legacy-ownership-normalization.md

8.3 KiB
Raw Permalink Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-057 architecture architect proposed 2026-06-10 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, <repo>/.git/objects, <repo>/.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 — образец «только читать/уведомлять, не трогать хост/прод»).