Manual do brabo-runner
Manual completo de instalação, uso e operação, movido do README para manter a porta de entrada curta. O resumo está no README.
CLI que roda na máquina do próprio usuário e se conecta ao engine do Brabo
por um canal Phoenix autenticado com ticket de uso único (ADR 0102/0103). Serve
aos projetos em modo runner (ADR 0104): a pasta do código fica na sua máquina,
sem bind mount. Com os privilégios de quem o executa, ele:
- executa os comandos dos agentes que o pipeline de
proposed_actionjá aprovou (exec), confirmando o caminho real da pasta; - abre um terminal interativo de verdade (PTY) para a aba Terminal da web;
- navega pelas pastas locais (
fs_list_dir/fs_home_dir) para o seletor "Procurar pasta..." da criação e adoção de projeto; - sobe o container do projeto (
container_start) pela porta de Docker do@brabo/docker-port(ADR 0128/0130) e cria pastas de projeto (workspace_create) quando há uma base consentida.
Ele não decide política: só faz o que o produto já aprovou. O desenho de cada
parte está nos docblocks de src/channel.ts, src/exec.ts, src/pty.ts,
src/guard.ts e src/fs-browser.ts. As regras de negócio estão em
docs/business-rules/runner.md.
Repositórios do Brabo
O Brabo é dividido em repositórios por camada (ADR 0252), e a documentação central está no brabo e em https://viralabs-dev.github.io/brabo/prd/.
- Depende de: brabo-contracts
(o
@brabo/docker-port, instalado do GitHub Packages na versão1.2.0, pelo aliasnpm:@viralabs-dev/brabo-docker-port@1.2.0) e brabo-ci (política de PR e release por tag). - É usado por: brabo (o
install.shbaixa o binário da Release do runner) e brabo-infra (aferições cruzadas e manifesto da Release do produto).
| Repositório | Papel |
|---|---|
| brabo | Porta de entrada: documentação central, ADRs, instalador e release do produto |
| brabo-api | API (NestJS, Drizzle, PostgreSQL com pgvector) |
| brabo-engine | Engine (Elixir/OTP, Phoenix, Oban): agentes, gates, sessões e runners |
| brabo-web | Interface web (React, Vite, TanStack, Tailwind) |
| brabo-runner | Este repositório: a CLI que roda na máquina do usuário |
| brabo-broker | Broker de containers, o único processo que fala com o Docker do servidor |
| brabo-contracts | Contratos compartilhados (shared, docker-port, contracts) |
| brabo-infra | Infraestrutura: composes, Kubernetes, backup e restore, docker-bake |
| brabo-ci | Workflows reutilizáveis de política de PR, aprovação, promoção e versão |
| brabo-qa | Testes entre repositórios: E2E, golden-sets e instrumentos de medição |
| brabo-workspace | Pasta-mãe que reúne os clones dos repositórios lado a lado |
Instalação
Pelo instalador (recomendado)
É o mesmo comando que a tela do projeto (modo runner) mostra:
curl -fsSLO https://github.com/viralabs-dev/brabo/releases/latest/download/install.sh && bash install.sh
O install.sh vem da Release do brabo; o binário do runner vem da Release do
viralabs-dev/brabo-runner na tag que o instalador fixa. Ele confere o binário
contra o checksums.txt assinado dessa Release, instala já executável
(install -m 0755), pareia a máquina pelo terminal (device-key create/finish,
abaixo) e instala o agente de máquina como serviço (RN-547).
O antigo modo automático pelo navegador (ADR 0118), que gravava binário,
brabo-runner.config.json e chave de dispositivo numa pasta, foi aposentado no
ADR 0203. Uma pasta configurada assim continua funcionando: a CLI ainda lê a
config e a chave da pasta de onde é executada, e uma flag explícita sempre vence
o arquivo local.
Binário standalone (sem Node nem npm)
O runner é distribuído só como binário da Release (AT-774); não há pacote no npm.
Baixe o executável da sua plataforma na Release do brabo-runner:
| Plataforma | Arquivo |
|---|---|
| Linux x64 | brabo-runner-linux-x64 |
| Linux ARM64 | brabo-runner-linux-arm64 |
| macOS Apple Silicon | brabo-runner-darwin-arm64 |
chmod +x ./brabo-runner-<plataforma>
./brabo-runner-<plataforma> --project <projectId> --dir <caminho-absoluto> --token brb_...
- macOS Intel não tem binário, por decisão (ADR 0174): no
macos-15-intelo binário compila mas reprova o--self-test-pty, por um bug do Bun com onode-pty(oven-sh/bun#25822). A mesma prova passa no Node, então ali não há instalação, e oinstall.shdiz isso e aponta onde pedir. - Windows não tem binário, por decisão (ADR 0187): sob Bun o pipe de saída
do ConPTY termina depois do primeiro pedaço. O
install.shjá recusa Windows (ADR 0150).
O binário é um arquivo só, com o addon nativo node-pty embutido (ADR 0112) e
extraído para um diretório temporário na primeira execução. Ainda não é
assinado nem notarizado (Gatekeeper e SmartScreen avisam no primeiro uso):
pendência do ADR 0112 que depende de o dono obter uma identidade de assinatura.
Uso
brabo-runner --project <projectId> --dir <caminho-absoluto> --token brb_... [--api-url <url>] [--base <pasta>]
Todas as flags são opcionais quando a pasta corrente tem
brabo-runner.config.json/brabo-runner-device-key.jwk.json; a flag explícita
sempre vence o arquivo local.
--project: o id do projeto, comexecution_mode=runner. Só um runner por projeto no cluster (Engine.Runners.Registry): um segundo é recusado no join. Sem--project, a CLI só segue como agente de máquina (abaixo).--dir: a pasta absoluta do código do projeto nesta máquina, raiz doexece do PTY. Sem ela, vale a pasta corrente. Se não existir, é criada (mkdir -p); se apontar para um arquivo, é erro (RN-435). No Linux, só é aceita dentro do$HOME, conferido antes da criação (RN-434). A navegação de pastas não fica presa a ela, de propósito (src/fs-browser.ts).--token: um Personal Access Token (brb_...), gerado em Configurações do projeto -> Tokens de acesso, ou a variávelBRABO_ACCOUNT_TOKEN. Nunca é gravado em disco. Sem token, autentica pela chave de dispositivo local (JWT EdDSA curto, assinado a cada tentativa de conexão).--api-url: flag ->BRABO_API_URL->apiUrldobrabo-runner.config.json->http://localhost:3000.--base: a base de projetos desta máquina, sob a qual cada projeto é uma subpasta (ADR 0151, RN-529). Sem a flag, é lida de$XDG_CONFIG_HOME/brabo/runner.json(senão~/.config/brabo/runner.json), onde o instalador a grava. Sem flag e sem arquivo, o runner roda sem base.
Base e --dir são coisas diferentes
--dir é a raiz deste projeto; a base é onde nasce a pasta de um projeto
novo. Um projeto fora da base continua válido: a base é regra de
criação, e a validação do --dir não a consulta. A base é local e
nunca chega pela rede; o servidor manda só o segmento relativo (ADR
0130/0144). A base é recusada, com motivo nomeado, quando é relativa, tem ..,
é /, é um arquivo, está fora do $HOME no Linux ou fica dentro da pasta deste
projeto (ou é igual a ela). Recusa vinda da flag encerra com código 2; vinda
do arquivo, vai para o stderr e o runner segue sem base.
Com base consentida, o runner declara a capacidade workspace no join e
atende workspace_create (RN-532): mkdir -p e git init, ou clone quando o
pedido traz URL de repositório (a credencial vai em env e o clone roda no
host, ADR 0145). No sucesso, ele empurra o workspace_confirm, a única
mensagem que grava workspace_verified_at. Os erros são nomeados (sem-base,
segmento, nao-e-pasta, mkdir, git). O estado.dir não muda: trocar
a raiz de contenção do guard.ts por uma mensagem de rede seria mover uma
fronteira.
A credencial de git não entra no container (RN-558, RN-676)
Com o container do projeto de pé, todo comando vai por docker exec, e a porta
de Docker não tem campo env, de propósito (ADR 0130). Por isso:
- o
git fetchautenticado, marcado pelo engine comgitCredenciado: truee comenvnão vazio, roda no host, na mesma pasta (é o bind mount de/work) (ADR 0193); - um
envsem a marca, com container ativo, é recusado antes de executar qualquer coisa, com uma saída que diz o que houve e que nada foi executado, sem citar nomes nem valores das variáveis.
Agente de MÁQUINA: sem --project, uma conexão por projeto (RN-544)
Com uma chave de dispositivo de máquina (ADR 0154, RN-543) e uma base
consentida, o runner roda sem --project: consulta GET /runner/projects e
abre uma conexão por projeto listado, cada uma em <base>/<workspaceDirName>.
# a base vem do instalador (~/.config/brabo/runner.json) ou de --base
brabo-runner --api-url https://brabo.exemplo
- Com ao menos uma conexão viva, a lista só é consultada na partida; projeto novo entra quando o agente reconecta.
- Lista vazia é normal e o agente FICA DE PÉ (RN-550): reconsulta a cada 15s, 30s e depois 60s, para quando chega a primeira lista não vazia e imprime um batimento a cada 30 consultas vazias. Dez falhas seguidas da consulta encerram com 1.
- Lista não vazia com todos os projetos recusados encerra com 1.
- Teto de tentativas e recusa de join são por projeto: o recusado termina nomeado e os outros seguem. Todo log leva o nome do projeto como prefixo.
- Credencial presa a um projeto é recusada com 403, e a CLI repassa a mensagem
nomeando a correção (
--project).--api-urlvem da flag, deBRABO_API_URLou do padrão; obrabo-runner.config.jsoné por projeto e não entra.
Importar chaves de API locais (credentials import)
Subcomando INTERATIVO (AT-611, RN-927): sem TTY, sai com 2 sem ler nada. Acha as
chaves de API de LLM que Claude Code, Codex, OpenCode, Kilo e Hermes já guardam,
lista cada uma (ferramenta, origem, provider do Brabo, 4 últimos dígitos) e
pergunta, item a item, se importa. Cada sim vira um POST /runner/llm-credentials
com a chave de dispositivo de MÁQUINA (PAT e chave de projeto levam 403); no 409,
pergunta se substitui. Login por assinatura só é listado. O valor da chave nunca
é impresso.
brabo-runner credentials import
Chave de dispositivo pelo terminal (device-key create | finish)
O par Ed25519 nasce nesta máquina, sem navegador (ADR 0155, RN-551); é assim que o instalador pareia o agente. A metade privada nunca viaja.
# 1) gera o par e imprime a JWK PÚBLICA (uma linha, no stdout)
PUB=$(brabo-runner device-key create)
# 2) quem tem a credencial (o instalador) registra a pública na api
ID=$(… POST {"name": "…", "publicKeyJwk": '"$PUB"'} … | jq -r .id)
# 3) carimba o `kid` (o id do registro) e grava a chave de verdade
brabo-runner device-key finish --id "$ID"
- São dois passos por causa do
kid, que só existe depois do registro (RN-475). Ocreategrava embrabo-runner-device-key.jwk.json.parcial, nome que o runner não procura. - Fica em
$XDG_CONFIG_HOME/brabo/(senão~/.config/brabo/);--dir <pasta>muda o destino. Chave privada em modo 600, pasta criada em 700. - stdout é para scripts, stderr é para gente.
- Sobrescrever uma chave completa é recusado, sem
--force: revogue a atual, apague o arquivo e recomece docreate.
Serviço de usuário (service install | uninstall | status)
Para o runner subir com a sua sessão: systemd --user no Linux, LaunchAgent no
macOS (ADR 0147, RN-518).
# de dentro de uma pasta com brabo-runner.config.json
brabo-runner service install
brabo-runner service status
brabo-runner service uninstall
# ou o agente desta MÁQUINA (atende todos os seus projetos em modo runner)
brabo-runner service install --machine
brabo-runner service status --machine
brabo-runner service uninstall --machine
- Sempre nível de usuário.
service installcomo root é recusado. Windows fica fora, por decisão: os três subcomandos recusam nomeando a plataforma. - Dois tipos de unit, que coexistem no disco (ADR 0154, RN-545): a de
projeto (
brabo-runner-<projectId>.service/dev.brabo.runner.<projectId>) e a de máquina (brabo-runner.service/dev.brabo.runner).--machineé opt-in explícito; com--project, é recusa nomeada. install --machineexige base consentida e recusa uma pasta combrabo-runner.config.json.installrecusa quando o OUTRO tipo já está instalado, sem--force.- A credencial é só a chave de dispositivo;
--token/BRABO_ACCOUNT_TOKENsão ignorados para não irem parar num arquivo de unit. uninstallremove a unit, obrabo-runner.config.jsone abrabo-runner-device-key.jwk.json(a pasta vem da própria unit; sem unit, passe--dir). A chave não é revogada no servidor. Sem tipo nomeado,uninstallrecusa e lista o que existe.statusresponde um de quatro estados: não instalado (4), rodando (0), parado (3) e gerenciador sem resposta (5).- O
PATH(e oXDG_CONFIG_HOME, quando definido) é congelado na unit: se mudou, rodeservice installde novo. Unit instalada por versão antiga comWorkingDirectory=entre aspas, ou com%/espaço noPATHou noXDG_CONFIG_HOME(AT-095), também se resolve reinstalando. Confira o valor que o systemd entendeu comsystemctl --user show -p Environment brabo-runner.service.
Reconexão
Quando a conexão cai, o runner pede um ticket NOVO e tenta de novo com
espera de 1s, 2s, 5s, 10s, 30s e teto de 10 tentativas seguidas sem sucesso.
Recusa de join (ticket inválido, outro runner no projeto) não é transitória:
encerra na hora, com a mensagem.
O auto-reconnect da lib phoenix fica desligado (reconnectAfterMs): o
ticket é de uso único (RN-108) e o reconnect da lib repete o ticket já
consumido. Uma versão antiga saiu com ele ligado e gerava recusa a cada ~5s e
429 na tela web do dono da conta. Se aparecer 429 sem explicação, confira se
o binário do runner está atualizado.
macOS e o spawn-helper do node-pty (AT-114, AT-342)
O tarball do node-pty@1.1.0 traz prebuilds/darwin-<arch>/spawn-helper sem o
bit de execução, e sem ele o terminal falha com posix_spawnp failed. Sob
Node, o runner confere e corrige esse bit ao carregar o node-pty
(src/spawn-helper.ts); se não conseguir, PARA com um erro que nomeia o caminho
e a correção (chmod +x "<caminho>"). O binário standalone já extrai o
spawn-helper com 0755 (native-pty-loader.ts).
Sob Bun, o tty.ReadStream que o node-pty usa morre no primeiro EAGAIN.
O runner troca esse leitor, só durante o spawn, por um leitor próprio do mesmo
fd que espera e tenta de novo (leitor-de-pty.ts). O --self-test-pty prova
isso com duas voltas e uma pausa entre elas.
Segurança
A fronteira de segurança do runner não é sandbox: é a composição de três
coisas, a autenticação (o token ou a chave de dispositivo da sua conta), o
pipeline de aprovação de sempre (todo comando de agente nasce como ação
proposta, sujeita à política do projeto) e o seu consentimento em rodar este
binário na sua máquina, com os seus privilégios. guard.ts e
espelho-guard.ts são best-effort por desenho.
Desenvolvimento
Exige Node.js 22.6 ou mais novo e pnpm (versão em packageManager). Branches
permanentes dev e main; tag só por workflow.
# precisa de ~/.npmrc com //npm.pkg.github.com/:_authToken=<PAT classic read:packages>
pnpm install
pnpm start -- --project <projectId> --dir <caminho-absoluto> --token brb_...
pnpm lint # oxlint --deny-warnings em src/ e scripts/
pnpm typecheck
pnpm test
pnpm build # dist/index.cjs (o que vai para o npm)
pnpm smoke # exercita o dist/ empacotado
Esses são os passos do ci.yml. O binário standalone exige
Bun e só compila para a plataforma CORRENTE:
pnpm build:bin
pnpm smoke:bin
Mais
As decisões de arquitetura do runner local estão nos ADRs do brabo:
0103
(runner local),
0104
(modo de execução),
0105
(token),
0106
(distribuição pelo npm),
0107
(navegação de pastas) e
0112
(binário standalone).
Licença
MIT. Os avisos de software de terceiros, com a oferta escrita para as
bibliotecas LGPL do runtime do Bun, estão em
THIRD_PARTY_NOTICES.md.