Pular para o conteúdo principal

Configuração

Toda a configuração é por variável de ambiente. Não há arquivo de configuração da aplicação — o que existe é o permissions.json, que é política do projeto, não configuração de processo.

Dois arquivos versionados são lidos em tempo de execução e não são configuração, embora seja fácil confundi-los com ela: o permissions.json acima e o docs/gates.yml (ADR 0054), o registro declarativo de gates. Nenhum dos dois tem variável de ambiente que aponte um caminho — o registro é achado subindo a partir de __dirname — e nenhum muda comportamento ao ser editado em produção: o registro DESCREVE os gates, não os aplica. Ele viaja dentro da imagem da api; veja o runbook.

O que viaja é o registro e nada daquilo para que ele aponta. A evidencia de um gate teste/ci nomeia arquivos sob apps/api/test/, scripts/ci/ e .github/, e nenhum deles está na imagem. Conferir que esses alvos existem é, portanto, uma afirmação sobre o repositório, cobrada no CI, nunca pelo processo que serve GET /gates — por isso a api lê o registro sem procurá-los. Ela já procurou, e respondia 500 em toda instalação (RN-070).

up --wait só prova o que tem healthcheck. No compose de desenvolvimento, api, engine e web não tinham nenhum, então docker compose up --wait os dava como prontos no instante em que o container subia — antes de o processo escutar. Foi assim que um container podia morrer segundos depois com o comando ainda anunciando sucesso. Os três agora carregam a mesma checagem que as imagens de produção já declaram no Dockerfile (/health na api e no engine, que toca o banco; / no servidor do Vite). O broker de containers ganhou a dele numa mudança à parte; postgres e neo4j sempre tiveram. Nada aqui é variável de ambiente — está escrito porque o --wait é aquilo em que os scripts confiam, e um serviço sem healthcheck o faz mentir.

Os defaults abaixo foram extraídos do código, não de documentação anterior. A coluna quando falha é a parte que economiza tempo: quase toda variável tem um default que funciona em desenvolvimento e um modo de falha específico em produção.

Uma variável só chega ao processo se o compose a mapear. O docker compose não repassa o ambiente do host para dentro do container: a variável chega se — e somente se — estiver escrita no environment: daquele serviço (ou num env_file). Isso não é detalhe; foi assim que ANAMNESE_ENABLED e PSYCHOLOGIST_ENABLED ficaram inalcançáveis em silêncio enquanto três lugares do código prometiam que a pausa de 2026-08-10 se revertia com X=true e um reinício. Medidas dentro do container do engine em execução, voltaram vazias e o runtime.exs caiu em "false" — sem erro em lugar nenhum. Toda flag booleana que o engine lê está mapeada agora nos três arquivos de compose (dev, produção e instalação — o de instalação só desde a AT-202), com o default do próprio código, salvo as duas chaves de boot dos agentes de fundo que produção e instalação desligam de propósito, e scripts/ci/flags-do-engine-no-compose.spec.ts reprova o build para a próxima que não estiver (RN-540). O Kubernetes fica de fora dessa regra de propósito: um Deployment/ConfigMap não intercepta nada, então ali não há chave quebrada a consertar.

Os defaults de desenvolvimento são inseguros de propósito. Valores como dev-master-key-change-me existem para que o pnpm dev suba sem cerimônia. Em produção eles precisam ser trocados — e para seis deles o processo recusa subir sem a troca (api ou engine, marcados com 🔒). Cinco seguem o padrão do ADR 0059/RN-093: ausente, igual ao literal público de exemplo ou curto demais derruba o boot. Veja a RN-114 para as quatro que se juntaram ao GIT_OAUTH_STATE_SECRET original.

Compose de instalação (docker-compose.install.yml)​

O compose que roda numa máquina instalada pega as cinco imagens do produto de variáveis obrigatórias, sem default — o install.sh as escreve no .env:

variávelo quê
BRABO_API_IMAGEreferência completa da imagem da api; ghcr.io/…@sha256:… quando a origem é o GHCR, brabo-api:prod quando construída localmente
BRABO_ENGINE_IMAGEo mesmo, para o engine
BRABO_WEB_IMAGEo mesmo, para o web
BRABO_BACKUP_IMAGEo mesmo, para o job de backup (sob o profile backup)
BRABO_BROKER_IMAGEo mesmo, para o broker de containers (ADR 0162). Escrita esteja ou não o broker ligado: o Compose interpola o arquivo inteiro antes de filtrar por profile, então um ${VAR:?…} num serviço desligado ainda recusa o arquivo (medido)

Elas são ${VAR:?…} de propósito. Com default, uma variável ausente subiria metade da stack numa imagem que ninguém escolheu, e o erro apareceria como comportamento estranho em vez de recusa.

Elas não estão no inventário gerado abaixo: o gerador varre apps/, e estas são lidas pelo Compose, não por código do produto (RN-527).

O broker está neste compose desde o ADR 0162, desligado, sob o profile container-broker, e o install.sh o liga só quando pergunta e a resposta é "s" (RN-575). Então ele escreve quatro linhas, juntas ou nenhuma:

variávelo que o install.sh escreve
COMPOSE_PROFILEScontainer-broker — lida pelo Compose a partir do --env-file, então up -d --wait sobe o broker sem flag
BROKER_URLhttp://broker:8090 — o lado da api; sem ela a api nunca chama o broker
DOCKER_GIDo gid do socket visto de dentro de um container, medido com a própria imagem do broker; falhar na medição é recusa nomeada, nunca o default 999
PROJECT_WORKSPACES_HOST_ROOT<DockerRootDir>/volumes/brabo_project_workspaces/_data, calculado antes da subida e conferido contra o Mountpoint real do volume depois dela

BRABO_PROJECTS_HOST_BASE não é escrita: o compose a deriva de BRABO_PROJECTS_BASE, como no compose de validação. Ligar ou desligar depois está no runbook.

Os modelos locais (Ollama, o profile llm) são perguntados logo depois do broker (RN-691). Responder "s" mede antes uma porta livre do host e escreve:

variávelo que o install.sh escreve
OLLAMA_PORT11434, ou a primeira porta livre até 11443 quando algo (um Ollama nativo) já escuta ali; a faixa inteira ocupada é recusa nomeada, sem escrever nada
COMPOSE_PROFILESllm, junto com container-broker numa linha SÓ quando os dois foram aceitos (container-broker,llm) — duas linhas não somam, a última vence

Sem elas a checagem de duplicata semântica é pulada. Ligar depois está no runbook.

api​

Essenciais​

