Pular para o conteúdo principal

FASE 30 — o agente local da máquina

Este documento é o recorte da fase: o que ela persegue, o que ela recusa, e em que ordem. As decisões moram no ADR 0154 e no ADR 0155 — o que estiver aqui e lá em desacordo, vale o ADR.

O que a fase persegue​

Instalar o Brabo numa linha e usar. Hoje a FASE 29 entrega quase isso: o install.sh verifica a própria origem, sobe o compose, instala o binário do runner com install -m 0755 e grava a base. E então para, em dois pontos que o próprio produto já declarava por escrito.

O primeiro é o agente local. A última linha do instalador diz que ele não pareia o agente com um projeto, e que a chave de dispositivo continua vindo da tela. Quem quiser usar o binário instalado volta ao navegador e termina colando no terminal o comando que apps/web/src/lib/runner-bootstrap.ts:398 monta. O comentário ao lado explica por que a tela não consegue evitar isso — "uma página web não executa binário na máquina de ninguém" — e está certo. O que ele não cobre é que o instalador não é uma página web: ele roda no terminal, com TTY, e o ADR 0150 recusou curl | sh justamente para poder perguntar.

O segundo é que ninguém consegue entrar. Medido nesta fase: o .env gerado não tem nenhuma variável de e-mail, MAIL_TRANSPORT cai em log (docker/docker-compose.install.yml:200), e o registro exige verificar e-mail — que, como o docblock de apps/api/src/scripts/provisionar-usuario.ts já registrava, "com o MailSender log-only não fecha sozinho". A instalação de uma linha termina dizendo "Pronto" e deixando a pessoa pescar um link de verificação em docker compose logs api.

Os dois se resolvem no mesmo lugar, e é isso que faz uma fase só.

O que a medição mudou no recorte​

A FASE 29 tirou o runner por máquina do escopo com uma medição: "um runner por projeto está imposto em cinco lugares independentes". Remedindo com arquivo:linha, três dos cinco não precisam mudar — e isso encolhe a fase pela metade.

#ondeo que impõeesta fase
1terminal_channel.ex:204tópico terminal:<projectId>não toca
2runner_socket.ex:73socket id com project_idnão toca
3Engine.Runners.SocketTicketticket carrega project_idnão toca
4apps/api/src/db/schema/auth.ts:325-326project_id NOT NULLvira nullable
5apps/runner/src/servico.ts:264,391unit brabo-runner-<projectId>vira uma por máquina

O motivo está no ADR 0154: os itens 1, 2 e 3 descrevem uma conexão, não um processo. Um processo que abra N conexões — uma por projeto — os satisfaz byte a byte, e preserva a recusa de segundo runner no mesmo projeto, que é uma garantia real e não um acidente. O que sobra é o que é do processo: a credencial (4) e a unit (5).

Nada no engine muda nesta fase. Foi o achado mais útil da medição, e é o que mantém RunnerReadiness (RN-507), o espelho (RN-516) e workspace_create (RN-532) byte a byte.

As sessões​

Uma entregável cada. A coluna "depende de" é que ordena.

#entregáveldepende deproibições próprias
1este documento, os ADRs 0154/0155 e a faixa de RN—não toca apps/, docker/, scripts/, install.sh; não edita ADR aceito
2FECHADA — api: project_id nullable, chave de máquina aceita em runner-ticket, GET runner/projects (RN-543)1não toca o engine; não afrouxa papel de rota nenhuma
3FECHADA — runner: N conexões, uma por projeto, descobertas pela rota (RN-544)2não toca RunnerReadiness, o espelho nem workspace_create
4FECHADA — runner: unit por máquina no service install, convivendo com as por projeto (RN-545)3não remove a unit por projeto; não muda Restart=on-abnormal
5FECHADA — api: POST /internal/first-account, conta verificada + workspace pessoal, 409 com qualquer usuário (RN-546)1não cria rota pública; não toca o registro normal
6FECHADA — install.sh: consentimento, conta, chave de máquina e service install (RN-547)4, 5não grava senha em lugar nenhum; sem TTY relata e sai 0
7FECHADA — web: a tela do projeto reconhece agente de máquina já pareado (RN-548)3não remove o fluxo do ADR 0118
8FECHADA — E2E em máquina limpa, docmap, docs:check (RN-549)6, 7não afrouxa gate para o E2E passar

