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/домена.