ADR 0193 — Git с учётными данными выполняется на ХОСТЕ runner; код выполняется в контейнере
Статус
Accepted. 2026-10-01 (AT-116, история HS-037, эпик EP-010, раунд 36;
решение владельца от 01.10: «операции git с учётными данными на ХОСТЕ, код в
контейнере»). Закрывает половину, которую RN-558 оставила
объявленной открытой, и ссылается, не редактируя их, на
ADR 0130 (порт Docker без свободного -e),
ADR 0145 (RunnerReadiness и три
предусловия режима runner) и
ADR 0056 (учётные данные git
только в ОКРУЖЕНИИ дочернего процесса). Доказательство — AT-111.
Контекст
Измерено в dev 2026-10-01, до изменения:
- ЕДИНСТВЕННЫЙ
execengine, несущийenv, — аутентифицированныйgit fetchизEngine.Actions.Workspace.RunnerGit.fetch!/3(GitAuth.env_de_auth/1, преобразованный в map;nilдля provider без токена).TerminalExecutorвызываетRunnerRouter.exec/4и никогда не передаётenv. В режимеrunnerнет ниpush, ниcloneс учётными данными черезexec: clone при создании папки путешествует вworkspace_create(RN-532), который уже выполняется на ХОСТЕ (criarPastaDoProjeto). RunnerReadiness(ADR 0145) требует ЗАРЕГИСТРИРОВАННОГОrunningконтейнера до любой операцииRunnerGit, а в проектеrunnerэта запись существует лишь потому, что ТОТ ЖЕ runner поднял контейнер, — что устанавливаетestado.containerAtivo. Когда он установлен,tratarExec(apps/runner/src/index.ts) направлял КАЖДУЮ команду вdocker exec, у которого нет поляenv(ADR 0130). Со времён RN-558 эта пара (envприсутствует, контейнер активен) ОТКЛОНЯЛАСЬ с меткойcredencial-nao-atravessa-o-container: ОБЫЧНЫЙ путь аутентифицированного fetch заканчивался названным отказом, никогда успехом.- Папка ОДНА И ТА ЖЕ с обеих сторон:
tratarContainerStartмонтируетestado.dir(корень, подтверждённый runner) в/work(DockerViaCli.start,${raizDoProjeto}:/work:rw,PONTO_DE_MONTAGEMвpackages/docker-port/src/docker-port.ts), аcwdкаждогоexecпроверяется внутриestado.dir(validarCwdDentroDaRaiz) и переводится в/work/...только когда идёт в контейнер..git, который fetch обновляет на хосте, — тот, который видит контейнер.
Два запрещённых выхода остаются запрещёнными: свободный -e в DockerPort (ADR
0130 отказался) и флаг «пропустить контейнер» в RunnerReadiness (он обрушил бы
третье предусловие ADR 0145 для всего exec).
Решение
-
Операция git с учётными данными выполняется на ХОСТЕ runner, даже при активном контейнере. Команда dev agent по-прежнему идёт в
docker exec. Учётные данные доходят до дочернего процесса путём, который всегда работал:apps/runner/src/exec.tsсливаетenvповерхprocess.env. -
Дискриминатор — ЯВНАЯ МЕТКА, поставленная engine, а не
env. PayloadexecполучаетgitCredenciado: true, опциональный, и ставит егоRunnerRouter.exec_git_credenciado/5, вызываемый ОДНОЙ точкой (RunnerGit.despachar/5, изfetch!/3, только когда есть учётные данные). Функция требует непустойenvчерез guard клаузы. Runner выполняет на хосте ТОЛЬКО при СОВПАДЕНИИ (метка — буквальноеtrueи непустойenv).Почему не один
env, который уже существовал: тогда любойexec, который пришёл бы сenv, ушёл бы из контейнера —envстал бы дверью выхода из сдерживания, которое ADR 0145 поставил для кода. С меткой выход — это НАЗВАННОЕ решение единственного вызывающего engine, иenvбез метки не получает хост. Метки самой по себе тоже недостаточно: без учётных данных нечего защищать, и команда следует привычной маршрутизации. -
Ни один порт ADR 0130 не меняется. У
DockerPort.execпо-прежнему нет поляenv; образ, сеть, монтирование и broker не затрагиваются.RunnerReadinessостаётся байт в байт: метка меняет ГДЕ выполняет runner, никогда ЛИ engine отправляет, — предусловие зарегистрированногоrunningконтейнера продолжает действовать и для fetch. -
Отказ RN-558 продолжает существовать и сужается до одного случая. Пара (
envприсутствует, контейнер активен) БЕЗ метки по-прежнему отклоняется с той же меткой протокола и источникомpolitica. Ни один вызывающий engine сегодня не производит такую пару; на практике она возникает из-за РАСХОЖДЕНИЯ версий —brabo-runner, предшествующий этому ADR, не читает метку, направляет fetch в контейнер и отказывает как раньше. Поэтому сообщениеEngine.Runners.CredencialDeGit.mensagem/2начинает говорить, как это исправить (обновить runner или остановить контейнер), вместо «недостающая половина не существует». Текст отказа runner тоже меняется (говорит «SEM a marca de git credenciado»), и fixtureapps/runner/fixtures/exec-result-recusa-de-credencial.jsonменяется вместе с ним. -
Инвариант лога сохраняется. Ни отказ, ни лог пути на хосте не упоминают имя или значение переменной
env— отказ даёт КОЛИЧЕСТВО, а лог хоста добавляет только[git credenciado, no host]. Текст команды по-прежнему логируется как всегда (он несёт helper из ADR 0056, который ссылается на переменные по имени, но никогда по значению, — здесь это не меняется).
Последствия
- АУТЕНТИФИЦИРОВАННЫЙ fetch/clone удалённого репозитория в режиме
runnerначинает работать с поднятым контейнером, что является обычным путём. RN-558 перестаёт срабатывать на нём. - Старый runner + новый engine: runner игнорирует метку и отказывает с меткой RN-558; engine говорит «обновите runner». Новый runner + старый engine: engine не присылает метку, и runner отказывает как раньше. Ни одна комбинация не возвращается к тихому отбрасыванию.
- Git с учётными данными выполняется с пользователем и
gitХОСТА, а не контейнера. Файлы, которые fetch записывает в.git, рождаются с владельцем процесса runner — который является владельцем смонтированной папки. Не измерено: контейнер проектаrunner, чей--userне совпадает с этим владельцем, увидел бы обновлённый.gitс другим владельцем; сегодня spec, которую api присылает runner (EspecificacaoDeContainerParaRunner), не имеетusuario, поэтому контейнер работает с пользователем образа, а эффект не проверялся на настоящем Docker. - RN-558 перечисляла этот вариант как тот, что «сломал бы инвариант о том, что
работа происходит внутри контейнера». Решение владельца принимает его с
этой записанной ценой: из контейнера выходит РАЗГОВОР
gitс удалённым репозиторием (fetch с учётными данными), никогда код проекта, —npm install, тесты и сборка dev agent продолжают выполняться вdocker exec. То, что приносит fetch, — объект git, и он становится рабочим файлом только вcheckout/worktree add, которые не несут учётных данных и следуют привычной маршрутизации. - Хосту нужен установленный
git. Он уже был нужен:workspace_create(RN-532) и путь без контейнера всегда выполнялиgitна хосте. - Метка — вторая константа протокола в паре
exec, рядом сenv. Она путешествует только когда равнаtrue(payload обычного терминала остаётся байт в байт прежним), и runner принимает только booleantrue— любое другое значение — отсутствие. - Вне охвата:
pushс учётными данными черезexecrunner сегодня не существует; если он появится, то войдёт через ту же функцию (exec_git_credenciado/5), никогда через второй путь метки. - Доказательство (AT-111) заменяет отказ успехом с обеих сторон:
apps/runner/src/index-handlers.spec.ts(при активном контейнере и метке настоящий helpergitполучает учётные данные, и настоящийgit fetch originуспешно выполняется на хосте, безdocker.exec;envбез метки по-прежнему отклоняется) иapps/engine/test/engine_web/channels/credencial_no_runner_test.exs(через настоящийTerminalChannelfetch идёт с меткой, и цепочка заканчивается{:ok, _}). Тесты ExUnit не запускались в окружении этой поставки (repo.hex.pmотвечал 403); их доказывает CI.