O que a sessão 2 fechou, e o que ela deixou declarado​

A metade da api está de pé: runner_device_keys.project_id é nullable, o PatAuthGuard resolve o papel contra o projeto pedido quando a credencial não nomeia nenhum, e GET runner/projects existe. Duas coisas que a medição do recorte não antecipava e a implementação obrigou a decidir, as duas registradas na RN-543:

  • A listagem da RN-519 passou a devolver as duas espécies, marcadas. Uma chave de máquina que não entrasse em listagem nenhuma seria invisível e permanente — o defeito que a RN-519 fechou, renascido na espécie nova.
  • A revogação ganhou alvo PLURAL. revogar de uma chave de máquina não tem um projectId a passar ao engine; deixar de derrubar reabriria a RN-520 em N conexões. Um {projeto, usuário} por projeto em modo runner do dono, sem tocar o engine.

E o que segue declarado, não feito: nenhuma rota da api CRIA chave de máquina ainda — quem registra é o install.sh (ADR 0155 ponto 4), na sessão 6. A rota nasce no PR que tiver o primeiro chamador real.

O que a sessão 3 fechou, e as três decisões que ela teve de tomar​

O agente local abre N conexões de verdade, e a medição do recorte se confirmou: nada no engine mudou, e os handlers do runner (tratarExec, tratarMirrorSync, tratarWorkspaceCreate) não mudaram uma linha — o que forçou o desenho foram os quatro campos POR PROJETO de EstadoDoRunner, que viraram N estados com docker e base compartilhados por valor. O modo antigo ficou byte a byte, e o portão novo é estreito: credencial de máquina e base consentida.

Três coisas o recorte não antecipava, e a RN-544 as registra:

  • Um projeto recusado não derruba os outros. O teto de tentativas e a recusa de join deixaram de ser do PROCESSO e passaram a ser do PROJETO; só quando nenhum sobra o processo sai com 1, com o desfecho de cada um. No modo de projeto único a disposição antiga fica idêntica, pelo raciocínio invertido: com UMA conexão, não sobra nada a atender.
  • A lista é consultada só no start. Repesquisar faria uma lista que volta MENOR — apagado? convertido? 500 transitório? — derrubar conexão viva por ambiguidade. O processo diz isso ao subir, e o gesto é reconectar.
  • Lista vazia é estado NORMAL, com saída 0. É o estado de toda instalação nova, e Restart=on-abnormal não a reergue de propósito. (Revisado ao dividir a sessão 6, RN-550: com ZERO conexões o agente passa a FICAR DE PÉ e reconsultar — a metade "só no start" fica intacta para quem tem conexão viva.)

E o que segue declarado, não feito: a apiUrl no modo de máquina vem de --api-url/BRABO_API_URL/default, e nunca do brabo-runner.config.json, que é por PROJETO — quem escreve a flag é a unit por máquina, da sessão 4. A unit continua por projeto: esta sessão mudou o PROCESSO, não o serviço.

O que a sessão 4 fechou, e as três decisões que ela teve de tomar​

O quinto e último acoplamento da tabela acima fechou: service install --machine instala brabo-runner.service / dev.brabo.runner, sem sufixo, e põe de pé o processo que a sessão 3 criou. A unit por projeto não foi removida nem renomeada, Restart=on-abnormal ficou byte a byte (trocá-lo desfaria também a decisão da sessão 3 de fazer lista vazia sair com 0), e nada na api, no engine ou no web foi tocado.

Uma medição mudou o mecanismo, e ela contraria a letra do ADR 0154 ponto 5. O ADR escreveu "service install sem --project instala a unit de máquina"; resolverProjeto tem duas fontes, e o caminho normal de hoje é rodar install sem flag nenhuma de dentro da pasta que o navegador configurou — é o brabo-runner.config.json que responde. Ao pé da letra, o ADR converteria em silêncio a instalação de quem já usa o produto, que é o oposto do que o mesmo ponto 5 promete. Então o discriminador virou a flag --machine, opt-in explícito, com as duas fontes de projeto intactas.