variáveldefaultquando falha
DATABASE_URLpostgres://brabo:brabo@localhost:5432/brabosem ela, nada sobe
PORT3000—
API_JSON_BODY_LIMIT10mbteto do corpo JSON que a api aceita, aplicado ao parser registrado em main.ts antes de qualquer rota. Existe para que uma mudança no teto de transporte do engine (TRANSPORT_MAX_BODY_BYTES, 8 MiB) não exija deploy novo — o default guarda folga sobre ele. Pequeno demais, e um turno de contexto grande volta como 413
NODE_ENV—production liga o CORS estrito e a validação das chaves
API_PUBLIC_URLhttp://localhost:3000usada nos callbacks de OAuth de git; errada = callback quebrado
ENGINE_URLhttp://localhost:4000 no código, http://engine:4000 no Composeos comandos síncronos api→engine falham. Deixe-a vazia no .env: definida ali, ela vence o default do Compose e a api tenta falar com localhost:4000 de dentro do próprio container — toda ativação de sessão morre com ECONNREFUSED e o frontend não sai do lugar. Cada ambiente já tem o default certo sem a linha
ENGINE_PUBLIC_URLo mesmo que ENGINE_URLusada só para montar o engineWsUrl (WebSocket) devolvido em POST .../runner-ticket/.../terminal-ticket — o runner e o web podem estar FORA do cluster, então o endereço interno (http://engine:4000) não serve; ENGINE_URL continua sendo a usada nas chamadas síncronas de sempre api→engine (RN-419)
BRABO_VERSIONdevvira service.version no resource do OpenTelemetry — é como se sabe que build gerou um trace. A imagem de release injeta a tag por um ARG do docker-bake.hcl; fora de release fica dev. Não aparece em /health, que de propósito não devolve a versão (veja a description da rota)
MIGRATIONS_FOLDER./src/db/migrations—

Segurança 🔒​

variáveldefaultquando falha
CREDENTIALS_MASTER_KEY 🔒dev-master-key-change-me só fora de produçãoembrulha as DEKs. Em produção a api recusa subir se ela falta, se é o default acima (público — está no .env.example) ou se tem menos de 16 caracteres (RN-114). Essa é só a checagem do BOOT: trocá-la por uma chave válida mas diferente sem re-embrulhar ainda deixa toda credencial ilegível sem erro nenhum — a falha aparece no primeiro uso. Veja a rotação
CREDENTIALS_MASTER_KEY_PREVIOUS—só durante a rotação. Presente = a api tenta a chave anterior quando a atual falha
GIT_OAUTH_STATE_SECRET 🔒dev-oauth-state-secret-change-me só fora de produçãoassina o state do OAuth; fraca = CSRF no fluxo de conexão de git. Em produção a api recusa subir sem ela, com o default acima (que é público — está no .env.example) ou com menos de 16 caracteres. Gere com openssl rand -base64 32. Veja o ADR 0059 e a RN-093
WEB_ORIGIN 🔒http://localhost:${WEB_PORT}em produção a api recusa subir se ela falta ou é *. O CORS é estrito por ambiente. A porta faz parte do valor: o web em :5174 é outra origem e é bloqueado — veja o ADR 0037. Nos composes, o default deriva de WEB_PORT, então mudar a porta leva o CORS junto; definir WEB_ORIGIN à mão sobrepõe a derivação e mantê-la coerente passa a ser responsabilidade sua
WEB_PORT5173 (dev) · 8088 (prod)porta publicada do web no host. Nenhum serviço a lê — ela alimenta o default de WEB_ORIGIN nos composes, e é isso que impede porta e CORS de divergirem

Auth first-party​

A auth mora no domínio da api, e desde o corte é também a única emissora. Decisões no ADR 0031 e no ADR 0032.

variáveldefaulto que faz
AUTH_JWT_SECRET 🔒dev-auth-jwt-secret-change-me só fora de produçãofrase de onde o par Ed25519 do access token é derivado por scrypt — nenhuma chave privada é commitada. Em produção a api recusa subir se ela falta, se é o default acima (público — está no .env.example) ou se tem menos de 16 caracteres (RN-114)
AUTH_JWT_SECRET_PREVIOUS—aceita só para verificação, durante a rotação; entra no JWKS e nunca assina
AUTH_TOKEN_PEPPER 🔒dev-auth-token-pepper-change-me só fora de produçãochave HMAC do hash dos tokens opacos (refresh, vínculos de conta, PATs) e da chave do balde de bloqueio. Segredo próprio — não cai mais em AUTH_JWT_SECRET (RN-613). Em produção a api recusa subir se ele falta, se é qualquer dos valores de exemplo deste repositório ou se tem menos de 16 caracteres. Instalação que rodou sem ele precisa defini-lo com o AUTH_JWT_SECRET atual, senão todo mundo é deslogado — veja o runbook
AUTH_ACCESS_TOKEN_TTL_MS90000015 min
AUTH_REFRESH_TOKEN_TTL_MS120960000014 dias
AUTH_REFRESH_ABSOLUTE_TTL_MS2592000000teto absoluto da família, contado do login — sem ele a rotação concede sessão eterna
AUTH_REGISTRATION_ENABLEDtruequalquer valor diferente de "false" mantém o cadastro aberto
AUTH_LOCKOUT_ENABLEDtruemesma convenção
AUTH_LOCKOUT_WINDOW_MS900000janela deslizante da contagem
AUTH_LOCKOUT_THRESHOLDS5:30,8:300,12:900escada do balde por email, falhas:segundos
AUTH_LOCKOUT_IP_THRESHOLDS20:30,30:120escada do balde por IP, mais permissiva e com teto curto
AUTH_EMAIL_TOKEN_TTL_MS172800000verificação de email, 48 h
AUTH_RESET_TOKEN_TTL_MS3600000redefinição de senha, 1 h
AUTH_SET_PASSWORD_TTL_MS604800000definição da primeira senha (usuário migrado), 7 dias — mais longo que a redefinição porque quem recebe não pediu
AUTH_IP_ATTEMPT_THRESHOLD60teto de tentativas por IP nas rotas de auth
AUTH_MAIL_LOG_TOKENSfalsesó dev: imprime no log o token de verificação/redefinição

O teto da escada por email é igual à janela de propósito. Com janela deslizante, quem insiste empurra a janela e fica bloqueado enquanto insistir; quem parou volta com a janela limpa. Um teto maior que a janela criaria um bloqueio que a janela não consegue representar, e exigiria uma coluna locked_until persistente com fila de desbloqueio. Não mexa num sem o outro.

Rotacionar o AUTH_TOKEN_PEPPER desloga todo mundo e invalida os tokens de verificação e de redefinição pendentes. Diferente das chaves, o pepper não tem _PREVIOUS. Veja o runbook.

Até onde as variáveis _PREVIOUS chegam. AUTH_JWT_SECRET_PREVIOUS, CREDENTIALS_MASTER_KEY_PREVIOUS e BRABO_SERVICE_TOKEN_PREVIOUS estão mapeadas no environment: dos três composes (dev, produção e instalação) como ${X:-} — a api recebe as três, o engine e o broker recebem BRABO_SERVICE_TOKEN_PREVIOUS (RN-595). Vazio é o mesmo que ausente em todo leitor, então fora de uma rotação nada muda; durante uma, basta defini-las no .env e recriar o serviço. Antes da RN-595 elas não estavam mapeadas em lugar nenhum e o docker compose não repassa o ambiente do host, então uma rotação virava troca seca em silêncio. No Kubernetes elas chegam aos Pods pelo cofre de segredos (AT-220): o ExternalSecret puxa o objeto brabo inteiro com dataFrom.extract, então um _PREVIOUS presente ali cai em brabo-secrets e um ausente simplesmente não está lá. O preço é que toda chave desse objeto vira variável de ambiente de api, engine, migrations e backup — o objeto do cofre não pode guardar mais nada. O procedimento está no runbook.

SMTP real (MailSender)​

O MailSender só manda email de verdade com MAIL_TRANSPORT=smtp — o default é log (link/token vão para o log da api, AUTH_MAIL_LOG_TOKENS acima), inclusive em produção: mandar email é opção do operador. Decisão no ADR 0096.

variáveldefaulto que faz
MAIL_TRANSPORTloglog (default) ou smtp. Qualquer outro valor cai em log
SMTP_HOST 🔒—host do provedor SMTP. Só com MAIL_TRANSPORT=smtp: em produção a api recusa subir se ele falta, se é só espaço ou se é o valor de exemplo publicado no .env.example (RN-114)
SMTP_PORT587porta do provedor — 587 é STARTTLS, 465 é TLS implícito (SMTP_SECURE=true)
SMTP_SECUREfalsetrue liga TLS implícito na conexão (tipicamente a porta 465)
SMTP_USER 🔒—usuário da auth SMTP. Mesma exigência de SMTP_HOST em produção
SMTP_PASSWORD 🔒—senha/token da auth SMTP. Mesma exigência de SMTP_HOST em produção — nunca aparece em log
SMTP_FROM—remetente, no formato "Nome <email@dominio>" (ou só o email). Mesma exigência de SMTP_HOST em produção, mais a validação do formato

O corpo do email é texto puro, sem HTML — a porta MailSender não carrega estrutura para corpo rico, e um motor de template seria superfície de injeção sem ganho. O link usa WEB_ORIGIN (acima).

Seed de desenvolvimento​

Consumidas por pnpm --filter api seed, não pela api em execução. Sem IdP externo, é daqui que vem a credencial para entrar no web local e no smoke.

variáveldefaulto que faz
BRABO_SEED_PASSWORDbrabo12345678senha dos usuários semeados (owner@brabo.dev, dev@brabo.dev), criados com o email já verificado
BRABO_FORCE_SEED—destrava o seed com NODE_ENV=production, onde por padrão ele recusa rodar. Não a defina num ambiente real: a conta nasce com senha conhecida e verificada

O seed é idempotente e não mexe na senha de quem já existe. Rodá-lo de novo depois de alguém trocar a própria senha não a reverte.

Rate limit​

Janela deslizante no Postgres — não há Redis (ADR 0027).

variáveldefaulto que faz
RATE_LIMIT_ENABLEDtruequalquer valor diferente de "false" o mantém ligado
RATE_LIMIT_WINDOW_MS60000tamanho da janela
RATE_LIMIT_USER300requisições por usuário por janela
RATE_LIMIT_IP600requisições por IP por janela

Se a tabela do rate limit está indisponível, a requisição passa. O guard protege contra abuso, não contra acesso não autorizado — esse é o trabalho do guard de autenticação, que roda antes.

Tráfego interno 🔒​

O segredo compartilhado que autentica api ↔ engine. A mesma variável dos dois lados — cada um envia o valor atual e aceita os dois, e é isso que torna a rotação possível sem indisponibilidade (RN-035).

variáveldefaulto que faz
BRABO_SERVICE_TOKEN 🔒dev-service-token-change-me só fora de produçãovai no cabeçalho X-Brabo-Service-Token e é o que o EngineServiceGuard compara em tempo constante. Em produção a api recusa subir se ele falta, se é o default acima (público — está no .env.example) ou se tem menos de 16 caracteres (RN-114)
BRABO_SERVICE_TOKEN_PREVIOUS—aceito só para verificação, durante a rotação. Quando definido, passa pela mesma regra do token atual: o espaço em volta é aparado, e em produção a api recusa subir se ele é o default público ou tem menos de 16 caracteres. Diferente do atual, nunca é obrigatório (RN-598)

Definir só o valor NOVO de um lado (sem passar pela dança do _PREVIOUS) não quebra o boot de ninguém: o sintoma é 403 em /internal/* e 401 nas chamadas da api ao engine. Procedimento no runbook. A checagem de BOOT acima (RN-114) é outra coisa: ela só falha com o default público ou com a variável ausente/curta, não com divergência entre os dois lados.

Git​

variáveldefaultnota
GIT_LOCAL_REPOS_ROOT/tmp/brabo-git-reposprovider local. Em /tmp, os repositórios somem no reboot
PROJECT_WORKSPACES_ROOT/tmp/brabo-project-workspacesworktrees dos agentes de projeto no modo Container. Precisa ser o mesmo caminho no engine, e o mesmo volume
BRABO_PROJECTS_BASEvazioa pasta única do host sob a qual moram os projetos no modo Mounted, montada por identidade em api e engine. Vazio é estado NORMAL: a api informa projectsBase: null e o assistente de projeto nem oferece o modo Mounted
GITHUB_OAUTH_CLIENT_ID / _SECRETvaziovazio = conexão OAuth com o GitHub indisponível (o PAT continua funcionando)
GITLAB_OAUTH_CLIENT_ID / _SECRETvazioidem

Projeto no modo Mounted: uma base, montada por identidade​

Desde o ADR 0072, um projeto pode nascer no modo Mounted (o antigo local, renomeado pelo ADR 0104) — o código mora numa pasta do usuário e PROJECT_WORKSPACES_ROOT não participa da raiz dele.

O caminho de um projeto é dado do projeto (projects.workspace_path), escolhido na criação e nunca variável de ambiente. O que o AMBIENTE fornece é a base sob a qual essas pastas moram — BRABO_PROJECTS_BASE, uma por instalação (ADR 0141, RN-500):

# .env — ABSOLUTO. `~` não é expandido pelo Compose.
BRABO_PROJECTS_BASE=/home/voce/projetos-brabo

Você não precisa escrever essa linha à mão. Desde o ADR 0146, pnpm bootstrap → Docker › Base de projetos roda scripts/dev/consentir-base.mjs, que propõe $HOME/projetos-brabo, recusa o que se sobreponha ao checkout do Brabo ou a PROJECT_WORKSPACES_HOST_DIR (duas raízes com donos opostos — compartilhar uma pasta faria um projeto Mounted chamado loja cair em cima de um projeto Container cujo workspace_dir_name também é loja), prova no macOS e no Windows que o Docker enxerga mesmo a pasta montando-a, e só então escreve a variável.

A prova é uma montagem, nunca a leitura do settings.json do Docker Desktop: esse arquivo não é documentado, muda entre versões e plataformas, e descreve o que o usuário configurou, não o que o daemon vai fazer — a mesma regra que o produto já aplica aos providers de LLM, onde capability só é declarada quando provada por execução. Ela tem três desfechos e eles não colapsam: provado, recusado (a variável não é escrita) e não deu para provar — sem imagem local para montar — que escreve a variável e diz em voz alta que a prova não rodou.

Rodado pelo menu, ele só informa: todo item do menu do bootstrap roda com stdin de /dev/null de propósito, então não pode perguntar. Para escolher, rode node scripts/dev/consentir-base.mjs no seu próprio terminal.

# docker/docker-compose.yml — em `api` E `engine`, já escrito
volumes:
- ${BRABO_PROJECTS_BASE:-brabo_projects_base}:${BRABO_PROJECTS_BASE:-/data/brabo-projects-base}

O operador define isso uma vez e reinicia api + engine; nada é editado por projeto, nunca. Antes disso, todo projeto montado precisava de uma linha de bind-mount escrita à mão nos dois serviços e de um reinício — que mata todo turno de agente, socket de terminal e chamada de LLM em voo na instalação.

A montagem é por identidade ($X:$X): o mesmo caminho absoluto no host e dentro dos dois containers. É isso que mantém honesta a string que o usuário digita e a tela mostra de volta, e é por isso que projectScopeRoot (api) e Engine.Actions.Workspace.workspace_dir/2 (engine) não precisam de camada de tradução.

Ela não é PROJECT_WORKSPACES_HOST_DIR, e nunca pode apontar para a mesma pasta. Um workspace gerenciado é nomeado por workspace_dir_name (UNIQUE) e um projeto montado é nomeado pelo usuário, então <base>/loja e um projeto container cuja pasta se chama loja colidiriam, com o bootstrap do repositório rodando git init dentro do projeto de outra pessoa. O ADR tem os outros dois motivos.

Com a variável não definida, o modo Mounted não é oferecido — nunca ofereça um modo que a instalação não consegue honrar. E o pnpm dev recusa subir quando a base se sobrepõe ao checkout do Brabo em qualquer sentido; essa checagem mora no preflight porque ele roda no host, e a api só consegue comparar com process.cwd() (/workspace dentro do próprio container). Veja o runbook.

LLM​

variáveldefaultnota
OLLAMA_HOSThttp://localhost:11434—
NINE_ROUTER_BASE_URLhttp://host.docker.internal:20128/v1URL base do 9Router (provider 9router, ADR 0222), que roda na máquina do dono, fora do compose. É da INSTALAÇÃO, não do workspace: o padrão só alcança o host porque os composes de instalação e de produção mapeiam host.docker.internal:host-gateway na api. Vale para as chamadas de chat, o catálogo e o teste de conexão da credencial
OLLAMA_REQUEST_TIMEOUT_MS300000teto de inatividade do socket do Ollama, não de duração total. Um modelo local tem latência até o primeiro token de outra ordem de grandeza, daí a variável própria; veja o ambiente de inferência
LLM_REQUEST_TIMEOUT_MS300000o mesmo teto de inatividade para os providers de API (OpenAI e compatíveis, Anthropic). Vale para "nem mandou os cabeçalhos" e para "parou de mandar pedaços no meio do stream" — veja providers de LLM
HUGGINGFACE_API_TOKEN(não definida)token opcional do Hugging Face Hub para a busca de modelos (GET /workspaces/:workspaceId/huggingface/models). Presente, vai como Authorization: Bearer … e sobe o rate limit do Hub; ausente, o cabeçalho simplesmente não é enviado — o Hub aceita busca pública sem credencial, e token ausente nunca é erro
HUGGINGFACE_HUB_URLhttps://huggingface.coURL base do Hub com que a busca fala. Sobreposição para testes (um servidor HTTP falso numa porta efêmera); em produção fica sem definir
HUGGINGFACE_REQUEST_TIMEOUT_MS15000teto de inatividade da requisição de busca ao Hub, mesma semântica de LLM_REQUEST_TIMEOUT_MS (o nome é montado numa constante, então o inventário gerado abaixo não o vê)

TODO(humano): nenhum arquivo de compose mapeia as três variáveis HUGGINGFACE_* no environment: do serviço api, e o serviço não tem env_file, então defini-las no .env não tem efeito sob o Compose — HUGGINGFACE_API_TOKEN deve ser repassada (ao menos em produção e na instalação)?

Grafo de conhecimento (ADR 0099)​

variáveldefaultnota
NEO4J_URI—ex.: bolt://neo4j:7687. Esta é a chave: vazia = grafo DESLIGADO, as rotas dependentes degradam (GraphUnavailableError/503) — ninguém precisa de um Neo4j local só para rodar a suíte. Em produção, a ausência de qualquer uma das três derruba o boot. Ligar o grafo em desenvolvimento é esta linha no .env e um reinício da api; até 2026-09-04 isso não fazia nada, porque o compose de dev não passava as três variáveis ao serviço api e o serviço não tem env_file — o docker-compose.prod.yml as fornecia desde o primeiro dia, por isso só o desenvolvimento era afetado
NEO4J_USERneo4j (dev)chega ao container com o mesmo default que o serviço neo4j usa para NEO4J_AUTH, então defina-a só para TROCAR o usuário — trocar ali troca os dois lados de uma vez. Dois defaults independentes deixariam a api autenticando com uma credencial que o servidor não tem mais
NEO4J_PASSWORD 🔒dev-neo4j-password-change-me (só dev)mesma regra de pareamento de NEO4J_USER. Sem default público em produção de propósito — não há "valor de exemplo" plausível para senha de banco, e o docker-compose.prod.yml mantém NEO4J_AUTH vazio para que o próprio entrypoint da imagem oficial recuse subir em vez de subir com uma senha adivinhável
GRAPH_PROJECTOR_INTERVAL_MS2000período do poller que esvazia a fila graph_projection da outbox e escreve handoffs/hipóteses/perfis/interações no grafo (RN-416)
CUSTO_RECONCILIACAO_INTERVAL_MS300000período do job da api que troca a ESTIMATIVA de custo de uma chamada abortada pelo orçamento de raciocínio pelo custo real do GET /generation do OpenRouter (RN-915); 0 desliga
ARTIFACT_PROJECTOR_INTERVAL_MS2000período do poller que esvazia a fila artifact_projection da outbox e escreve os artefatos de cada agente como Markdown sob a pasta docs/ do projeto (RN-523). Projeção DERIVADA: o event log continua sendo a fonte, e a pasta pode ser apagada e reconstruída

Observabilidade​

variáveldefaultnota
OTEL_EXPORTER_OTLP_ENDPOINT—ausente desliga a EXPORTAÇÃO, não a instrumentação (ADR 0035). O span continua sendo criado e o trace_id continua aparecendo no log — é isso que dá correlação em desenvolvimento, sem coletor. Sem ela, o span é descartado no fim em vez de sair do processo
OTEL_SERVICE_NAMEbrabo-api—
OTEL_DIAG_LOG—1 liga o log de diagnóstico do próprio OTel
LOG_LEVELinfo em produção, debug foradecide também o FORMATO junto com NODE_ENV: fora de produção o log sai legível (pino-pretty no processo, com a árvore de camadas); em produção, uma linha JSON por evento
METRICS_GAUGE_INTERVAL_MS15000período de coleta dos gauges de domínio

engine​

Essenciais​

variáveldefaultquando falha
DATABASE_URLecto://brabo:brabo@localhost:5432/brabonote o esquema ecto://, não postgres://
POSTGRES_HOST / _USER / _PASSWORDlocalhost / brabo / brabousadas quando o DATABASE_URL não é montado
POOL_SIZE—um pool esgotado trava o Oban e a fila para de ser consumida
PORT4000—
PHX_HOST / PHX_SERVER—PHX_SERVER=true é o que faz a release servir HTTP
SECRET_KEY_BASE 🔒—obrigatória na release (runtime.exs, bloco :prod, o raise padrão do Phoenix). Até a RN-114, o docker-compose.prod.yml fornecia um literal público como fallback e mascarava esse raise — a variável sempre chegava DEFINIDA. O raise em si não mudou
API_URLhttp://localhost:3000o engine chama a api de volta por aqui
ECTO_IPV6——
SKIP_MIGRATIONS—usada pelo Job de migration

Cluster e desligamento​

variáveldefaultnota
DNS_CLUSTER_QUERY—o Service headless que forma o cluster Erlang. Sem ela cada réplica é uma ilha e todo rollout drena tudo
SHUTDOWN_DRAIN_TIMEOUT_MS45000janela do preStop. Sobe junto com terminationGracePeriodSeconds, nunca sozinha
SESSION_HEARTBEAT_TIMEOUT_MS30000quanto a sessão espera pelo heartbeat da aba antes de perguntar à api se há trabalho pendente
SESSION_CONVERSATION_IDLE_TIMEOUT_MS28800000 (8h)teto de uma conversa ociosa: um agente conversacional esperando o usuário segura a sessão além do heartbeat por esse tempo, contado do fim do turno dele; passado isso a sessão fecha como closed com a causa conversation_idle_timeout (RN-581). Mapeada para o engine nos três composes (dev, produção, instalação) com esse mesmo default; não escrita em deploy/k8s/, de propósito — nada ali intercepta o ambiente, então a ausência é o default, e mudá-la é um item env: no patch do engine do overlay
RELEASE_NAME / RELEASE_NODE—identidade do nó na distribuição

Harness​

variáveldefaultnota
TOOL_LOOP_MAX_ITERATIONS8teto de voltas do laço do agente conversacional. Esgotado, o agente termina com um artefato de bloqueio
TOOL_LOOP_MAX_ITERATIONS_EXECUCAO60teto dos dev agents. Maior porque eles exploram o repositório antes de escrever — e porque o task_budget_micros segura o gasto por baixo
TOOL_LOOP_MAX_ITERATIONS_GATE60teto dos subagentes de QA, pelo mesmo motivo
DEFAULT_CONTEXT_WINDOW128000usado quando o modelo não declara a janela (os agentes conversacionais nunca declaram); era 8192 até a RN-702
CONTEXT_COMPACTION_THRESHOLD0.7fração da janela que dispara a compactação
LLM_TURN_TIMEOUT_MS3000005 min por turno
TOOL_ROUTER_TIMEOUT_MS2000teto, em ms, de UMA chamada ao roteador de ferramentas Jev (ADR 0179, RN-625). Lida pela api, não pelo engine. Passado o teto, o passo cai no catálogo inteiro de ferramentas com motivoDaQueda: timeout; o turno nunca falha por causa do Jev. 0 de 328 requisições medidas passou de 2 000 ms. A chave liga/desliga é por workspace (PUT workspaces/:id/tool-router), não variável de ambiente
TERMINAL_ACTION_TIMEOUT_MS120000teto de um comando de terminal (2 min desde a RN-757: um npm install simples não cabia nos 15 s antigos). O engine espera esse valor + 90 s no container-exec e + 120 s no propose_action de uma ação terminal; acima de ~180 s a espera de fora passa do limite de 300 s de cabeçalhos do fetch do Node que a api usa para chamar /internal/actions/execute
TERMINAL_OUTPUT_MAX_BYTES32768teto em BYTES da saída de um comando (RN-074). A saída fica no histórico do laço e viaja em todo turno seguinte; sem teto, um find numa árvore grande derruba a execução inteira com um 413 do provider
READ_FILE_MAX_BYTES32768teto em BYTES do conteúdo lido por read_file (RN-141) — a mesma classe de estouro da RN-074, pela porta do read_file em vez do terminal; variável independente, mesmo valor por coincidência de contexto
SEARCH_WORKSPACE_MAX_BYTES32768teto em BYTES do texto final de search_workspace (RN-150) — a mesma classe de estouro da RN-074/RN-141, pela porta da busca; variável independente
SEARCH_WORKSPACE_MAX_HITS500teto do NÚMERO de ocorrências que search_workspace coleta antes de montar a resposta (RN-150) — para de varrer/ler conteúdo assim que bate o teto, evitando pagar I/O numa árvore com ocorrências demais só para truncar por bytes depois
SECOPS_SCAN_TIMEOUT_MS1800003 min para o scanner do SecOps
TRANSPORT_MAX_BODY_BYTES8388608 (8 MiB)teto de TRANSPORTE que a compactação de contexto respeita por cima da janela do modelo (RN-412) — a janela efetiva é min(context_window, este teto convertido em tokens), então a compactação dispara ANTES de o corpo estourar o limite HTTP da api, não só quando o modelo "esqueceria"
GRAPH_INSTRUCTION_TEMPLATES_ENABLEDfalseliga a fonte :graph de InstructionFiles — hoje só a identidade do ux-designer resolve um template do grafo antes do texto inline (RN-413). Nome PRÓPRIO, não o GRAPH_TEMPLATES_ENABLED abaixo — as duas colidiriam com defaults opostos se dividissem a chave
variáveldefaultnota
PSYCHOLOGIST_ENABLEDfalsepausa GLOBAL de rodadas NOVAS (automáticas e sob demanda) — decisão de produto do usuário em 2026-08-10, não bug, mesma forma de flag de ANAMNESE_ENABLED abaixo (que foi religada na RN-680; esta não). Não apaga nada que já exista. Ligá-la exige reiniciar o engine (RN-117). Nenhuma chave de boot faz par com ela: o gatilho automático do Psicólogo é o fechamento da sessão, não um tick
PSYCHOLOGIST_TRIAGE_THRESHOLD20eventos na sessão que separam a análise leve da pesada
PSYCHOLOGIST_MAX_ITERATIONS_LEVE / _PESADA4 / 8—
PSYCHOLOGIST_BUDGET_MICROS_LEVE / _PESADA50000 / 300000USD 0,05 e USD 0,30 por análise
PSYCHOLOGIST_MAX_PROMPT_EVENTS_LEVE / _PESADA50 / 400quantos eventos entram no prompt
PSYCHOLOGIST_MAX_PAYLOAD_CHARS600truncamento do payload de cada evento
PSYCHOLOGIST_RAG_TOP_K3quantos trechos relevantes do rag_search entram no contexto, descontados do teto de eventos recentes acima (RN-417)
GRAPH_TEMPLATES_ENABLEDfalseliga a resolução de psychologist-kickoff/anamnese-kickoff/context-manager-summarize como template do grafo — chave COMPARTILHADA entre Psicólogo, Anamnese e o prompt de sumarização do ContextManager (RN-417), não confundir com GRAPH_INSTRUCTION_TEMPLATES_ENABLED acima. Desligada (o default) quer dizer que a API do grafo nem é chamada; ligada, qualquer falha cai no texto inline sem erro

Anamnese​

variáveldefaultnota
ANAMNESE_ENABLEDtrueflag de produto: pode acontecer uma rodada NOVA (periódica e sob demanda). false é uma pausa GLOBAL que não apaga nada (RN-115); mudá-la exige reiniciar o engine. Foi false de 2026-08-10 (a pausa do usuário) até o dono religar a Anamnese com correções em 2026-10-01 (RN-680): rodada sem sujeito elegível — membro efetivo do projeto com interação própria na janela — não faz chamada de LLM. Sozinha não basta para a rodada PERIÓDICA: START_ANAMNESE (chave de boot, abaixo) também precisa ser true, e as duas respondem perguntas diferentes
ANAMNESE_INTERVAL_SECONDS90015 min entre rodadas
ANAMNESE_MIN_EVENTS10abaixo disso não roda — evita traçar perfil sobre ruído
ANAMNESE_INITIAL_WINDOW_DAYS30janela da primeira rodada
ANAMNESE_MAX_ITERATIONS6—
ANAMNESE_BUDGET_MICROS200000USD 0,20 por rodada
ANAMNESE_MAX_PROMPT_EVENTS500—
ANAMNESE_MAX_PAYLOAD_CHARS600—

Guardas de carga​

variáveldefaultnota
START_OUTBOX_DRAINtruechave de boot do drain da outbox (Engine.Workers.OutboxDrainWorker, a cada 2s) — o ÚNICO consumidor dos eventos session/task/container que a api escreve na outbox — mais a poda de worktree órfão e a adoção de sessão órfã. Desligada, ninguém entrega esses eventos: sessão que a api fechou continua com o processo e os agentes conversacionais rodando (RN-581), e dev agent esperando decisão de ação, gate, a PR dele ou o container chegar a running nunca acorda. Ela NÃO liga o Psicólogo: o drain só o enfileira com PSYCHOLOGIST_ENABLED=true. false só no compose de produção (docker-compose.prod.yml), de propósito; o compose de instalação usa o true do código desde a AT-219
START_ANAMNESEtrueguarda de CARGA de teste/dev: impede que kickoff/0 sequer seja chamado no boot, mas não decide nada de produto — não confundir com ANAMNESE_ENABLED (produto: pausa GLOBAL, sobrevive a qualquer valor desta). Desligá-la impede enfileiramentos novos, não limpa a fila. Os jobs acumulados rodam no próximo boot — a fila precisa ser expurgada. Veja o ambiente de inferência
START_MODEL_SYNCtruetick periódico da sincronização do catálogo de modelos. Desligá-lo não congela nada: o botão "Atualizar catálogo" da página Providers chama o mesmo caso de uso (RN-043)
MODEL_SYNC_INTERVAL_SECONDS21600 (6h)o catálogo de um provider muda em escala de dias, e cada rodada gasta uma chamada de API por provider — daí o default generoso
START_GATE_RESCUEtruetick periódico do resgate de ciclo de gate (Engine.Gates.GateRescuer, RN-140). Desligá-lo não muda o boot: o resgate roda ali uma vez de qualquer jeito
GATE_RESCUE_INTERVAL_SECONDS300 (5 min)um gate travado bloqueia a PR inteira do usuário — intervalo bem mais curto que o da Anamnese/sincronização de modelos, e cada tick custa só uma consulta quase sempre vazia
GATE_RESCUE_STALE_AFTER_SECONDS900 (15 min)generoso de propósito: o ToolLoop de um subagente de QA pode legitimamente rodar até TOOL_LOOP_MAX_ITERATIONS_GATE (60) iterações, e um limiar curto resgataria — e duplicaria — um ciclo apenas lento

Tráfego interno e observabilidade​

variáveldefault
BRABO_SERVICE_TOKEN 🔒dev-service-token-change-me só fora de produção — o mesmo valor da api. O espaço em volta é aparado, e um valor só de espaço conta como ausente. Numa release :prod o engine recusa subir se ele falta, se é o default público acima ou se tem menos de 16 caracteres, nomeando a variável — a mesma regra da api (RN-601)
BRABO_SERVICE_TOKEN_PREVIOUS 🔒— aceito só para verificação, durante a rotação. Aparado; numa release :prod o engine recusa subir se ele é o default público ou tem menos de 16 caracteres. Nunca obrigatório, e igual ao atual quer dizer que não há rotação (RN-601)
OTEL_EXPORTER_OTLP_ENDPOINT— o exportador Elixir fala HTTP/protobuf na 4318, não gRPC na 4317. Ausente desliga só a exportação (traces_exporter: :none), não a instrumentação — veja o ADR 0035
WEB_ORIGIN— a mesma variável da api, e aqui ela alimenta DUAS coisas: o check_origin do socket Phoenix (o painel do time ao vivo) e o CORS HTTP das rotas de saúde, que o navegador precisa para ler /health (ADR 0037). Ausente em produção fecha o CORS e mantém o check_origin no default estrito do Phoenix — o engine sobe mesmo assim, diferente da api
PROJECT_WORKSPACES_ROOT/tmp/brabo-project-workspaces — a mesma da api, no mesmo volume

SOME_APP_SSL_CERT_PATH, SOME_APP_SSL_KEY_PATH e MIX_TEST_PARTITION são sobras do scaffold do Phoenix e da configuração de teste. Não as configure.


web​

O web é estático, servido pelo nginx. Ele lê a configuração de duas fontes, e a distinção importa:

fontequandocomo
import.meta.env.VITE_*buildembutida no bundle. Mudar exige rebuild
window.__BRABO_CONFIG__runtimeservida em /config.js, gerado pelo entrypoint do container

Por isso a mesma imagem serve todo ambiente: o /config.js é reescrito no boot. As VITE_* são o fallback de desenvolvimento.

variávelserve para
VITE_API_URLendereço da api
VITE_ENGINE_URLendereço do engine (canal Phoenix)
VITE_LOG_LEVELnível do logger JSON do navegador (default info). Num cluster manda a chave WEB_LOG_LEVEL do brabo-config, que o entrypoint escreve no /config.js — VITE_* só conta num build local
VITE_BRABO_VERSIONversão mostrada no rodapé das telas de auth (default dev). A única que é de build por escolha, não por limitação — veja abaixo

Página em branco depois do deploy é quase sempre o /config.js apontando para localhost — o smoke do deploy confere exatamente isso.

Por que a versão não passa pelo /config.js​

URLs são do ambiente: a mesma imagem precisa falar com a api de staging e com a de produção, e é para isso que o /config.js existe (ADR 0024). A versão é do artefato — a imagem brabo-web:1.1.2 não deveria conseguir informar outra coisa. Se viesse do /config.js, o rodapé viraria um campo editável em vez de uma identidade, e um ConfigMap errado faria a tela mentir sobre qual build está rodando.

O caminho inteiro, do commit à tela: o release.yml calcula versao=${TAG#v} → passa como VERSION ao docker buildx bake → o alvo web do docker-bake.hcl a converte em VITE_BRABO_VERSION → o ARG/ENV do docker/web/Dockerfile.prod a expõe ao pnpm build → o Vite a embute em import.meta.env → o runtime-config.ts a lê → o AuthLayout a mostra. O mesmo VERSION alimenta o BRABO_VERSION da imagem da api (ADR 0036).


Backup​

Consumidas pela imagem de backup, não pelas apps — como CronJob no Kubernetes e como docker compose run --rm backup … numa instalação por compose. Detalhes em Restauração; o raciocínio está no ADR 0152.

O destino é um de dois, e a escolha é BACKUP_DIR. Defina-a e o backup vai para disco; deixe-a vazia e ele vai para o S3, que é o que o CronJob do Kubernetes faz. As cinco variáveis BACKUP_S3_* deixaram de ser obrigatórias incondicionalmente: elas são a configuração de um destino, e exigir um bucket antes de poder migrar exige uma infraestrutura que uma instalação de máquina única nunca pediu.

variáveldefaultnota
BACKUP_DIR— (S3)diretório de destino. Definida ⇒ disco; vazia ⇒ S3. No serviço do compose o default é /backups
BRABO_BACKUP_HOST_DIRbackup_local (volume nomeado)caminho do host montado em /backups. O default de volume nomeado serve para verificar um backup; é o destino errado para migrar, porque docker compose down -v o apaga junto com o que ele devia salvar
BACKUP_S3_ENDPOINT / BACKUP_S3_BUCKET—destino compatível com S3; obrigatórias só quando BACKUP_DIR está vazia
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY—credencial do bucket
BACKUP_KEEP_DAILY7retenção por contagem, não por idade
BACKUP_KEEP_WEEKLY4—
GIT_REPOS_DIR/data/git-reposos repositórios bare dos projetos do provider local. Ausente ⇒ o passo é pulado e dito (o CronJob do Kubernetes não monta esse volume)
RESTORE_DB—nome do banco de destino da restauração
RESTORE_PREFIXdaily/weekly/ para restaurar de uma cópia semanal
RESTORE_GIT_PREFIXgit-daily/git-weekly/ para a cópia semanal dos repositórios bare
RESTORE_GIT_FORCE—1 permite extrair por cima de repositórios bare existentes. Desligada por padrão: sobrepor dois estados de repositório produz uma mistura de que nenhum git reclama
RESTORE_ADMIN_URL—conexão com permissão CREATEDB; em produção é separada do DATABASE_URL

Quatro prefixos, não dois: daily/ e weekly/ guardam os dumps do Postgres, git-daily/ e git-weekly/ os arquivos dos repositórios bare. Ficam separados porque a retenção é por contagem — duas espécies de arquivo sob um prefixo fariam "guardar 7" significar três backups e meio.

Inferência local (containers)​

Estas não são lidas pelo nosso código — são do container ollama, e estão aqui porque são a causa mais frequente de um agente se comportar de forma estranha. A tabela de sintomas está no ambiente de inferência.

variávelpor quê
OLLAMA_CONTEXT_LENGTHo default de 4096 trunca em silêncio um prompt montado para 128k
OLLAMA_MAX_LOADED_MODELScom OLLAMA_KEEP_ALIVE alto, os modelos se acumulam até a memória estourar
OLLAMA_KEEP_ALIVEquanto tempo o modelo fica residente
DEMO_QA_MODELaponta o gate de QA para um modelo de API — o binding por agente vence o do projeto

A versão da imagem ollama não é variável — é digest. Como toda imagem de terceiro em docker/, ela é pinada como ollama/ollama:0.40.6@sha256:… (ADR 0159, tag inline desde o ADR 0178), e o mesmo digest é usado por ci.yml/golden-set-rag.yml, porque o piso do golden-set do RAG é chaveado pelo modelo e não pelo ambiente (ADR 0138). Então um comportamento de inferência que mudou entre dois docker compose pull deixou de ser causa possível: nada se move a não ser que alguém edite o digest. Subir a versão é um procedimento do runbook, e o scripts/ci/imagens-pinadas.ts recusa uma subida que alcance só parte dos lugares onde a tag aparece.


Observabilidade local (containers)​

Também não lidas pelo nosso código: são as portas do overlay docker/docker-compose.observability.yml, que sobe Prometheus, Loki e Grafana ao lado da stack de desenvolvimento (pnpm dev:obs). O mecanismo está em observabilidade local.

variáveldefaultpor quê
GRAFANA_PORT3001a mesma porta que o Grafana do cluster usa; os dois não coexistem, pelo mesmo motivo de pnpm dev e make deploy-local não coexistirem
PROMETHEUS_PORT9090a porta que o runbook já usa no kubectl port-forward do cluster
LOKI_PORT3100só para consultar direto; o caminho normal é pelo Grafana

O overlay não define OTEL_EXPORTER_OTLP_ENDPOINT: sem um Collector no meio, apontar as apps para um endereço que não existe só produz um erro de exportação a cada turno (ADR 0035 separou instrumentar de exportar exatamente por isso). Métricas e logs funcionam sem ela; traces ainda exigem o cluster.


Broker de containers​

O broker (ADR 0130) é o único processo do produto que fala com um daemon Docker no servidor, e o único serviço com /var/run/docker.sock montado. Localmente ele sobe com o pnpm dev; em produção fica sob profiles: ["container-broker"]. Os dois arquivos de compose divergem de propósito (ADR 0146, ponto 3) — não os uniformize.

Ele já foi desligado por padrão nos dois, sob o argumento de que nada o chamava ainda. Essa premissa morreu: ele tem quatro chamadores reais, e o ADR 0144 fez o modo Mounted subir o container pelo broker — então, com o profile desligado, o modo local padrão terminava em BrokerIndisponivelError. O que sustenta a assimetria é o que o socket significa de cada lado: quem roda pnpm dev já o tem (está rodando docker compose), enquanto em produção ele é uma fronteira real de privilégio e o operador não é o desenvolvedor.

Isso muda quando o broker roda, nunca o que ele aceita: as cinco camadas de contenção estão intactas, e ele continua recebendo um projectId mais uma de cinco operações, sem parâmetro em lugar nenhum onde escrever privileged ou um -v livre.

Subir por padrão foi o que expôs um defeito que nunca tinha importado: a imagem local instalava as dependências no START, e a única rede do broker é internal: true, então não há registry a alcançar — o container morria com getaddrinfo EAI_AGAIN registry.npmjs.org, e sem healthcheck o up --wait ainda imprimia Healthy. As dependências agora se instalam no BUILD e o serviço tem healthcheck; a rede não foi tocada, e não deve ser. O detalhe operacional mora no runbook.

Em produção, suba-o explicitamente:

docker compose -f docker/docker-compose.prod.yml \
--profile container-broker up -d broker
variáveldefaultquando falha
BROKER_URLhttp://broker:8090 no compose de dev; vazia em produção e na instalação (lida pela api)Vazia é estado NORMAL: quem lê o estado observado de um container diz então "não observado" em vez de herdar o registrado (RN-486), e a api responde brokerConfigurado: false, então a criação de projeto só oferece runner (RN-573). Localmente o broker sobe por padrão, então o compose de dev a aponta por default para o serviço que ele sobe (RN-599) — um valor no .env ainda vence. Os composes de produção e de instalação não têm default de propósito: ali o broker fica sob profile, e quem o liga escreve a URL
BROKER_PORT8090Porta em que o broker escuta. Ele NÃO publica nada no host — só a api o alcança, pela rede internal: true do compose
BRABO_SERVICE_TOKEN 🔒dev-service-token-change-me em desenvolvimentoO MESMO segredo de api ↔ engine, no mesmo cabeçalho. Com NODE_ENV=production o broker recusa subir quando ele está vazio, quando é o literal público do repositório ou com menos de 16 caracteres — a regra da RN-114, que aqui guarda um processo que fala com o Docker do host. BRABO_SERVICE_TOKEN_PREVIOUS, quando definido, passa pela mesma regra salvo a obrigatoriedade: aparado, e em produção o broker recusa subir se ele é o literal público ou tem menos de 16 caracteres (RN-601)
API_URLhttp://api:3000De onde o broker LÊ a decisão do Arquiteto. Ele não recebe uma especificação de container; ele vem buscá-la (RN-485)
PROJECT_WORKSPACES_HOST_ROOT—A raiz das pastas de projeto no HOST, não dentro de container nenhum. Sem ela, start recusa nomeando esta variável e as outras quatro operações continuam funcionando. Não a confunda com PROJECT_WORKSPACES_ROOT, que é o caminho dentro dos containers: o -v é resolvido pelo DAEMON contra o sistema de arquivos do host, e um caminho de dentro da api o faria criar e montar uma pasta VAZIA
DOCKER_GID999 (compose)O gid do grupo docker do host (getent group docker | cut -d: -f3). O socket é root:docker e o broker roda sem root, então o compose usa group_add. O default é o mais comum e está errado em várias distribuições. Desde o ADR 0146 isso importa em TODA máquina de desenvolvimento, não só onde alguém ligou o profile — errar não quebra o boot, quebra o uso: toda operação morre com "permission denied" no socket, e isso só aparece quando alguém propõe container_start. O pnpm dev informa o estado desta variável a cada execução (RN-512), e diz "não se aplica" numa máquina sem grupo docker em vez de acusá-la

PROJECT_WORKSPACES_HOST_ROOT não pode ser derivada de um volume Docker gerenciado. No compose de dev ela deriva de PROJECT_WORKSPACES_HOST_DIR (acima) — então deixar essa sem definir, o estado de um .env.example recém copiado, deixa o broker sem raiz e o modo container sem container: a stack sobe saudável e o container_start termina recusado. O pnpm dev informa isso a cada execução, no mesmo molde do DOCKER_GID, e nunca recusa subir (RN-599). Ele informa também o caso do ~: o Compose expande ~ na origem do bind-mount que api/engine usam, mas NÃO na variável que o broker recebe (medido com docker compose config), então escreva o caminho JÁ EXPANDIDO. E um PROJECT_WORKSPACES_HOST_ROOT explícito que difere da pasta que api/engine montam é informado como divergente — o container receberia uma pasta em que os agentes nunca escreveram. Trocar o volume gerenciado por uma pasta do host não migra o que já está no volume project_workspaces: os dados ficam lá, só deixam de ser o que api e engine enxergam.

BRABO_PROJECTS_HOST_BASE é a SEGUNDA raiz do broker — a base dos projetos Mounted, também no host (ADR 0141, ADR 0144). Diferente da de cima, ela não precisa ser preenchida: o compose a deriva de BRABO_PROJECTS_BASE, que já é um caminho do host por definição (é o que api e engine montam por identidade). Defina-a explicitamente só se o daemon Docker alcança essa pasta por outro caminho.

É ela que permite a um projeto Mounted ter container (RN-503): a api envia um localizador discriminado (localizacao.tipo — gerenciada ou montada) mais o segmento relativo, e o broker o resolve contra a raiz correspondente. As duas raízes nunca se substituem. Faltar esta faz o start de um projeto mounted recusar com 503, NOMEANDO a variável, sem tocar em container — cair em PROJECT_WORKSPACES_HOST_ROOT montaria a pasta de outro projeto, porque a raiz gerenciada é nomeada por workspace_dir_name e a base é nomeada pelo usuário.


Usuário dos containers de dev (build)​

Também não lidas pelo nosso código, e nem são variável de ambiente de runtime: são build.args do Docker para as três imagens de dev (docker/api/Dockerfile, docker/web/Dockerfile, docker/engine/Dockerfile), consultados uma vez no docker compose build e embutidos no USER com que cada container roda. Existem para que os containers rodem com o mesmo UID/GID do seu usuário do host em vez de root — tudo o que é escrito num bind mount (apps/api/dist, um workspace de projeto no modo de execução mounted) já é seu, sem sudo chown depois.

variáveldefaultpor quê
DEV_UID1000o seu id -u. NÃO é lido do ${UID} do shell — ele é somente leitura no bash e não é exportado para o ambiente por padrão, então o arquivo de compose sempre o veria vazio
DEV_GID1000o seu id -g, mesmo raciocínio

1000/1000 casa com a configuração Linux mais comum de um desenvolvedor só e com o usuário node que o node:26-alpine (a imagem base de api/web) já traz — quando o seu par casa, os Dockerfiles reaproveitam esse usuário em vez de criar um novo. Um ambiente que já tem containers rodando como root de antes dessa mudança precisa de um chown único dos volumes nomeados node_modules/_build/deps/.mix/.hex (ou de docker compose down -v para deixá-los ser recriados) — veja a nota em Primeiros passos.

Os composes de produção e de instalação são diferentes: o engine roda da imagem de produção como o usuário fixo engine (uid/gid 1000, sem DEV_UID), com read_only: true e um tmpfs em /home/engine. Esse tmpfs é declarado uid=1000,gid=1000,mode=0700 — não é variável — porque o semgrep (o gate de SecOps) escreve ~/.semgrep ali; criado como root, o scan não devolve nada e o gate aprova sem análise estática (RN-707, runbook O gate de SecOps está preso em "SAST não rodou").

As duas imagens do engine pinam o semgrep com o argumento de build SEMGREP_VERSION (1.183.0, o mesmo em docker/engine/Dockerfile e Dockerfile.prod, mantidos iguais por scripts/ci/semgrep-do-engine.spec.ts). É argumento de build, não variável de runtime — nada vai no .env. Sem pin, o pip pegou a 1.179.0, que não publica wheel musllinux: na imagem Alpine o pacote vinha do sdist sem semgrep-core e todo scan falhava. Subir a versão é escolher uma release com wheel musllinux no PyPI e mudar os dois Dockerfiles no mesmo PR.

As imagens do engine rodam Elixir 1.20.4 / OTP 29.1.1 sobre Alpine 3.24 (builder hexpm/elixir:1.20.4-erlang-29.1.1-alpine-3.24.2, runtime alpine:3.24.3, as duas por digest — ADR 0209). OTP abaixo de 29.1 aborta no boot sob a musl 1.2.6 do Alpine 3.24 em hosts cuja CPU tem AMX (sys_sigaltstack(): Internal error); não desça o OTP sem descer a musl. Nada vai no .env.

Nenhum dos seis Dockerfiles usa corepack (o Node 25+ não o traz mais, AT-360): o build instala o pnpm do tarball do registry, conferido contra um sha512 escrito no próprio Dockerfile — os argumentos de build PNPM_VERSION (11.3.0, o mesmo do packageManager em package.json, para o pnpm nunca tentar baixar outra versão em runtime) e PNPM_SHA512. São argumentos de build, não variáveis de runtime, e não há nada a definir no .env: subir o pnpm é mudar o packageManager, o PNPM_VERSION dos workflows e os dois argumentos nos seis Dockerfiles no mesmo PR — veja a seção do runbook sobre a imagem do broker.


Variáveis de ferramenta (não do produto)​

Tudo acima é produto: o que quem opera uma instalação define no .env do deploy. O que segue é ferramenta — lido só pelo CI e por quem desenvolve o Brabo. Nada disso pertence ao .env de um operador, e essa distinção é o motivo de o inventário abaixo marcar cada fonte.

Scripts de demonstração e seed — apps/api/scripts/​

Eles conduzem agentes reais contra um Ollama local, então o que recebem é um nome de modelo e um prazo. Todos têm default e nenhum é obrigatório.

variáveldefaulto que muda
DEMO_MODELqwen2.5-coder:7bo modelo que todo script de demonstração usa, a não ser que um dos abaixo o sobreponha para um papel específico
DEMO_QA_MODELDEMO_MODELsó o agente de QA, nas demonstrações dos gates de PR — para julgar com um modelo diferente do que escreveu o código
DEMO_WORKFLOWS_MODELDEMO_MODELsó o agente de Infra na demonstração de workflows do GitHub
DEMO_MODEL_LEVEqwen2.5-coder:7bo modelo leve do Psicólogo (o que a passada barata usa)
DEMO_MODEL_PESADOqwen-psicologo-pesado:latesto modelo pesado do Psicólogo. É uma variante de Modelfile local, não uma tag publicada — daí a de baixo
DEMO_OLLAMA_COPY_FROMqwen2.5-coder:7ba base de que a demonstração copia quando precisa criar o modelo pesado no Ollama local
DEMO_TIMEOUT_MS1800000 (30 min); 900000 na demonstração de dev agentquanto o script espera a execução terminar antes de desistir
DEMO_ANAMNESE_TIMEOUT_MS900000 (15 min)o mesmo prazo, para a demonstração da Anamnese
DEMO_PSICOLOGO_TIMEOUT_MS600000 (10 min)o mesmo prazo, para a demonstração do Psicólogo
GOLDEN_SET_QA_MODELqwen2.5-coder:latesto modelo que seed-golden-set-qa.ts registra como juiz do golden-set de QA (ADR 0123)

NOME, que o inventário abaixo lista sob esta fonte, não é variável: é o literal process.env.NOME dentro de uma string de prompt em demo-pr-gates.ts, ensinando o agente a substituir um segredo fixo no código. O inventário extrai por padrão e não tem como distinguir uma menção de uma leitura — ele está listado aqui em vez de filtrado na fonte, porque uma lista de ignorados nome a nome é um segundo lugar a manter em dia.

E2E de navegador — e2e/​

e2e/ não é membro do workspace (ADR 0120) e roda contra o compose de produção, por isso os endereços dele são as portas publicadas e não as de dev. As credenciais são as da conta do seed — as mesmas que o docker/smoke.sh cria. O inventário varre todo .ts sob e2e/, inclusive os specs do Playwright — ali o spec é o código que lê o ambiente, não um teste unitário ao lado dele (AT-124).

variáveldefaulto que muda
E2E_API_URLhttp://localhost:3000onde o suporte HTTP da suíte bate na api
E2E_WEB_URLhttp://localhost:8088o baseURL que o Playwright abre, e a origem de que depende a asserção de CSRF
E2E_USERowner@brabo.deva conta com que a suíte entra
E2E_PASSWORDa senha do seed fixa em e2e/suporte/api.tsa senha dessa conta. É a senha de um seed local, nunca uma real — não é repetida aqui para que a única cópia continue sendo a do código

Cluster local e provas agendadas — deploy/k8s/​

O bootstrap (make deploy-local) e as provas de propriedade que o .github/workflows/propriedades.yml roda toda semana leem estas do ambiente. Todas têm default; nenhuma pertence ao .env de uma instalação. O inventário varre todo deploy/k8s/*.sh e casa só a expansão com default (${X:-…}), que é como um script de shell lê uma variável de fora (AT-212).

variáveldefaulto que muda
BRABO_CLUSTER_NAMEbraboo nome do cluster k3d/kind que o bootstrap cria ou reaproveita
BRABO_CLUSTER_TOOLk3dkind troca o bootstrap para o kind — que ignora NetworkPolicy em silêncio, daí o k3d como default
BRABO_KEEP_CLUSTERnão definida1 reaproveita um cluster existente com o mesmo nome em vez de apagá-lo e recriá-lo
BRABO_SKIP_BUILDnão definida1 pula o build das imagens de produção e usa as que já estão no daemon Docker
BRABO_SKIP_OBSERVABILITYnão definida1 pula Tempo, Loki, Collector, Alloy e Grafana (Prometheus e o adapter ficam: o HPA do engine os lê). Só o propriedades.yml a define (AT-177)
TAGnão definidauma tag de release (vX.Y.Z-qa.N) para o bootstrap fazer checkout e deploy em vez da árvore de trabalho; exige árvore limpa
BRABO_NAMESPACEbraboo namespace com que todo script de prova fala
API_URL / ENGINE_URL / WEB_URLhttp://localhost:3000 / :4000 / :8088onde os scripts de prova alcançam os serviços do cluster — as portas que o load balancer do k3d publica. Mesmos nomes das variáveis de produto acima, outro leitor: aqui são só o alvo dos scripts
SMOKE_USERowner@brabo.deva conta com que o smoke e as provas entram
BRABO_SMOKE_PASSWORDa senha local do seed, fixa nos scriptsa senha dessa conta — também o que o bootstrap passa ao pod do seed como BRABO_SEED_PASSWORD
HPA_JOBS60quantos jobs de sonda o make hpa-test enfileira para fazer o engine escalar
HPA_TIMEOUT240segundos que o make hpa-test espera pelo scale-up
ROLLOUT_SESSIONS5quantas sessões o make rollout-test abre antes do rollout
ROLLOUT_CONVERGENCE_SECONDS120o teto para toda sessão ser adotada ou drenada depois do rollout. Não o suba para virar verde uma rodada vermelha (veja a lacuna da sessão órfã no CLAUDE.md)
ROLLOUT_EVIDENCE_DIRum mktemp -donde o make rollout-test escreve a evidência (logs dos pods, eventos, amostras de réplicas); o propriedades.yml a define e sobe a pasta
EVIDENCIA_INTERVALO2segundos entre as amostras dos coletores de evidência; o spec o encurta (AT-174)
RESTORE_MUTACAOnão definidatabela-faltando transforma o make test-restore no make test-restore-mutacao, a quebra deliberada que ele precisa pegar (AT-126)
RESTORE_KEEP_JOBnão definida1 mantém os Jobs de backup/restauração depois do make test-restore, para inspeção
PROJECT_WORKSPACES_ROOT_NO_POD/data/project-workspacesonde o make test-reprojecao-artefatos-k8s procura a pasta de artefatos dentro do pod da api
KUBE_VERSION1.31.0a versão do Kubernetes contra a qual o make k8s-validate valida os overlays renderizados

Inventário completo​

As tabelas acima explicam o que cada variável faz. Esta seção é o inventário: extraído do código a cada pnpm docs:generate, ele existe para que uma variável nova não fique sem documentação em lugar nenhum sem que ninguém perceba.

⚠️ Bloco gerado por pnpm docs:generate. Não edite à mão — o próximo build o sobrescreve.

Inventário extraído do código: 182 variáveis lidas em tempo de execução. Todas têm descrição nas tabelas acima.

Cada fonte é marcada produto — o que quem opera a instalação configura no .env do deploy — ou ferramenta, lida só pelo CI e por quem desenvolve o produto. Variável de ferramenta nunca pertence ao .env de um operador.

api — 67 variáveis · produto

  • API_JSON_BODY_LIMIT (apps/api/src/main.ts)
  • API_PUBLIC_URL (apps/api/src/application/use-cases/auth/start-social-login.use-case.ts)
  • ARTIFACT_PROJECTOR_INTERVAL_MS (apps/api/src/application/artifact-projection/artifact-projector.ts)
  • AUTH_ACCESS_TOKEN_TTL_MS (apps/api/src/infrastructure/security/ed25519-access-token-issuer.ts)
  • AUTH_EMAIL_TOKEN_TTL_MS (apps/api/src/application/use-cases/auth/auth-config.ts)
  • AUTH_IP_ATTEMPT_THRESHOLD (apps/api/src/application/use-cases/auth/auth-config.ts)
  • AUTH_JWT_SECRET (apps/api/src/infrastructure/security/auth-key-material.ts)
  • AUTH_JWT_SECRET_PREVIOUS (apps/api/src/infrastructure/security/auth-key-material.ts)
  • AUTH_LOCKOUT_ENABLED (apps/api/src/infrastructure/persistence/drizzle/drizzle-login-throttle.ts)
  • AUTH_LOCKOUT_IP_THRESHOLDS (apps/api/src/infrastructure/persistence/drizzle/drizzle-login-throttle.ts)
  • AUTH_LOCKOUT_THRESHOLDS (apps/api/src/infrastructure/persistence/drizzle/drizzle-login-throttle.ts)
  • AUTH_LOCKOUT_WINDOW_MS (apps/api/src/infrastructure/persistence/drizzle/drizzle-login-throttle.ts)
  • AUTH_MAIL_LOG_TOKENS (apps/api/src/infrastructure/mail/log-mail-sender.ts)
  • AUTH_REFRESH_ABSOLUTE_TTL_MS (apps/api/src/application/use-cases/auth/auth-config.ts)
  • AUTH_REFRESH_TOKEN_TTL_MS (apps/api/src/application/use-cases/auth/auth-config.ts)
  • AUTH_REGISTRATION_ENABLED (apps/api/src/application/use-cases/auth/auth-config.ts)
  • AUTH_RESET_TOKEN_TTL_MS (apps/api/src/application/use-cases/auth/auth-config.ts)
  • AUTH_SET_PASSWORD_TTL_MS (apps/api/src/application/use-cases/auth/auth-config.ts)
  • AUTH_TOKEN_PEPPER (apps/api/src/infrastructure/security/auth-key-material.ts)
  • BRABO_FORCE_SEED (apps/api/src/scripts/provisionar-usuario.ts)
  • BRABO_PROJECTS_BASE (apps/api/src/infrastructure/filesystem/project-workspaces-root.ts)
  • BRABO_SEED_PASSWORD (apps/api/src/db/seed.ts)
  • BRABO_SERVICE_TOKEN (apps/api/src/infrastructure/security/service-token.ts)
  • BRABO_SERVICE_TOKEN_PREVIOUS (apps/api/src/infrastructure/security/service-token.ts)
  • BRABO_VERSION (apps/api/src/tracing.ts)
  • BROKER_URL (apps/api/src/infrastructure/http-clients/container-broker.client.ts)
  • CREDENTIALS_MASTER_KEY (apps/api/src/infrastructure/security/envelope-encryption.service.ts)
  • CREDENTIALS_MASTER_KEY_PREVIOUS (apps/api/src/infrastructure/security/envelope-encryption.service.ts)
  • CUSTO_RECONCILIACAO_INTERVAL_MS (apps/api/src/application/use-cases/llm/reconciliar-custo-estimado.use-case.ts)
  • DATABASE_URL (apps/api/src/db/migrate.ts)
  • ENGINE_PUBLIC_URL (apps/api/src/application/use-cases/runner/request-runner-ticket.use-case.ts)
  • ENGINE_URL (apps/api/src/application/use-cases/runner/request-runner-ticket.use-case.ts)
  • GIT_LOCAL_REPOS_ROOT (apps/api/src/infrastructure/git/local-git-provider.ts)
  • GIT_OAUTH_STATE_SECRET (apps/api/src/infrastructure/security/oauth-state-secret.ts)
  • GITHUB_OAUTH_CLIENT_ID (apps/api/src/infrastructure/git/github-oauth-client.ts)
  • GITHUB_OAUTH_CLIENT_SECRET (apps/api/src/infrastructure/git/github-oauth-client.ts)
  • GITLAB_OAUTH_CLIENT_ID (apps/api/src/infrastructure/git/gitlab-oauth-client.ts)
  • GITLAB_OAUTH_CLIENT_SECRET (apps/api/src/infrastructure/git/gitlab-oauth-client.ts)
  • GRAPH_PROJECTOR_INTERVAL_MS (apps/api/src/application/graph-projection/graph-projector.ts)
  • HUGGINGFACE_API_TOKEN (apps/api/src/infrastructure/huggingface/huggingface-client.ts)
  • HUGGINGFACE_HUB_URL (apps/api/src/infrastructure/huggingface/huggingface-client.ts)
  • LOG_LEVEL (apps/api/src/infrastructure/observability/logger.config.ts)
  • MAIL_TRANSPORT (apps/api/src/infrastructure/mail/smtp-config.ts)
  • METRICS_GAUGE_INTERVAL_MS (apps/api/src/infrastructure/observability/domain-gauges.collector.ts)
  • MIGRATIONS_FOLDER (apps/api/src/db/migrate.ts)
  • NEO4J_PASSWORD (apps/api/src/infrastructure/graph/neo4j-config.ts)
  • NEO4J_URI (apps/api/src/infrastructure/graph/neo4j-config.ts)
  • NEO4J_USER (apps/api/src/infrastructure/graph/neo4j-config.ts)
  • NINE_ROUTER_BASE_URL (apps/api/src/infrastructure/llm/nine-router-provider.ts)
  • NODE_ENV (apps/api/src/infrastructure/graph/neo4j-config.ts)
  • OLLAMA_HOST (apps/api/src/infrastructure/llm/ollama-provider.ts)
  • OTEL_DIAG_LOG (apps/api/src/tracing.ts)
  • OTEL_EXPORTER_OTLP_ENDPOINT (apps/api/src/tracing.ts)
  • OTEL_SERVICE_NAME (apps/api/src/tracing.ts)
  • PORT (apps/api/src/main.ts)
  • PROJECT_WORKSPACES_ROOT (apps/api/src/infrastructure/filesystem/project-workspaces-root.ts)
  • RATE_LIMIT_ENABLED (apps/api/src/interfaces/http/shared/rate-limit.guard.ts)
  • RATE_LIMIT_IP (apps/api/src/interfaces/http/shared/rate-limit.guard.ts)
  • RATE_LIMIT_USER (apps/api/src/interfaces/http/shared/rate-limit.guard.ts)
  • RATE_LIMIT_WINDOW_MS (apps/api/src/infrastructure/observability/domain-gauges.collector.ts)
  • SMTP_FROM (apps/api/src/infrastructure/mail/smtp-config.ts)
  • SMTP_HOST (apps/api/src/infrastructure/mail/smtp-config.ts)
  • SMTP_PASSWORD (apps/api/src/infrastructure/mail/smtp-config.ts)
  • SMTP_PORT (apps/api/src/infrastructure/mail/smtp-config.ts)
  • SMTP_SECURE (apps/api/src/infrastructure/mail/smtp-config.ts)
  • SMTP_USER (apps/api/src/infrastructure/mail/smtp-config.ts)
  • WEB_ORIGIN (apps/api/src/infrastructure/mail/smtp-mail-sender.ts)

engine — 63 variáveis · produto

  • ANAMNESE_BUDGET_MICROS (apps/engine/config/runtime.exs)
  • ANAMNESE_ENABLED (apps/engine/config/runtime.exs)
  • ANAMNESE_INITIAL_WINDOW_DAYS (apps/engine/config/runtime.exs)
  • ANAMNESE_INTERVAL_SECONDS (apps/engine/config/runtime.exs)
  • ANAMNESE_MAX_ITERATIONS (apps/engine/config/runtime.exs)
  • ANAMNESE_MAX_PAYLOAD_CHARS (apps/engine/config/runtime.exs)
  • ANAMNESE_MAX_PROMPT_EVENTS (apps/engine/config/runtime.exs)
  • ANAMNESE_MIN_EVENTS (apps/engine/config/runtime.exs)
  • API_URL (apps/engine/config/runtime.exs)
  • BRABO_SERVICE_TOKEN (apps/engine/config/runtime.exs)
  • BRABO_SERVICE_TOKEN_PREVIOUS (apps/engine/config/runtime.exs)
  • CONTEXT_COMPACTION_THRESHOLD (apps/engine/config/runtime.exs)
  • DATABASE_URL (apps/engine/config/dev.exs)
  • DEFAULT_CONTEXT_WINDOW (apps/engine/config/runtime.exs)
  • DNS_CLUSTER_QUERY (apps/engine/config/runtime.exs)
  • ECTO_IPV6 (apps/engine/config/runtime.exs)
  • GATE_RESCUE_INTERVAL_SECONDS (apps/engine/config/runtime.exs)
  • GATE_RESCUE_STALE_AFTER_SECONDS (apps/engine/config/runtime.exs)
  • GRAPH_INSTRUCTION_TEMPLATES_ENABLED (apps/engine/config/runtime.exs)
  • GRAPH_TEMPLATES_ENABLED (apps/engine/config/runtime.exs)
  • LLM_TURN_TIMEOUT_MS (apps/engine/config/runtime.exs)
  • MIX_TEST_PARTITION (apps/engine/config/test.exs)
  • MODEL_SYNC_INTERVAL_SECONDS (apps/engine/config/runtime.exs)
  • OTEL_EXPORTER_OTLP_ENDPOINT (apps/engine/config/runtime.exs)
  • PHX_HOST (apps/engine/config/runtime.exs)
  • PHX_SERVER (apps/engine/config/runtime.exs)
  • POOL_SIZE (apps/engine/config/runtime.exs)
  • PORT (apps/engine/config/runtime.exs)
  • POSTGRES_HOST (apps/engine/config/test.exs)
  • POSTGRES_PASSWORD (apps/engine/config/test.exs)
  • POSTGRES_USER (apps/engine/config/test.exs)
  • PROJECT_WORKSPACES_ROOT (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_BUDGET_MICROS_LEVE (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_BUDGET_MICROS_PESADA (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_ENABLED (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_MAX_ITERATIONS_LEVE (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_MAX_ITERATIONS_PESADA (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_MAX_PAYLOAD_CHARS (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_MAX_PROMPT_EVENTS_LEVE (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_MAX_PROMPT_EVENTS_PESADA (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_RAG_TOP_K (apps/engine/config/runtime.exs)
  • PSYCHOLOGIST_TRIAGE_THRESHOLD (apps/engine/config/runtime.exs)
  • READ_FILE_MAX_BYTES (apps/engine/config/runtime.exs)
  • SEARCH_WORKSPACE_MAX_BYTES (apps/engine/config/runtime.exs)
  • SEARCH_WORKSPACE_MAX_HITS (apps/engine/config/runtime.exs)
  • SECOPS_SCAN_TIMEOUT_MS (apps/engine/config/runtime.exs)
  • SECRET_KEY_BASE (apps/engine/config/runtime.exs)
  • SESSION_CONVERSATION_IDLE_TIMEOUT_MS (apps/engine/config/runtime.exs)
  • SESSION_HEARTBEAT_TIMEOUT_MS (apps/engine/config/runtime.exs)
  • SHUTDOWN_DRAIN_TIMEOUT_MS (apps/engine/config/runtime.exs)
  • SOME_APP_SSL_CERT_PATH (apps/engine/config/runtime.exs)
  • SOME_APP_SSL_KEY_PATH (apps/engine/config/runtime.exs)
  • START_ANAMNESE (apps/engine/config/runtime.exs)
  • START_GATE_RESCUE (apps/engine/config/runtime.exs)
  • START_MODEL_SYNC (apps/engine/config/runtime.exs)
  • START_OUTBOX_DRAIN (apps/engine/config/runtime.exs)
  • TERMINAL_ACTION_TIMEOUT_MS (apps/engine/config/runtime.exs)
  • TERMINAL_OUTPUT_MAX_BYTES (apps/engine/config/runtime.exs)
  • TOOL_LOOP_MAX_ITERATIONS (apps/engine/config/runtime.exs)
  • TOOL_LOOP_MAX_ITERATIONS_EXECUCAO (apps/engine/config/runtime.exs)
  • TOOL_LOOP_MAX_ITERATIONS_GATE (apps/engine/config/runtime.exs)
  • TRANSPORT_MAX_BODY_BYTES (apps/engine/config/runtime.exs)
  • WEB_ORIGIN (apps/engine/config/runtime.exs)

web — 4 variáveis · produto

  • VITE_API_URL (apps/web/src/lib/runtime-config.ts)
  • VITE_BRABO_VERSION (apps/web/src/lib/runtime-config.ts)
  • VITE_ENGINE_URL (apps/web/src/lib/runtime-config.ts)
  • VITE_LOG_LEVEL (apps/web/src/lib/runtime-config.ts)

broker — 7 variáveis · produto

  • API_URL (apps/broker/src/config.ts)
  • BRABO_PROJECTS_HOST_BASE (apps/broker/src/config.ts)
  • BRABO_SERVICE_TOKEN (apps/broker/src/config.ts)
  • BRABO_SERVICE_TOKEN_PREVIOUS (apps/broker/src/config.ts)
  • BROKER_PORT (apps/broker/src/config.ts)
  • NODE_ENV (apps/broker/src/config.ts)
  • PROJECT_WORKSPACES_HOST_ROOT (apps/broker/src/config.ts)

api/scripts — 15 variáveis · ferramenta

  • DATABASE_URL (apps/api/scripts/demo-repo-bootstrap.ts)
  • DEMO_ANAMNESE_TIMEOUT_MS (apps/api/scripts/demo-anamnese.ts)
  • DEMO_MODEL (apps/api/scripts/demo-anamnese.ts)
  • DEMO_MODEL_LEVE (apps/api/scripts/demo-psicologo.ts)
  • DEMO_MODEL_PESADO (apps/api/scripts/demo-psicologo.ts)
  • DEMO_OLLAMA_COPY_FROM (apps/api/scripts/demo-psicologo.ts)
  • DEMO_PSICOLOGO_TIMEOUT_MS (apps/api/scripts/demo-psicologo.ts)
  • DEMO_QA_MODEL (apps/api/scripts/demo-pr-gates-area-qa.ts)
  • DEMO_TIMEOUT_MS (apps/api/scripts/demo-dev-agent-real.ts)
  • DEMO_WORKFLOWS_MODEL (apps/api/scripts/demo-infra-workflows-github.ts)
  • GIT_LOCAL_REPOS_ROOT (apps/api/scripts/demo-repo-bootstrap.ts)
  • GOLDEN_SET_QA_MODEL (apps/api/scripts/seed-golden-set-qa.ts)
  • NOME (apps/api/scripts/demo-pr-gates.ts)
  • OLLAMA_HOST (apps/api/scripts/demo-psicologo.ts)
  • RATE_LIMIT_WINDOW_MS (apps/api/scripts/relatorio-seguranca-runtime.ts)

e2e — 4 variáveis · ferramenta

  • E2E_API_URL (e2e/suporte/api.ts)
  • E2E_PASSWORD (e2e/suporte/api.ts)
  • E2E_USER (e2e/suporte/api.ts)
  • E2E_WEB_URL (e2e/playwright.config.ts)

deploy/k8s — 22 variáveis · ferramenta

  • API_URL (deploy/k8s/rollout-test.sh)
  • BRABO_CLUSTER_NAME (deploy/k8s/bootstrap.sh)
  • BRABO_CLUSTER_TOOL (deploy/k8s/bootstrap.sh)
  • BRABO_KEEP_CLUSTER (deploy/k8s/bootstrap.sh)
  • BRABO_NAMESPACE (deploy/k8s/hpa-test.sh)
  • BRABO_SKIP_BUILD (deploy/k8s/bootstrap.sh)
  • BRABO_SKIP_OBSERVABILITY (deploy/k8s/bootstrap.sh)
  • BRABO_SMOKE_PASSWORD (deploy/k8s/bootstrap.sh)
  • ENGINE_URL (deploy/k8s/smoke.sh)
  • EVIDENCIA_INTERVALO (deploy/k8s/rollout-evidencia.sh)
  • HPA_JOBS (deploy/k8s/hpa-test.sh)
  • HPA_TIMEOUT (deploy/k8s/hpa-test.sh)
  • KUBE_VERSION (deploy/k8s/validate.sh)
  • PROJECT_WORKSPACES_ROOT_NO_POD (deploy/k8s/test-reprojecao-artefatos.sh)
  • RESTORE_KEEP_JOB (deploy/k8s/test-restore.sh)
  • RESTORE_MUTACAO (deploy/k8s/test-restore.sh)
  • ROLLOUT_CONVERGENCE_SECONDS (deploy/k8s/rollout-test.sh)
  • ROLLOUT_EVIDENCE_DIR (deploy/k8s/rollout-test.sh)
  • ROLLOUT_SESSIONS (deploy/k8s/rollout-test.sh)
  • SMOKE_USER (deploy/k8s/bootstrap.sh)
  • TAG (deploy/k8s/bootstrap.sh)
  • WEB_URL (deploy/k8s/smoke.sh)

TODO(humano): não há validação de esquema das variáveis no boot (algo como zod/envalid na api ou NimbleOptions no engine). Hoje um valor numérico inválido vira NaN em silêncio e um erro de digitação no nome de uma variável cai no default sem aviso. As únicas exceções são as marcadas com 🔒, que falham explicitamente em produção.