ADR 0193 — O git credenciado roda no HOST do runner; o código roda no container
Status
Accepted. 2026-10-01 (AT-116, história HS-037, épico EP-010, rodada 36;
decisão do dono em 01/10: "operações de git credenciadas no HOST, código no
container"). Fecha a metade que a RN-558 deixou
declarada como aberta e referencia, sem editá-los, o
ADR 0130 (a porta de Docker sem -e livre), o
ADR 0145 (RunnerReadiness e as três
pré-condições do modo runner) e o
ADR 0056 (a credencial de
git só no AMBIENTE do processo filho). A prova é a AT-111.
Context
Medido em dev em 2026-10-01, antes de mudar:
- O ÚNICO
execdo engine que carregaenvé ogit fetchautenticado deEngine.Actions.Workspace.RunnerGit.fetch!/3(GitAuth.env_de_auth/1, convertido em mapa;nilpara provider sem token).TerminalExecutorchamaRunnerRouter.exec/4e nunca passaenv. Não existepushnemclonecredenciado viaexecno modorunner: o clone da criação de pasta viaja emworkspace_create(RN-532), que já roda no HOST (criarPastaDoProjeto). RunnerReadiness(ADR 0145) exige containerrunningREGISTRADO antes de qualquer operação deRunnerGit, e num projetorunneresse registro só existe porque o MESMO runner subiu o container — o que setaestado.containerAtivo. Com ele setado,tratarExec(apps/runner/src/index.ts) roteava TODO comando aodocker exec, que não tem campo deenv(ADR 0130). Desde a RN-558 esse par (envpresente, container ativo) era RECUSADO com a marcacredencial-nao-atravessa-o-container: o caminho COMUM do fetch autenticado terminava em recusa nomeada, nunca em sucesso.- A pasta é a MESMA dos dois lados:
tratarContainerStartmontaestado.dir(a raiz confirmada pelo runner) em/work(DockerViaCli.start,${raizDoProjeto}:/work:rw,PONTO_DE_MONTAGEMempackages/docker-port/src/docker-port.ts), e ocwdde todoexecé validado dentro deestado.dir(validarCwdDentroDaRaiz) e traduzido para/work/...só quando vai ao container. O.gitque um fetch atualiza no host é o que o container enxerga.
As duas saídas vetadas seguem vetadas: um -e livre na DockerPort (o ADR
0130 recusou) e uma flag "pula container" em RunnerReadiness (derrubaria a
terceira pré-condição do ADR 0145 para o exec inteiro).
Decision
-
A operação de git credenciada roda no HOST do runner, mesmo com container ativo. O comando do dev agent continua indo ao
docker exec. A credencial chega ao processo filho pelo caminho que sempre funcionou:apps/runner/src/exec.tsmescla oenvsobreprocess.env. -
O discriminador é uma MARCA EXPLÍCITA posta pelo engine, não o
env. O payloadexecganhagitCredenciado: true, opcional, e quem o põe éRunnerRouter.exec_git_credenciado/5, chamada por UM ponto (RunnerGit.despachar/5, a partir defetch!/3, só quando há credencial). A função exigeenvnão vazio por guarda de cláusula. O runner roda no host SÓ com a CONJUNÇÃO (marcatrueliteral eenvnão vazio).Por que não o
envsozinho, que já existia: com ele, qualquerexecque chegasse com umenvescaparia do container — oenvviraria a porta de saída da contenção que o ADR 0145 pôs para o código. Com a marca, a saída é uma decisão NOMEADA de um único chamador do engine, e umenvsem a marca não ganha o host. A marca também não basta sozinha: sem credencial não há o que proteger, e o comando segue o roteamento de sempre. -
Nenhuma porta do ADR 0130 muda.
DockerPort.execcontinua sem campo deenv; a imagem, a rede, o mount e o broker não são tocados.RunnerReadinessfica byte a byte: a marca muda ONDE o runner executa, nunca SE o engine despacha — a pré-condição do containerrunningregistrado continua valendo para o fetch também. -
A recusa da RN-558 continua existindo, e encolhe para um caso. O par (
envpresente, container ativo) SEM a marca segue recusado com a mesma marca de protocolo e origempolitica. Nenhum chamador do engine produz esse par hoje; na prática ela aparece por DESCOMPASSO de versão — umbrabo-runneranterior a este ADR não lê a marca, roteia o fetch ao container e recusa como antes. Por isso a mensagem deEngine.Runners.CredencialDeGit.mensagem/2passa a dizer o conserto (atualizar o runner, ou parar o container), em vez de "a metade que falta não existe". O texto da recusa do runner também muda (diz "SEM a marca de git credenciado"), e a fixtureapps/runner/fixtures/exec-result-recusa-de-credencial.jsonmuda junto. -
A invariante de log continua. Nem a recusa nem o log do caminho host citam nome ou valor de variável do
env— a recusa dá a CONTAGEM, e o log do host acrescenta só[git credenciado, no host]. O texto do comando continua sendo logado como sempre (ele carrega o helper do ADR 0056, que referencia as variáveis por nome, mas nunca o valor — isso não muda aqui).
Consequences
- O fetch/clone de repositório remoto AUTENTICADO em modo
runnerpassa a funcionar com o container de pé, que é o caminho comum. A RN-558 deixa de disparar nele. - Runner antigo + engine novo: o runner ignora a marca e recusa com a marca da RN-558; o engine diz "atualize o runner". Runner novo + engine antigo: o engine não manda a marca, e o runner recusa como antes. Nenhuma combinação volta ao descarte silencioso.
- O git credenciado roda com o usuário e o
gitdo HOST, não com os do container. Os arquivos que o fetch escreve em.gitnascem com o dono do processo do runner — que é o dono da pasta montada. Não medido: um container de projetorunnercujo--usernão seja esse dono veria o.gitatualizado com outro dono; hoje a spec que a api manda ao runner (EspecificacaoDeContainerParaRunner) não temusuario, então o container roda com o usuário da imagem, e o efeito não foi exercitado contra Docker real. - A RN-558 listava esta opção como a que "quebraria o invariante de que o
trabalho acontece dentro do container". A decisão do dono a aceita com
esse preço escrito: o que sai do container é a CONVERSA do
gitcom o remoto (o fetch credenciado), nunca o código do projeto —npm install, testes e build do dev agent continuam nodocker exec. O que o fetch traz é objeto de git, e só vira arquivo de trabalho numcheckout/worktree addque não carrega credencial e segue o roteamento de sempre. - O host precisa ter
gitinstalado. Ele já precisava: oworkspace_create(RN-532) e o caminho sem container sempre rodaramgitno host. - A marca é uma segunda constante de protocolo no par
exec, ao lado doenv. Ela só viaja quando étrue(o payload do terminal comum fica byte a byte o de antes), e o runner aceita só o booleanotrue— qualquer outro valor é ausência. - Fica de fora:
pushcredenciado peloexecdo runner não existe hoje; se nascer, entra pela mesma função (exec_git_credenciado/5), nunca por um segundo caminho de marca. - A prova (AT-111) troca a recusa por sucesso nos dois lados:
apps/runner/src/index-handlers.spec.ts(com container ativo e a marca, o helper real dogitrecebe a credencial e umgit fetch originreal sucede no host, semdocker.exec;envsem a marca segue recusado) eapps/engine/test/engine_web/channels/credencial_no_runner_test.exs(peloTerminalChannelreal, o fetch vai marcado e a corrente termina em{:ok, _}). Os testes ExUnit não rodaram no ambiente desta entrega (repo.hex.pmrespondia 403); quem os prova é o CI.