As três decisões que o recorte deixava em aberto, registradas na RN-545:

  • status responde sobre a espécie perguntada, e o código não soma. Os quatro estados e os quatro códigos (RN-088) não viram oito; a outra espécie aparece em TEXTO, como presença lida do disco, dizendo que o estado dela não foi perguntado ao gerenciador. Sem --machine e sem projeto resolvível, a resposta é sobre a máquina — a única unit cujo nome não precisa de argumento.
  • install RECUSA quando a outra espécie já está instalada. Não remove nada, não grava nada, e nomeia o uninstall de cada unit encontrada. Sem --force: as duas juntas seriam dois processos disputando o mesmo projeto, e o servidor negaria um deles. A garantia do ponto 5 (unit já instalada continua funcionando) fica intacta — o que se recusa é criar a sobreposição agora.
  • uninstall sem espécie nomeada não remove nada, e lista o que existe. Ele não herda o default de status de propósito: ler a espécie errada custa uma linha errada, remover a errada custa um serviço e uma chave.

Duas recusas próprias do install --machine não estavam no recorte e são medidas: a pasta não pode ter brabo-runner.config.json (o serviço subiria em modo de PROJETO, em silêncio, atendendo um só) e a base precisa estar consentida (sem ela o agente sai em uso() no primeiro boot). A unit congela XDG_CONFIG_HOME — é ela que decide onde o arquivo da base mora, e nenhum dos dois gerenciadores repassa o ambiente do shell — mas nunca o valor da base.

E o que segue declarado, não feito: o install --machine não sabe se a chave daquela pasta é mesmo de máquina (em disco as duas espécies são o mesmo arquivo, e quem sabe é o servidor), então uma pasta com chave de projeto instala a unit sem erro e a recusa aparece no primeiro boot. E ninguém CRIA chave de máquina ainda — é a sessão 6.

O que a sessão 5 fechou, e a decisão que o ADR 0155 não tinha​

A sessão 5 fechou com uma decisão que o ADR 0155 não tinha enfrentado, e ela vale para quem pegar a sessão 6: provisionarUsuario recusa rodar com NODE_ENV=production, e o instalador roda exatamente lá. Em vez de BRABO_FORCE_SEED, o núcleo virou ProvisionarUsuarioUseCase e a recusa ficou no script — o que ela protege é senha CONHECIDA criada SEM interação humana, e a rota é outra categoria. A sessão 5 também acrescentou o que o ADR não dizia e a RN-410 exige: o workspace pessoal nasce na mesma transação, senão a instalação fecharia com um login que atravessa e um dashboard onde "Novo projeto" não tem onde criar.

O que a sessão 7 fechou, e as três decisões que ela teve de tomar​

A marca de espécie que a sessão 2 acrescentou à listagem — "sem elas na lista, uma chave de máquina seria invisível em toda tela" — ganhou consumidor. O painel de onboarding do runner passa a reconhecer chave de MÁQUINA ativa e, com ela, para de mandar parear: o que falta não é pareamento, é o agente estar rodando. Nada da api, do engine ou do runner foi tocado.

As três decisões que o recorte deixava em aberto, e o que cada uma virou (a RN-548 tem o porquê inteiro):

  • Onde aparece: no PAINEL, e por isso nos três montadores de uma vez — mas não com o mesmo resultado. O reconhecimento é por PROJETO, porque a rota é GET /projects/:projectId/runner-device-keys: TerminalPanel e FolderBrowserModal sempre têm projeto, e o NewProjectWizard só depois da criação antecipada (RN-437). Sem projectId não há a quem perguntar. AmbienteDoProjeto foi CONSIDERADO e recusado: o docblock dele já declara que presença de runner é conhecimento de primeira mão do canal, e uma linha sobre chave registrada ali seria um terceiro proxy competindo com um sinal mais forte — e sobre a MÁQUINA, não sobre o projeto.
  • Com chave e sem conexão viva — o caso mais comum — a tela diz as duas coisas que sabe e nomeia as duas que não sabe. Anuncia o pareamento (com os nomes das chaves e a data do último uso), e ao lado: chave registrada não é agente rodando e a lista é da sua CONTA, não deste navegador. É a régua do workspaceVerifiedAt aplicada um passo antes, tom accent e nunca success incluído. O gesto que ela oferece é conferir o SERVIÇO na máquina pareada, e quem responde pelo agora continua sendo a EsperaDoRunner, reusada.
  • A espécie aparece pelo CUSTO dela, não como rótulo. O painel não lista chaves nem revoga — diz que uma chave de máquina atende todos os projetos do dono, e que revogá-la derruba o agente local em todos eles.

