Перейти к основному содержимому

ADR 0145 — Docker становится реальным предусловием режима runner

Контекст​

Режим runner (ADR 0104) существует для кода, живущего в папке пользователя БЕЗ bind-mount на сервер, подтверждённой CLI (brabo-runner), работающим на её машине. Со времён ADR 0137 (RN-497) тот же CLI также поднимает контейнер Docker НА МАШИНЕ ПОЛЬЗОВАТЕЛЯ, а Engine.Actions.TerminalExecutor направляет каждую одобренную команду терминала в runner всякий раз, когда workspace ПОДТВЕРЖДЁН, а runner ПОДКЛЮЧЁН, — не проверяя, есть ли хоть один running контейнер. Выбор хост-vs-контейнер был целиком ВНУТРЕННИМ для runner (EstadoDoRunner.containerAtivo): при поднятом контейнере он выполнял через docker exec; без него молча падал на чистый ХОСТ, выполняя команду вне какой-либо изоляции.

Это делало runner несогласованным с container/mounted, которые со времён ADR 0143 (RN-502) уже ОТКАЗЫВАЮТСЯ выполнять терминал без зарегистрированного running контейнера — никогда не деградируют до локального System.cmd. Для runner «локальным» был сам хост пользователя: тот же тихий fallback, который ADR 0143 закрыл для двух других режимов, оставался открытым здесь, снаружи сервера.

Второй дефект, объявленный со времён RN-478 и так и не исправленный: материализация рабочего дерева dev agent (Engine.Actions.Workspace.ensure!/4) пыталась выполнить ЛОКАЛЬНЫЕ File.mkdir_p!/ System.cmd("git", ...) внутри процесса engine против пути, который физически существует только на машине пользователя, — у процесса engine нет туда никакого bind-mount. Предыдущее исправление лишь улучшило СООБЩЕНИЕ о неизбежном сбое; dev agent по-прежнему не имел, где работать.

Третий пункт, найденный в ходе исследования: container_start (действие, которое по-настоящему поднимает контейнер, ADR 0130/0133) тоже обслуживал runner, но его payload (imagem/network/resources/rationale — Infra ВЫБИРАЕТ кандидата из маршрутизации Arquiteto) никогда не имел смысла для этого пути: broker никогда не достигает папки проекта runner, поэтому нет маршрутизации, против которой выбирать, а поднимаемый образ — просто тот, что уже решён.

Решение​

Docker становится реальным предусловием режима runner, без fallback на хост. Три предусловия, которые нужны runner, чтобы любая команда достигла машины пользователя, — ПОДТВЕРЖДЁННЫЙ workspace, ПОДКЛЮЧЁННЫЙ runner и ЗАРЕГИСТРИРОВАННЫЙ running контейнер (ProjectContainerLifecycle.running?/1, ТОТ ЖЕ предикат, что у container/mounted), — живут в одной функции, Engine.Runners.RunnerReadiness, с двумя потребителями: TerminalExecutor (который уже проверял первые два) и новым модулем ниже.

Материализация worktree начинает происходить ВНУТРИ настоящего контейнера проекта, на машине пользователя, — по ТОМУ ЖЕ каналу, который уже выполняет терминал. Engine.Actions.Workspace.ensure!/4 разветвляется по execution_mode: локально для container/mounted (без изменений), через Engine.Actions.Workspace.RunnerGit (новый) для runner. RunnerGit выполняет git init/remote add/fetch/ checkout и эквиваленты File.dir?/ls/rm_rf, которые нужны Engine.Dev.WorktreeManager, как обычные команды оболочки, доставляемые Engine.Runners.RunnerRouter.exec/5, — никогда не новый механизм, всегда после того, как RunnerReadiness.verificar/1 подтвердит три предусловия. WorktreeManager (create/3, remove/2, list/1, cleanup_orphans/2) разветвляется по тому же критерию, а периодическая задача Engine.Dev.WorktreeCleanup начинает ПРОПУСКАТЬ (а не проваливать) проект runner без готового runner СЕЙЧАС, продолжая нормально для остальных.

Протокол exec/exec_result получает опциональное поле env. Аутентифицированному git fetch нужны учётные данные provider (ADR 0056), которые могут путешествовать только в окружении дочернего процесса — никогда argv, никогда файл. Поскольку этот дочерний процесс теперь рождается на машине пользователя, env (Record<string,string>) входит в payload, который отправляет engine; runner СЛИВАЕТ этот env поверх process.env перед spawn (никогда не заменяет — потерял бы PATH) и никогда не передаёт его на путь docker exec (без поддержки env в packages/docker-port, намеренно — см. Consequences) и не печатает его ни в одном логе.

container_start целиком переходит в исключительное владение container/mounted (оба через broker, RN-503). Рождается container_start_via_runner, новый тип действия, исключительный для runner. Тот же калибр, что у container_start (maintainer, require_approval по умолчанию, настраивается в permissions.json), — но у схемы есть только опциональный rationale: он не выбирает никакого кандидата, а поднимает уже РЕШЁННЫЙ образ (ObterSpecDeContainerUseCase). Engine.Infra.Tools. ProposeContainerStartViaRunner, новый инструмент Infra Lead, перехватывается InfraLeadServer ДО вызова propose_action — он ЛОКАЛЬНО проверяет (Project.get/1 + Engine.Runners.Registry.connected?/1, без HTTP, в том же процессе BEAM) execution_mode и наличие подключённого runner, отказывая с названной причиной вместо того, чтобы предлагать вслепую.

Последствия​

  • Проект runner без running контейнера теряет терминал и worktree dev agent, пока кто-нибудь не предложит и не одобрит container_start_via_runner. Цена принята и ИЗМЕРЕНА против реального прецедента: родственное изменение для container/mounted (ADR 0143, RN-502) уже имело тот же формат — «начинает требовать там, где раньше не требовало» — и вышло feature/, MINOR, без ветки breaking/. Ни у одного существующего проекта runner не было гарантированной изоляции до этой поставки (тихий fallback на хост — ровно то, что закрывается), поэтому никакое прежнее правильное поведение не отнимается — лишь небезопасное поведение перестаёт существовать.
  • env доходит до git fetch только когда runner выбирает путь ХОСТА. Если у runner уже есть активный контейнер (EstadoDoRunner.containerAtivo), tratarExec направляет КАЖДЫЙ exec — включая RunnerGit — через docker exec, у которого нет поля env (измеренное решение ADR 0130: сохранить порт Docker с закрытым типом, без свободного -e). git fetch, отправленный в этом состоянии, молча теряет учётные данные для приватного репозитория. Не решено в этой поставке — расширение packages/docker-port для приёма env затронуло бы ОБОИХ его потребителей (runner и broker) и осталось вне охвата; объявлено на случай, когда объём проектов runner с приватным репозиторием это оправдает.
  • RunnerReadiness.verificar/1 выполняется заново при каждом вызове RunnerGit (ещё один запрос на операцию git/worktree) — приемлемо, потому что ни одна функция этого модуля не находится на горячем пути (та же линейка, что уже действовала для Engine.Actions.Workspace.ensure!/4 до этой поставки).
  • Пробел, который RN-494 объявила для propose_container_start (ни он, ни GetInfraContextUseCase не ограничивают по executionMode — Infra Lead может предлагать вслепую), остаётся ровно таким же для того инструмента. НОВЫЙ инструмент (container_start_via_runner) рождается без этого пробела, потому что рождается умеющим отказывать, — но это не распространяется назад: propose_container_start по-прежнему не умеет различать режим, отдельное решение, вне охвата API/домена.