ADR 0155 — A primeira conta nasce no terminal, e o agente local sobe pareado
Status
Accepted em 2026-09-27 (AT-108), depois de implementado — o mesmo caminho
do ADR 0055. Nasceu
Proposed na sessão 1 da FASE 30 (2026-09-11) e foi exercitado pelas RN-543..552
nas sessões 2 a 8 (a E2E em máquina limpa, RN-549, é a prova de ponta a ponta),
com o fechamento narrado em
historico-de-fases.md e em
fase-30-runner-por-maquina.md.
Onde o texto abaixo diverge do código, a decisão NÃO foi reescrita: a
divergência está nomeada em "Notas de aceitação", no fim.
Segunda das duas decisões da FASE 30. Depende do ADR 0154: sem identidade de máquina, não há o que registrar aqui.
Context
O install.sh termina com o compose de pé e o binário do runner no disco. A
pessoa abre o navegador e encontra uma tela de login numa instalação onde
ninguém existe.
E o caminho de sair dali está quebrado numa instalação nova. Medido:
- o
.envque o instalador gera não tem nenhuma variável de e-mail (install.sh, o heredoc que grava as onze chaves); MAIL_TRANSPORTcai no defaultlog(docker/docker-compose.install.yml:200), e o compose diz no comentário ao lado que "o comportamento continua log-only mesmo aqui";- o registro exige verificar e-mail, e o docblock de
apps/api/src/scripts/provisionar-usuario.tsjá registra a consequência com todas as letras: "o fluxo normal de registro exige verificar e-mail — que com oMailSenderlog-only não fecha sozinho".
Ou seja: quem instala pela linha única se cadastra, não recebe nada, e a única
saída é ir pescar o link em docker compose logs api. Uma instalação que
termina dizendo "Pronto" e não deixa ninguém entrar.
provisionarUsuario existe exatamente para esse buraco — cria um usuário com
senha já verificada — e hoje tem dois chamadores, o seed de demonstração e
o smoke. Nenhum deles é a instalação de verdade.
Decision
O instalador cria a primeira conta, com consentimento explícito no terminal, e sobe o agente local já pareado.
1. Só a PRIMEIRA conta, e só quando não há nenhuma
O passo roda quando a instalação não tem usuário algum. Havendo qualquer um, o instalador não oferece — não é um criador de contas, é o fechamento de uma instalação nova. Numa migração (RN-530) os usuários vêm no restore, e o passo se cala pelo mesmo critério.
A rota é interna e autenticada por BRABO_SERVICE_TOKEN — o mesmo segredo que
o instalador acabou de gerar e escrever no .env, com modo 600 —, e recusa se
já existir usuário. Não nasce rota pública de "criar o primeiro owner": uma
rota pública que cria owner é uma corrida entre quem instalou e quem escaneou a
porta.
2. Sem verificação de e-mail, e o motivo é o que autoriza
A conta nasce verificada, por provisionarUsuario. Isso não afrouxa o
registro normal, que fica byte a byte: o que a verificação de e-mail prova é
"esta pessoa controla esta caixa", e quem está rodando o instalador já provou
algo mais forte — controla a máquina, o .env com os cinco segredos e o
daemon do Docker. Exigir dela a prova mais fraca, por um canal que a instalação
sabe estar desligado, é teatro.
O instalador diz isso na saída, em vez de deixar a pessoa descobrir que sua conta é diferente das outras.
3. O consentimento é explícito, e sem TTY o passo não acontece
Mesma régua da RN-511 e do consentir-base.mjs: o passo pergunta e-mail e
senha, e sem terminal interativo relata em vez de decidir. Não existe
caminho em que uma conta nasça com senha escolhida pelo script — nem gerada,
nem default.
A senha é lida sem eco e confirmada. Recusa mínima (comprimento) é a mesma do domínio, chamada pelo mesmo código; o instalador não carrega uma segunda régua de senha, que divergiria.
4. A chave de máquina é registrada ali, e o serviço sobe com ela
Com a conta criada, o instalador registra uma chave de dispositivo de máquina
(ADR 0154), grava a privada com o kid dentro (RN-475, o defeito que fez o
modo automático nunca autenticar) e instala o serviço — a unit por máquina
do ADR 0154 ponto 4.
O par é gerado na máquina; a privada nunca viaja. É o mesmo desenho do
navegador (ADR 0118), pelo mesmo motivo, e mantém verdadeira a frase que a
RN-519 usa para recusar a visão de maintainer: "a privada de uma chave de
dispositivo nunca sai do navegador" — passa a ser "nunca sai da máquina".
Detalhado na implementação (RN-552). Este ponto não dizia por qual credencial o instalador registra a chave, e as duas candidatas eram reais. A rota —
POST /internal/machine-device-keys— nasceu com o service token, o mesmo da primeira conta: é o mesmo instalador, no mesmo minuto, e o token prova controle da MÁQUINA, que é o que este passo quer provar. A alternativa, credencial do usuário recém-criado, contrariaria o ponto 5 deste ADR ("o instalador não faz login por ninguém") e daria à senha lida no TTY um segundo uso, onde as Consequences prometem que ela é usada e descartada. O custo — um service token vazado passa a poder FABRICAR credencial de acesso duradoura — está declarado emdocs/security-surface.md, e três contenções vivem na rota: não háuserIdno corpo (o dono é o usuário ÚNICO da instalação,409com zero ou mais de um), registrar SUBSTITUI as chaves de máquina ativas do dono (máquina reinstalada passa; mil chaves não existem), e uma JWK comdé recusada por nome em vez de gravada. Detalhado em vez de contrariado, e por escrito porque este ADR ainda éProposed: ADR aceito nunca se edita, o novo referencia o antigo.
5. O serviço sobe sem projeto, e isso é o estado normal
O agente instalado consulta GET runner/projects (ADR 0154 ponto 3) e não
encontra nada, porque ainda não há projeto. Ele espera, e isso não é erro:
não emite falha, não sai com código diferente de zero, e service status diz
"de pé, nenhum projeto em modo runner" — um dos quatro estados que a RN-518 já
distingue, e não um quinto.
Quando a pessoa criar o primeiro projeto em modo runner na web, o agente já
está lá. É esse o "linkar a pasta depois" que a fase persegue, e ele acontece
por workspace_create (RN-532), sem ninguém voltar ao terminal.
O que este ADR recusa explicitamente
- Criar conta quando já existe uma. Ver o ponto 1.
- Rota pública de primeiro owner. Ver o ponto 1.
- Senha gerada pelo script, ou default. Ver o ponto 3. Um instalador que escolhe senha produz instalações com a mesma senha.
- Afrouxar a verificação de e-mail do registro normal. O ponto 2 cria um caminho para um caso nomeado; não mexe no outro.
- Ligar SMTP sozinho. O instalador não pergunta servidor de e-mail e não
grava
MAIL_*:MAIL_TRANSPORT=logcontinua o default declarado, aqui como em produção. O que ele faz é parar de depender dele para a instalação fechar. - Pedir credencial de LLM. Está fora: a instalação termina utilizável, e a chave de provider é decisão de quem vai gastar (RN-058).
Consequences
- A instalação de uma linha passa a fechar de verdade: termina com conta, login possível e agente local de pé. Hoje termina com um login que ninguém consegue atravessar sem ler log de container.
- O instalador passa a manejar uma senha. Ela é lida sem eco, usada e
descartada — nunca gravada no
.env, nunca no marcador, nunca no log. O marcador ($XDG_STATE_HOME/brabo/) ganha o e-mail do owner criado, que é identificação e não segredo, e nada mais. - Um segundo caminho de criação de usuário passa a existir em produção. Ele é estreito (primeira conta, rota interna, service token) e some assim que a instalação tem gente — mas é uma superfície nova, e está declarada.
provisionarUsuarioganha um terceiro chamador, e o primeiro que não é automação de teste. O docblock dele diz hoje que existe para seed e smoke; passa a dizer também para a instalação.- A FASE 29 fica coerente consigo mesma. Ela recusou o instalador de uma linha "enquanto os artefatos não forem assinados" e assinou antes de instalar; esta fase fecha a outra ponta — instalar não é entregar bytes, é deixar utilizável.
Notas de aceitação (2026-09-27)
Conferido contra o código na aceitação. A decisão fica como está; estes são os pontos em que o texto e o código divergem, e onde o código resolveu:
- Ponto 2 e Consequences — "a conta nasce verificada, por
provisionarUsuario", que "ganha um terceiro chamador". O texto não enfrentou que o scriptprovisionarUsuarioRECUSA rodar comNODE_ENV=production(apps/api/src/scripts/provisionar-usuario.ts:37), e o instalador roda exatamente em produção. A resolução foi extrair o núcleo — o trio de escritas que faz a conta nascer verificada — paraProvisionarUsuarioUseCase(apps/api/src/application/use-cases/auth/provisionar-usuario.use-case.ts:43) e deixar a recusa no SCRIPT, que é quem ela protege (senha conhecida criada sem interação humana). Quem ganhou o chamador novo foi o caso de uso, pela rota da primeira conta (apps/api/src/application/use-cases/auth/criar-primeira-conta.use-case.ts:85); o script continua com os dois de antes, seed e smoke, eBRABO_FORCE_SEEDNÃO é o caminho do instalador (RN-546). - Ponto 4 — por qual credencial a chave de máquina é registrada. Já
detalhado no próprio texto, enquanto
Proposed: o service token, pelaPOST /internal/machine-device-keys(RN-552). O par nasce no terminal por dois comandos,brabo-runner device-key createefinish --id, porque okidé o id do registro no servidor e só existe depois dele (RN-551). - Ponto 5 — "não sai com código diferente de zero". A espera com zero
projetos existe (RN-550, que corrigiu a saída com 0 da RN-544 justamente por
contrariar este ponto), mas tem teto de DEZ consultas seguidas sem resposta
útil, e aí o agente sai com 1 nomeando o número
(
apps/runner/src/espera-de-projetos.ts:75, a saída emapps/runner/src/index.ts:1547). Lista vazia não conta como falha — só a api que não responde. - Ponto 5 — "
service statusdiz 'de pé, nenhum projeto em modo runner'". Ostatusnão sabe de projeto: ele responde do disco e do gerenciador, com os quatro estados da RN-518 (apps/runner/src/servico.ts:246), e a unit de máquina esperando aparece comorodando. Quem diz "nenhum projeto em modo runner" é o LOG do próprio agente (apps/runner/src/index.ts:1529). O ponto de não haver quinto estado se cumpre; o texto atribuído aostatus, não.