E o que segue declarado, não feito: ninguém CRIA chave de máquina ainda (é o install.sh, sessão 6), então hoje a tela só é exercitável com uma chave registrada à mão. O comando oferecido é brabo-runner service status --project <id>, a forma que existe hoje — a por máquina chega com a unit da sessão 4 (fechado depois, AT-106: com chave de máquina reconhecida o painel oferece brabo-runner service status --machine; com chave de projeto, segue o --project <id>). Uma chave de PROJETO ativa não muda o painel: é o defeito irmão, um escopo abaixo. E a tela de listar/revogar chave continua não existindo.

O que a divisão da sessão 6 fechou — e por que ela não é sessão 8​

A sessão 6 tocaria apps/api, apps/runner e o install.sh de uma vez, e entrega de três workspaces é onde uma sessão se perde. Ela foi dividida em três: as duas peças abaixo, disjuntas e em paralelo, e o install.sh depois delas, como orquestrador e nada mais — a sessão 6 segue aberta, com esse recorte menor. Nenhuma destas duas é sessão numerada da fase, e em especial nenhuma é a sessão 8, que é o E2E e continua aberta na tabela acima.

As duas peças que faltavam do lado do agente, e as duas vêm do ADR 0155.

A primeira é uma REVISÃO da sessão 3, e a decisão é a ASSIMETRIA (RN-550): com zero conexões o agente deixa de sair e passa a ESPERAR, reconsultando GET runner/projects a 15s/30s/60s até o primeiro projeto aparecer; com conexão viva, a lista continua sendo lida só no start, byte a byte. O argumento da sessão 3 continua inteiro onde ele vale — lista que volta MENOR é ambígua, e derrubar conexão viva por ambiguidade troca estado certo por palpite —, e com zero conexões não há nada a derrubar. O que caiu foi o argumento do exit 0 ("um serviço ativo que não faz nada"), porque o processo passou a fazer algo e a dizer que faz. Medido nesta sessão: Restart=on-abnormal não olha código de saída — ele reergue por sinal, watchdog ou timeout —, então nada nas duas units muda, e o efeito é a unit de máquina ficar active (running) de verdade. Esperar não tem teto; falhar tem (dez consultas seguidas, saída 1 nomeando o número).

A segunda fecha a lacuna que as sessões 2, 3 e 4 declararam — "ninguém CRIA chave de máquina ainda" — pela metade que é do agente (RN-551): brabo-runner device-key create|finish gera o par Ed25519 nesta máquina. A decisão aqui é o CORTE: o CLI não fala com a api, porque quem registra é o instalador com o BRABO_SERVICE_TOKEN, e dar esse segredo ao agente ampliaria o que ele pode muito além do que ele precisa — cada lado guarda um segredo e nenhum vê o do outro. Disso decorre o desenho de dois passos: o kid é o id do REGISTRO e só existe depois dele, então o create grava um .parcial que o leitor ignora e o finish grava o nome de verdade. O arquivo que o runner lê nunca existe sem kid — o defeito da RN-475, impossível por construção.

O que segue declarado, não feito: a ROTA que registra a pública de máquina e o install.sh que encadeia os três comandos continuam sendo a sessão 6.

E a metade de API da mesma divisão (RN-552): POST /internal/machine-device-keys, engine-service, corpo { name, publicKeyJwk } e resposta com o id que vira o kid da privada (RN-475). A credencial foi decidida e o custo está declarado: o service token, porque exigir credencial do usuário recém-criado obrigaria o instalador a abrir na máquina exatamente a sessão viva que o ADR 0155 ponto 5 recusou abrir — e daria um segundo uso à senha que a RN-546 descarta de propósito. O preço: um BRABO_SERVICE_TOKEN vazado passa a poder fabricar credencial de acesso duradoura, não só falar com rotas internas.

Três contenções vivem na rota, não no texto: não há userId no corpo (o dono é o usuário ÚNICO da instalação, e zero ou mais de um é 409 sem escrever nada — a condição da RN-546 com outra forma, sobre a INSTALAÇÃO e nunca sobre o argumento); registrar SUBSTITUI as chaves de máquina ativas do dono na mesma transação, então máquina reinstalada passa e mil chaves são impossíveis por cláusula, sem número que envelheça; e uma JWK com d é recusada por nome, com a régua de forma virando UMA, no domínio, chamada pelos dois registradores.

O que segue declarado, não feito: numa instalação sem PROJETO, tela nenhuma alcança uma chave de máquina — listar e revogar são as duas por /projects/:projectId/... —, e é por isso que registrar substitui em vez de deixar órfã: uma órfã ali seria viva e inalcançável.

O que a sessão 6 fechou, e as quatro decisões que ela teve de tomar​

Com o recorte menor, a sessão 6 foi orquestração e nada mais: nenhuma linha de apps/ mudou, nenhuma rota nasceu, e as quatro peças que o install.sh encadeia já existiam testadas. O buraco que a fase abriu no começo — "numa instalação nova ninguém consegue entrar" — fecha aqui, e a promessa do ponto 5 do ADR 0155 passa a acontecer de verdade: o agente sobe sem projeto, espera, e pega o primeiro que a pessoa criar na web.

As quatro decisões, registradas inteiras na RN-547:

  • Quando um elo do meio falha, nada é desfeito e tudo é relatado — e o script sempre sai 0. Os elos já cumpridos são úteis por si (com a conta a pessoa entra, com a chave a máquina está pareada), desfazer exigiria apagar conta e revogar chave, e não há rota para isso nem deveria haver uma que o instalador chame sozinho. O preço é uma instalação que pode terminar pela metade; o que ela nunca faz é terminar pela metade em SILÊNCIO — cada falha é uma linha nomeada num bloco final, com o comando para repetir.
  • Idempotência é o CÓDIGO HTTP, e 409 não é falha. Segunda execução, ou migração cujo restore trouxe os usuários: o passo se cala dizendo por quê e não entra nas pendências. 000 — o curl que não falou com a api — tem tratamento próprio, senão "a api não subiu" seria lido como "a api recusou". E o passo do agente só roda quando ESTA execução criou a conta: com usuário preexistente, registrar chave de máquina cunharia credencial duradoura para quem não pediu nada nesta rodada.
  • O name da chave é o hostname. A RN-551 declarou que o CLI não nomeia — é campo do registro, e quem nomeia é quem registra. Quem abre a lista de chaves precisa saber a QUAL máquina ir, e o hostname é o nome que a pessoa já usa para falar das máquinas dela.
  • O .parcial que sobra quando o registro não fecha FICA, e é nomeado. Apagá-lo seria o instalador decidir que sabe algo que não sabe: um timeout depois de a api gravar é indistinguível de um antes, e o arquivo pode ser a única cópia da metade privada de uma chave já registrada.

Duas coisas mudaram e não estavam no recorte. A senha é enviada pelo STDIN do curl, com o service token num --config 600 — /proc/<pid>/cmdline é legível por qualquer usuário da máquina, e uma senha em argv ficaria exposta durante a requisição. E o --help mentia: ele imprime o cabeçalho, que é comentário, e a regra que proíbe falar de "sessões de fase" a quem instala só lia linhas de código — então "ESTA VERSÃO NÃO INSTALA NADA" sobreviveu meses depois de o script subir o compose e instalar o runner. Corrigido, com teste sobre a SAÍDA do --help, e o intervalo fixo de linhas que o cortava em silêncio virou "até a primeira linha que não é comentário".

E o que segue declarado, não feito: a instalação de ponta a ponta numa máquina limpa — compose de verdade, api de verdade, binário do runner de verdade, unit escrita e agente esperando — é o E2E da sessão 8, e só ele pode provar. Nos testes desta sessão o brabo-runner é um dublê de shell e a api é um servidor de teste: o que se prova é o ENCADEAMENTO, nunca a integração. E o install --machine continua sem saber se a chave daquela pasta é mesmo de máquina (a lacuna da RN-545) — a diferença é que agora existe um caminho que CRIA uma de máquina ali, então a recusa no primeiro boot deixou de ser o desfecho provável.

O que a sessão 8 fechou — e as três decisões que ela teve de tomar​

A última da fase, e a única que podia responder a pergunta que a fase inteira persegue. A sessão 6 declarou por escrito que os testes dela provam o ENCADEAMENTO e nunca a integração — brabo-runner dublê de shell, api de mentira —, e é essa lacuna que fecha aqui.

O harness não nasceu nesta sessão: .github/workflows/install-e2e.yml existe desde a FASE 29 (RN-534) e já rodava o instalador publicado num runner efêmero do Actions, que é literalmente uma máquina limpa. O que ele provava parava no .env. A sessão o estendeu, e o que ele prova agora são os cinco elos da RN-547 contra api e binário de verdade, o agente de pé esperando, e o primeiro projeto em modo Runner sendo pego sem ninguém voltar ao terminal.

As três decisões, registradas inteiras na RN-549:

  • O sinal de "o agente está de pé" é a LINHA que ele escreve, nunca o estado do gerenciador. perguntarEstado mapeia activating para rodando — com razão (RN-088: "subindo" não é "parado") —, então systemctl is-active e service status --machine respondem de pé para um processo que vai morrer dois segundos depois. A linha da RN-550 só existe depois de o agente ter lido a chave do disco, tirado ticket, falado com a api e recebido lista VAZIA: ela prova pareamento e espera de uma vez, e é um FATO GRAVADO, não uma amostragem de estado — por isso esperar por ela com teto não é corrida. O status entra depois dela, como confirmação, nunca no lugar.
  • O desfecho se prova pela API, não pelo navegador. O sujeito é o AGENTE, e o que ele observa é GET /runner/projects. Um navegador criaria o projeto pela mesma rota com uma camada a mais que falha por motivos alheios — e não tornaria a asserção mais forte, porque ela continua sendo a linha do journal. A camada de navegador existe (e2e/, ADR 0120), responde outra pergunta e assume o compose de produção de pé: trazê-la faria o E2E do instalador depender de um segundo harness numa máquina que deveria estar limpa.
  • A metade que roda em PR é um TESTE, não o workflow. O E2E segue fora de pull_request — abrir essa porta é dar ao instalador um jeito de pular a verificação de origem, que é o que o ADR 0150 recusa. Só que workflow que roda em tag é workflow cujo erro aparece na tag: scripts/dev/install-e2e.spec.ts guarda as duas formas de ele apodrecer calado — o gate afrouxado, e a frase do install.sh reescrita, que faz as asserções não falharem, e sim SUMIREM.

Dois defeitos apareceram, os dois invisíveis porque o workflow nunca rodou. O primeiro era do harness e foi corrigido: o arquivo de respostas do TTY começava por nao-migrar, que numa máquina limpa cai na pergunta do MARCADOR — ela não casa s|S|sim|SIM, o script dizia "Nada foi gravado" e saía 0, e a asserção do marcador teria reprovado. O segundo é ACHADO e ficou como achado: o install.sh publicado não sobe nada sozinho numa máquina limpa — ele usa docker/docker-compose.install.yml, que não é asset da Release, não entra no checksums.txt assinado e ele não baixa em lugar nenhum (com ./postgres/init.sql junto, são três arquivos). Quem segue o curl … install.sh do runbook morre em "no such file or directory" depois de já ter verificado assinatura e gravado o .env. O workflow os traz à mão, pela tag, num passo que diz que é achado; consertar é decidir entre asset assinado e clone, e é entrega própria.

E o que segue declarado, não feito: este E2E não roda no PR que o escreveu — a primeira execução real é a próxima tag final, e nenhum check do PR prova uma linha dele. Ficam fora, por decisão: a segunda execução pelo caminho de MIGRAÇÃO (RN-530), o macOS (o passo completo é Linux — launchd, e o Docker que o runner hospedado não tem) e o navegador.

Faixa reservada: RN-543..555. ADRs 0154 e 0155. A RN-541 já está alocada em branch não mergeada — o salto é deliberado, pelo critério que a FASE 29 registrou ao resolver três colisões pela data.

O que a fase entregou, no conjunto​

Oito sessões, mais as duas peças que saíram da divisão da sessão 6 e não são sessões numeradas. O que a fase prometeu no começo — instalar o Brabo numa linha e usar — passou a acontecer:

  • A identidade do agente local deixou de ser por PROJETO e passou a ser da MÁQUINA. Os dois acoplamentos que a medição do recorte disse que precisavam mudar mudaram: runner_device_keys.project_id é nullable (RN-543) e a unit é uma por máquina (RN-545). Os outros três não mudaram, e a medição que os salvou é o achado mais útil da fase: tópico, socket id e ticket descrevem uma CONEXÃO, e N conexões os satisfazem byte a byte.
  • Nada no engine mudou. RunnerReadiness (RN-507), o espelho (RN-516) e workspace_create (RN-532) ficaram byte a byte, e os handlers do runner também.
  • Uma instalação nova deixou de nascer sem ninguém dentro. MAIL_TRANSPORT continua log e o instalador continua não ligando SMTP — o que mudou é que a primeira conta nasce no TERMINAL, já verificada (RN-546/RN-547), com o workspace pessoal na mesma transação. Pescar link de verificação em docker compose logs api deixou de ser o caminho.
  • O agente sobe pareado e ESPERA. O par Ed25519 nasce na máquina (RN-551), só a pública viaja (RN-552), o id do registro vira o kid (o invariante da RN-475, agora impossível de violar por construção), e com zero projetos o processo fica de pé reconsultando (RN-550) em vez de sair.
  • A tela reconhece quem já está pareado (RN-548), sem nunca afirmar que o agente está rodando — chave registrada prova pareamento, não processo vivo.
  • E o conjunto está medido de ponta a ponta (RN-549), contra api e binário de verdade, numa máquina limpa.

O que a fase não fechou está logo abaixo, em "Lacunas que a fase encosta e não resolve", e nenhuma delas mudou de tamanho no caminho — exceto a da TELA de listar/revogar chave, que ficou maior de propósito: a fase acrescentou uma espécie de chave a listar e não construiu a tela. Acrescente-se o achado do compose que a sessão 8 mediu, e a lacuna do install --machine, que continua sem saber se a chave daquela pasta é mesmo de máquina — em disco as duas espécies são o mesmo arquivo, e quem sabe é o servidor.

O que esta fase NÃO toca​

Declarado nos dois ADRs, e repetido aqui porque é o que dá sentido ao resto:

  • decide.ts e os tetos absolutos — nenhum ganha exceção, nem configurável
  • proposed_action como origem de todo efeito externo de agente; criar pasta e parear agente continuam sendo configuração consentida, não ação de agente (a linha da RN-516)
  • a imutabilidade do event log
  • RunnerReadiness (RN-507) e suas três pré-condições — e nunca uma flag "pula container", pelo motivo do ADR 0147 ponto 4
  • o espelho (RN-515/516/517) e workspace_create (RN-532): os dois são por projeto e viajam na concessão do join daquela conexão
  • o registro normal com verificação de e-mail, e o default MAIL_TRANSPORT=log
  • o fluxo de configuração pelo navegador (ADR 0118)

Lacunas que a fase encosta e não resolve​

  • A revogação continua alcançando {projeto, usuário}, nunca {chave} (RN-520) — e passa a custar mais: revogar derruba o agente de todos os projetos daquela máquina. A FASE 29 declarou esse custo por antecipação; aqui ele chega. Fechar exige mudar o alvo da revogação, que é frente própria.
  • Não há tela onde listar e revogar chave de dispositivo. A RN-519 abriu o GET na api; o web nunca ganhou a tela. A fase acrescenta uma espécie de chave a listar e não constrói a tela — a lacuna fica maior, e declarada. A RN-548 passa a CONSUMIR essa listagem no painel de onboarding, o que não a fecha: ler para reconhecer é outra coisa que listar para revogar.
  • Ponto único de falha. Um processo por máquina no lugar de N: o serviço cair tira o agente de todos os projetos. É o preço do desenho, não um descuido.
  • A credencial de git segue descartada no docker exec em modo runner (ADR 0145) — esta fase não a toca, e nada aqui a piora.
  • BRB-031 fecha por decisão do mantenedor, não por consequência: a fase cria o caminho sem chmod e mantém o do navegador. Escolher aposentar o segundo é decisão à parte.
  • BRB-017 (obrigação GPL da imagem do engine, P1) fica onde estava, e esta fase o torna mais concreto pelo mesmo motivo que a FASE 29: mais instalações em máquinas de terceiros.