Pular para o conteúdo principal

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

[Unreleased]​

[0.4.10] - 2026-10-25​

Added​

  • Workflow dogfood-linux.yml: o gate de dogfood D1/D3a/D9 roda no CI. Novo workflow_dispatch (input ref, runner ubuntu-latest, permissions: contents: read) que instala o Ollama pinado por versao + SHA-256 verificado, sobe o servidor e puxa o modelo, e roda scripts/dogfood/linux-clean-install.sh --source local contra o candidato. O job falha se o resumo.txt disser FALHOU e publica dogfood/linux/** como artifact. Sem continue-on-error.
  • Assistente de Primeiro Uso no app desktop (#1636, fatia 3). Na primeira execucao, o Garra Desktop abre um wizard de cinco telas em pt-BR — Boas-vindas -> Modo -> QR -> Personalidade -> Pronto — para um usuario nao-dev instalar o .msi, abrir o app, escanear o QR e conversar com o Garra pelo WhatsApp sem abrir terminal nenhuma vez. O QR e rasterizado num canvas a partir da matriz que o sidecar garraia whatsapp link --machine (fatia 1) emite em NDJSON — o payload cru (credencial) nunca chega a webview. A escolha "proprio" vs "dedicado" persiste em onboarding.json (em app_config_dir()) e e configuravel depois em Ajustes -> WhatsApp (trocar modo e refazer pareamento).
  • Mapeamento de modo e persona para a config (#1636, fatia 3). "Meu proprio numero" aplica whatsapp access open --yes — o Garra responde qualquer um, e por isso a casca so aceita esse modo com a confirmacao explicita do usuario (confirmoAberto), nunca por um invoke silencioso. "Numero dedicado" aplica whatsapp access restricted e whatsapp allow <numero pessoal>: o Garra fica num chip dedicado e o usuario fala dele pelo numero pessoal, que precisa estar autorizado (senao o portao ficaria com 0 autorizados e descartaria o proprio dono em silencio); o numero e validado/normalizado no core (normalizar_numero, E.164). Nenhum default de seguranca e afrouxado em silencio. A persona (nome saneado, estilo, idioma pt-BR por padrao) vira agent.system_prompt pelo novo ConfigLoader::set_agent_system_prompt. O concluido so e gravado — e o gateway so reinicia — apos todos os passos sairem com exit 0.
  • Endurecimento da webview do desktop (#1636, fatia 3). CSP restritiva no tauri.conf.json (default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; ...): todas as telas do desktop passaram a carregar o JS por arquivo externo (onboarding.js, settings.js, chat-bar.js, boot.js), sem script inline. A janela do wizard ganhou uma capability propria e minima (capabilities/onboarding.json, so IPC + eventos), fora da default ampla.
  • Logica do onboarding testada no CI (#1636, fatia 3). A casca Tauri (garraia-desktop, fora dos gates de CI) ficou fina: a montagem do comando do sidecar, o mapeamento modo->config, o primeiro-uso e o texto da persona vivem em garraia-desktop-core::onboarding (puro, com testes). A UI tem um teste Playwright (crates/garraia-desktop/tests/) que abre onboarding.html sem Tauri e sem gateway, com um window.__TAURI__ falso, e dirige o fluxo com eventos NDJSON gravados — inclusive um QR real da fatia 1 — afirmando a navegacao, o canvas desenhado, a persistencia do modo/persona e o caminho de erro. O desktop.yml ganhou o job onboarding-ui e um tripwire de grep no assert-ui-assets. O roteiro manual do pareamento real (com celular) esta em docs/desktop.md.
  • Runtime Node.js privado empacotado no instalador do Garra Desktop (#1636, fatia C). Os instaladores do desktop (MSI/NSIS no Windows, .deb/AppImage no Linux, .dmg/.app no macOS) agora trazem o runtime Node privado e a ponte pre-construida do WhatsApp (node_modules + THIRD_PARTY_NOTICES.txt + manifest.json), para que o usuario nao precise instalar Node, npm ou usar terminal, e nenhuma dependencia seja baixada na primeira execucao. Os scripts fonte-unica de build (build-installer.ps1, build-desktop-linux.sh, build-desktop-macos.sh) montam o bundle da fatia B em crates/garraia-desktop/src-tauri/node-runtime/ antes do cargo tauri build, e o tauri.conf.json mapeia esse diretorio para os resources do Tauri. O node/ e o bridge-whatsapp/ ficam exatamente num dos caminhos do contrato do resolver (fatia A), relativos ao sidecar garraia: ao lado do exe no Windows, em Contents/Resources/ no macOS e em /usr/lib/garraia-desktop/ no .deb/AppImage. O Node privado e usado sempre por caminho explicito, sem mexer no PATH global nem substituir instalacoes existentes. LICENSE do Node e THIRD_PARTY_NOTICES vao no pacote.
  • Smoke de instalador em ambiente limpo no CI (#1636, fatia C). O desktop.yml ganhou jobs smoke-windows/smoke-linux/smoke-macos que, por plataforma, pegam o instalador gerado, extraem a arvore num caminho com espaco, escondem todo Node global do PATH e rodam o sidecar instalado garraia whatsapp self-test --json, exigindo node_origin == privado, self_test == ok e o carregamento de baileys + pino + sharp, alem de provar que o node/ e o bridge-whatsapp/node_modules sao irmaos do sidecar no layout do contrato. Jobs irmaos cobrem ainda a inicializacao pelo desktop (desktop-launch-{linux,windows,macos}: o app sobe, o sidecar responde GET /api/health e morre junto com o app em <=15s), a atualizacao e a desinstalacao (update-{linux,macos,windows}: v0.4.9 -> novo, a sessao em ~/.garraia/whatsapp sobrevive, e a desinstalacao tira o pacote e preserva o dado do usuario). Sem continue-on-error. Fora de escopo automatizado ficam so o pareamento real do WhatsApp (precisa de telefone + QR) e a assinatura/notarizacao do app (decisao do dono); marcados em docs/desktop.md.
  • Novo docs/desktop.md (#1636, fatia C). Documenta o layout por formato, o que o instalador traz, o que a desinstalacao remove e o que fica (~/.garraia/whatsapp/ preservado), como o bundle e montado e o que nao e validado automaticamente.
  • Runtime Node.js privado + ponte do WhatsApp empacotados e verificados (#1636, fatia B). Novo scripts/desktop/node-runtime/build-node-runtime.sh: dado --platform win-x64|linux-x64|darwin-arm64 --out <dir>, baixa o artefato OFICIAL do Node.js (versao fixada em node-version.txt, hoje v24.22.1 LTS Krypton), verifica a PROCEDENCIA por GPG contra as chaves do release team vendorizadas no repo (keys/*.asc + allowlist keys/FINGERPRINTS.txt, num GNUPGHOME isolado, exigindo que a chave primaria que assinou esteja na lista) e a INTEGRIDADE por SHA-256 (nome de arquivo inteiro), extrai so o runtime (node/node.exe + LICENSE), monta a ponte com npm ci --omit=dev --no-audit --no-fund usando o Node e o npm do PROPRIO tarball (sem cross-install), gera THIRD_PARTY_NOTICES.txt (Node + todos os pacotes de node_modules, falhando se algum nao tiver licenca identificavel) e grava manifest.json (versao, plataforma, sha256, fingerprint do assinante, estado do lock da ponte). Layout combinado com o resolver Rust: node/node.exe no Windows, node/bin/node no Unix. Novo workflow .github/workflows/node-runtime.yml (matriz das 3 plataformas em pull_request, workflow_dispatch e workflow_call para a fatia C reusar): monta o bundle num caminho com espaco, esconde o Node global do runner e prova que o bundle roda sozinho (carrega baileys+pino+sharp com o Node privado, sem NODE_PATH), publica cada bundle como artifact de retencao curta e roda um teste negativo que prova a recusa de artefato ou SHASUMS adulterado. Nao mexe em bridge.mjs nem no codigo Rust da ponte. Refs #1636.
  • Node privado por caminho explicito para a ponte do WhatsApp (#1636). O modo vinculado agora resolve o node por caminho explicito, nesta ordem: GARRAIA_NODE_BIN (absoluto; invalido e erro, nunca fallback silencioso para a PATH), depois o Node privado empacotado ao lado do executavel em execucao (layouts fixos por plataforma, relativos ao current_exe()), e so entao a PATH. O resolver nunca muda o PATH do processo nem substitui instalacoes existentes: o Node escolhido e invocado pelo caminho absoluto. O gateway e a CLI usam o mesmo resolver e registram a origem (env/privado/path).
  • npm so e exigido quando ha dependencias a instalar (#1636). Com a arvore pronta (node_modules ja no lugar), o pareamento e o atendimento sobem sem npm nenhum; npm so e cobrado quando o plano e instalar, e sua ausencia vira um erro claro em vez de uma falha de spawn crua.
  • Semeadura da arvore pre-construida da ponte (#1636). Quando ha um diretorio de ponte pre-construida ao lado do Node privado e ele bate com o package-lock.json embutido no binario, o node_modules e copiado (atomicamente, sem shell) para o diretorio da ponte em vez de rodar npm ci; arvore que nao bate e ignorada com aviso, caindo no comportamento de sempre. Prepara a fatia de empacotamento (nenhuma primeira execucao precisa baixar dependencia).
  • garra whatsapp self-test [--json] (#1636). Resolve o Node e roda bridge.mjs --self-test, que importa todas as dependencias de execucao (Baileys, pino, sharp) sem abrir socket nem parear, e reporta a origem do Node, o caminho, a versao e a versao de cada dependencia. Smoke dos instaladores em ambiente sem Node global.
  • Maquina de estados do onboarding do desktop (#1636). Novo modulo onboarding em garraia-desktop-core (sem Tauri, dentro dos gates de CI): as telas do Assistente de Primeiro Uso — BoasVindas → ModoWhatsApp → Qr → Personalidade → Pronto — como estado puro, sem relogio nem I/O, no padrao do state.rs. A tela do QR mapeia os eventos NDJSON do garra whatsapp link --machine (fatia 1) via o tipo EventoNdjson (serde, tolerante a eventos desconhecidos). Avancar nao pula o vinculo, Falhou permite tentar de novo e nenhum estado terminal avanca sozinho. Inclui o tipo OnboardingPersistido (round-trip para onboarding.json). So logica e testes; a janela e a fiacao da webview ficam para a fatia 3. Refs #1636
  • garra whatsapp link --machine: pareamento em NDJSON (#1636). Novo modo- maquina do link: nao exige terminal, nao le stdin e emite NDJSON no stdout (uma linha por evento, com schema e event — status, qr, error, log). O QR sai como matriz de 0/1 pelo mesmo motor de QR do terminal, via a nova qr::modules em garraia-channels (um motor so, sem risco de polaridade invertida); o payload cru nunca entra. A ordem que e contrato se mantem: session_saved antes de channel_enabled. Erros tambem saem como NDJSON e o exit code segue os mesmos sysexits do link. O link interativo fica inalterado. Para o app desktop consumir como sidecar; ver docs/whatsapp.md secao "Modo maquina". Refs #1636
  • Site de documentacao Docusaurus em website/, publicado no GitHub Pages (#1639). O conteudo nao e escrito la: vem do markdown que ja existe no repositorio (README, docs/**, docs/adr/**, wiki/**, CHANGELOG, ROADMAP, CONTRIBUTING, ...) por um script de sync em Node puro — fonte unica de verdade, nada reescrito a mao. Portugues (Brasil) como idioma padrao e ingles parcial (Introducao, Instalacao, Releases e as paginas What's New), com o restante caindo para o portugues. Busca offline (@easyops-cn/docusaurus-search-local), tema claro/escuro e o workflow docs-site.yml, que compila cada PR e publica no Pages a cada push na main. Publicado em https://michelbr84.github.io/GarraRUST/ (url/baseUrl configuraveis por env). Nao altera o ci.yml nem o site garraia.org.
  • Canal WhatsApp linked: guarda de cadencia entre turnos por chat (#1650). Passando de 20 turnos admitidos em 60s no mesmo chat, o turno e cortado com log possivel loop entre agentes, com um unico aviso por rajada; turno cortado nao conta na janela, entao o chat volta sozinho quando o outro lado cala. Teto ainda fixo (configuravel e contador em doctor ficam como follow-up).
  • Fundacao de schema de billing do Garra Cloud: plan/subscription/invoice (#1652). Migration 035 do garraia-workspace adiciona tres entidades. plans e um catalogo global (sem RLS, mesma classe de roles/permissions), semeado apenas com o plano free sem preco (price/currency NULL = preco nao fixado neste repositorio MIT). subscriptions e invoices sao por tenant sob FORCE RLS (isolamento por group_id, fail-closed via NULLIF, WITH CHECK identico ao USING). Dinheiro e sempre inteiro de centavos + ISO 4217; ids de provedor de pagamento sao opcionais e UNIQUE por provedor; nunca ha PII de cartao. A fatura e ledger imutavel (garraia_app so SELECT/INSERT). Todo grupo sem assinatura viva resolve para o plano Free pela funcao garraia_effective_plan_id (sem trigger nem backfill; respeita RLS), entao um tenant novo nasce Free sem intervir ninguem. So o schema: webhook Stripe/Pix (#1653), entitlements (#1654), metering (#1655) e a pagina de signup (#1656) ficam para as proximas issues. ADR 0026.
  • Diagnostico nativo e seguro da instalacao do Garra (#1669). Nova tool de agente garra_diagnostics no garraia-gateway: relatorio estruturado e somente leitura da propria instalacao — versao instalada, estado do gateway, todos os canais com estado, servidores MCP, capacidades e ferramentas disponiveis, configuracao efetiva validada (garraia config check), presenca e ORIGEM de credenciais sem nunca revelar valores (vault / config.yml / env), amostra limitada e redigida das ultimas linhas do log e componentes relevantes (ponte Node do WhatsApp, banco alcancavel). Saida em JSON com uma entrada por verificacao {id, estado, evidencia, detalhe} e vocabulario fechada de estados (ok, erro, nao_configurado, bloqueado, degradado, nao_suportado). Controle de acesso por sessao do operador + turno (sessao_do_operador/turno_restrito), fail-closed: quem nao e o operador recebe uma recusa e nada mais. File jail para a leitura do log (so dentro do diretorio do produto), redacao de segredos (garraia_security) em todo texto e zero shell. Refs #1669
  • Catálogo versionado de funcionalidades embutido no binário (#1670). O Garra conhece e explica as funcionalidades da versão que está executando, inclusive após instalação limpa (sem clone do repositório): a fonte única é o manifesto crates/garraia-gateway/assets/catalogo-funcionalidades.json (chaves estáveis em inglês, conteúdo pt-BR), embutido via include_str! — o build falha se o arquivo-fonte não existir e nada é lido do disco em runtime. Cada recurso traz descrição curta, pré-requisitos, limitações e estado de disponibilidade (disponivel / configuravel / opcional), com marcador de feature/config (gate) para o que depende de compilação ou interruptor (sandbox, TLS, mcp-http) — recurso que exige configuração nunca aparece como disponivel. Consulta sob demanda pela nova tool garra_capabilities (filtros name/category, limite de itens; sem filtro devolve só contagens — consumo de contexto baixo), integrada ao garra_status, que ganha a seção catalog com a versão do catálogo e o enlace da tool. Em turno restrito a tool devolve só o subconjunto que a permissão permite (itens operator_only retidos e contados).

Changed​

  • A automacao de release marca make_latest: false explicito para prereleases. O release.yml ja punha prerelease: true em tags -alpha/-beta/-rc; agora tambem diz ao GitHub, com a mesma condicao, para nao torna-las "Latest release" (estavel: make_latest: true, o default agora explicito). Assim um RC nunca e servido por /releases/latest, e o garra update (fixo em /releases/latest) e o updater Tauri (que consulta releases/latest/download/latest.json) continuam na estavel anterior.
  • Site de documentacao: cada secao ganha um indice gerado. As URLs de categoria do Docusaurus (/instalacao/, /gateway/, /seguranca/...) listam as paginas da secao em vez de responder 404; a secao Releases mantem a pagina propria.
  • A guarda de cadencia entre turnos (#1658) completa a #1650: teto configuravel (channels.whatsapp_linked.max_turnos_per_minute, default inalterado de 20 turnos/60s), backoff crescente por rajada (60s -> 120s -> 240s, teto 8min, zera apos 2min de silencio), teto menor para mensagens vazias/stub (3 por janela, min com o teto do operador) e turnos_ultima_hora no diagnostico do gateway (tool garra_diagnostics, operator-only). O garraia doctor offline continua sem esse contador — memoria de outro processo, declarado na propria tool.

Fixed​

  • O boot do gateway sonda o Whisper antes de declarar o STT pronto. A linha STT initialized - Whisper at ... saia incondicionalmente, mesmo com o servidor STT fora do ar, enquanto o TTS logo acima ja era sondado - o que levava o operador (e o proprio agente Garra) a crer que o STT estava vivo. Agora o boot chama WhisperClient::health_check() com orcamento de timeouts.health (nao trava o boot num host que engole pacotes) e loga STT ready so quando o Whisper responde, ou um warn "not reachable" quando nao; o cliente segue registrado nos dois casos, porque o servidor pode subir depois.
  • Catalogo de funcionalidades acompanha o que entrou depois da 0.4.9. O manifesto crates/garraia-gateway/assets/catalogo-funcionalidades.json (#1670), de onde o Garra tira a resposta sobre as proprias funcionalidades, ganha os itens garra_diagnostics (#1669) e desktop (app com o Assistente de Primeiro Uso, #1636), e corrige o whatsapp_linked: Node 20+ no host so e pre-requisito da CLI, porque o app desktop ja traz um Node privado; pareamento pelo assistente; cadencia entre turnos (#1650) e o alcance do whatsapp self-test. O caminho da allowlist no catalogo passa a ser o que se escreve no config.yml (channels.whatsapp_linked.allow, sem um settings. que o YAML nao tem, porque o campo e achatado); o mesmo vale para o max_turnos_per_minute da #1650. O mcp-client registra o jail fail-closed com o servidor filesystem fixado em 2.0.0 (#1684), e instalacao-atualizacao passa a citar o install.ps1 e o canal de pre-lancamento. O runbook de release (docs/releasing.md) ganha o passo de subir o catalog_version junto com o bump do Cargo, que o teste da #1670 exige.
  • Voz com o Chatterbox oficial volta a funcionar: o cliente agora descobre a assinatura do generate_tts_audio. O cliente mandava so 4 valores ([text, lang, null, 1.0]), mas o app oficial (multilingual_app.py) liga 7 entradas (texto, idioma, referencia, exaggeration, temperature, seed, cfg_weight). O Gradio 6.x recusava a chamada curta com event: error, a rota /api/tts caia no fallback de texto sem avisar ninguem, e o docs/voice.md dizia que funcionava. Agora o cliente le GET /gradio_api/info, monta o data com exatamente o numero de valores que o servidor declara (usando os parameter_default dele, ou os defaults do app oficial), cacheia a assinatura e cai no layout oficial de 7 valores quando /gradio_api/info nao responde. Servidores customizados de 4 parametros continuam funcionando. Um event: error no SSE agora vira erro com a mensagem do servidor, e /api/tts e o canal de voz do Telegram registram warn com o motivo antes do fallback de texto.
  • O Dashboard do Web Console mostra o bind real em vez de "0.0.0.0" fixo. O card "Gateway port" trazia "Listening on 0.0.0.0" fixo no HTML, enganoso para quem escuta em 127.0.0.1; agora ele reflete o host:port real derivado do gateway_url que /api/health ja devolve. A linha "Config dir" do painel Health, que sempre mostrou "-" (nenhum JS a preenchia), foi removida: a unica fonte do caminho no backend e gateada (/api/diagnostics), e /api/health e auth-free, entao expor o diretorio ali vazaria o home/usuario do SO a um peer anonimo num bind nao-loopback.
  • docs/voice.md corrige o exemplo de /api/stt e documenta o pin do torch. O exemplo usava --data-binary, mas o handler exige multipart com o campo file (um corpo cru volta 400 No audio file provided); agora usa -F "file=@audio.wav". A pagina tambem explica que o cliente se adapta a assinatura do generate_tts_audio e avisa que o chatterbox-tts fixa torch==2.6.0, que nao tem kernels Blackwell (sm_120) e quebra em RTX 50xx, com o override para as wheels cu128.
  • Os instaladores nao caem num prerelease pelo caminho nao pinado. Quando /releases/latest nao resolvia, o ultimo fallback de install.sh e install.ps1 pegava o primeiro release nao-draft — e prereleases contam, entao um curl | sh / irm | iex podia instalar um -rc/-beta/-alpha. Os dois instaladores agora pulam prerelease: true tambem (alem de draft: true) e escolhem a estavel mais recente; o install.sh classifica cada release por tag_name/draft/prerelease em qualquer ordem (scanner com profundidade de chaves, imune a chaves {} dentro de body). Para instalar um RC de proposito, pine GARRAIA_VERSION=vX.Y.Z-rcN.
  • Fixa o @modelcontextprotocol/server-filesystem autoprovisionado em @2.0.0 (o upstream deprecou a serie por data 2026.8.31 e voltou ao semver; latest agora e 2.0.0). O handshake initialize + tools/list e identico entre as duas versoes (mesmas 14 tools, mesmos argumentos de caminho). Um mcp.json existente fixado em @2026.8.31 continua valido e e reportado como pinado (OK) — o gateway nunca reescreve o arquivo do usuario; so instalacao nova recebe @2.0.0.
  • O seletor "LLM Provider" do Web Console mostra as instancias reais, nao so tipos fixos. Desde a #1540 um provider e registrado pela CHAVE do perfil (llm.<chave>), entao llm.ollama-qwen3.provider: openai registra sob o id ollama-qwen3. O GET /api/providers ainda comparava os ids contra um catalogo fixo de TIPOS, entao o console marcava "OpenAI (Not Configured)" / "Ollama (Not Configured)" e nunca mostrava ollama-qwen3. A resposta ganha um campo aditivo instances (id, kind, modelo, se e o padrao, se esta em fallback) e o active do catalogo passa a considerar o tipo configurado quando ha instancia daquele kind. PATCH /api/providers/default e POST /api/providers/test ja resolviam pelo id da instancia.
  • README destaca a v0.4.9 como release mais recente, nao mais a v0.3.9. A secao de marcos do README.md ainda anunciava a v0.3.9 (set 2026) como novidade; agora reflete a v0.4.9 (out 2026) com base na prosa de abertura do CHANGELOG.md e no link da wiki correspondente.
  • Atualiza spin 0.10.0 -> 0.10.1 e 0.9.8 -> 0.9.9 no Cargo.lock (0.10.0 e 0.9.8 foram yanked; derrubava Security Audit e cargo-deny nos PRs).
  • Ponte do WhatsApp agora sobe o Node no Windows (#1636). O env_clear() da contencao do processo filho tirava SystemRoot e os demais essenciais de sistema, e o Node abortava logo no boot com Assertion failed: ncrypto::CSPRNG(nullptr, 0) (codigo 134) — a inicializacao do RNG de cripto nao acha a cripto do Windows sem eles. O bridge passa a herdar, so no Windows, os mesmos essenciais que o transporte stdio dos servidores MCP ja herdava (SystemRoot, SystemDrive, windir, COMSPEC, PATHEXT, USERPROFILE, APPDATA, ...), nenhum deles segredo nem vetor de execucao de codigo. Afetava pareamento, atendimento e npm ci, nao so o novo self-test.
  • No Linux e no macOS o sidecar do desktop tambem nao fica orfao numa morte abrupta do app (#1636). O Job Object so existe no Windows; para o par app->sidecar no Unix, a casca lanca o gateway com garra start --exit-with-parent <pid-do-app>. O gateway vigia esse PID numa task de ~1 s (garraia_common::parent_watch: reparentamento via getppid() — a prova de reuso de PID — ou o PID sumindo) e, quando o app some, pede o encerramento gracioso pelo caminho do SIGTERM, derrubando a ponte Node pelo fluxo normal. Flag e nao variavel de ambiente, para o PID nao vazar para filhos; so em primeiro plano. No Windows a flag e no-op (o Job Object cobre). Logica pura testada e um teste de ponta a ponta no CI Linux/macOS matam o PID vigiado e afirmam que a vigia sai.
  • A ponte Node do WhatsApp e o sidecar do desktop nao sobrevivem orfaos quando o pai morre de forma abrupta (#1636). kill_on_drop e o Drop so cobrem a saida normal e o panic; um crash ou "Finalizar tarefa" no Gerenciador de Tarefas nao roda nenhuma limpeza. Agora, no Windows, a ponte e o sidecar entram num Job Object KILL_ON_JOB_CLOSE (modulos jobobject em garraia-channels e garraia-desktop-core), entao o SO encerra a arvore inteira ao fechar o handle quando o pai morre; no Unix a ponte nasce no proprio grupo de processos (setpgid) e o teardown varre o grupo com kill(-pgid), pegando netos; e em todas as plataformas a ponte sai ao ver EOF no stdin (o pai que morreu fecha o pipe), que e a rede de seguranca do macOS, sem PDEATHSIG. Um job de CI dedicado executa os testes de orfao em ubuntu, windows e macos — incluindo matar o pai com TerminateProcess e afirmar que nenhum descendente sobrevive.
  • find_executable da ponte do WhatsApp so aprova arquivo com bit de execucao no Unix; um node em 0644 deixa de passar na deteccao e de virar npm ci com codigo 126 (#1646).
  • O caminho nao-streaming autonomo do runtime (app mobile, /ws, /v1/chat/completions, chat REST) nao publica mais o marcador [no textual response provided by the model]: turno inteiramente vazio vira erro explicito, como no streaming (#1648, follow-up do #1048).
  • garraia doctor avisa quando ha outra copia de garraia/garra no PATH com versao diferente da que esta rodando (ex.: build antigo em ~/.local/bin mascarando o .deb novo), listando os dois caminhos e o fix; campo binary no --json (#1649).
  • O log de reconexao do canal WhatsApp vinculado agora informa o motivo da queda (reason) e o codigo do Baileys (reason_code) quando o bridge os envia; antes a linha WhatsApp vinculado caiu; reconectando nao dizia por que a sessao caiu (#1663).
  • A imagem Docker volta a compilar com os forks de third_party/ (#1694). O [patch.crates-io] que aponta tauri-plugin-updater e utoipa-swagger-ui para forks vendorizados (#1668) entrou depois da v0.4.9, e o Dockerfile nunca copiava third_party/: o cargo chef cook abortava com "patch location /build/third_party/... does not contain packages" e o Deploy do tag v0.4.10-rc1 nao publicou imagem. O Dockerfile agora copia third_party/ no planner e nos dois lados do cook; o teste dockerfile_copies_every_patch_path_outside_crates prende a regra em todo PR, e o novo workflow docker-build.yml constroi a imagem (sem push) e responde /ping quando muda o Dockerfile, o Cargo.toml/Cargo.lock da raiz ou third_party/. O smoke Windows do desktop ganhou tetos internos que cabem no teto do job, para um travamento deixar log em vez de virar cancelamento mudo.
  • /api/diagnostics deixa de acender amarelo numa instalacao nova (#1695). A linha tools.capabilities saia warning em toda instalacao limpa, porque device_read/device_execute (sem adapter de hardware) e telegram_send_voice (modo voz desligado) contavam como indisponiveis — visto no Windows e no Linux limpos do v0.4.10-rc1, contra o criterio D9 do gate de release ("sem nenhum aviso espurio"). Agora ausencia de configuracao vira not_configured (neutro, como o bash sem sandbox desde a #1471): as tools de hardware devolvem not_configured quando nenhum adapter (hardware.mqtt/hardware.home_assistant) foi declarado, e o registro de capacidades trata tts_unconfigured como nao configurado. Configurado e quebrado continua amarelo: adapter declarado sem nenhum dispositivo segue no_devices, e canal configurado fora do ar segue channel_offline. O motivo de origem continua no reason_code.
  • Sem Node.js na maquina, o boot nao provisiona mais um MCP filesystem que nunca sobe (#1696). A entrada autoprovisionada roda npx, e numa instalacao nova sem Node (a CLI num Linux limpo, ou o desktop num Windows sem Node global, ja que o Node privado do instalador e so da ponte do WhatsApp e nao traz npx) o spawn falhava com No such file or directory, o supervisor gastava os restarts e o /api/diagnostics ficava amarelo com causa: other. Sem npx no PATH o boot nao grava o mcp.json nem cria o workspace, e a linha mcp.servers fica skipped dizendo o que falta e como ligar o filesystem; as file tools nativas seguem disponiveis, e o primeiro boot depois de instalar o Node.js 20+ provisiona sozinho. Um mcp.json existente continua intocado, e servidor configurado que nao sobe segue acusando.
  • No Windows, o /api/diagnostics deixa de dizer que o workspace esta "fora do data_dir" (#1698). O data_dir e canonicalizado, e no Windows o canonico e \\?\C:\..., enquanto as raizes do mcp.json sao gravadas como C:\...; a comparacao crua falhava e toda instalacao Windows via mcp.filesystem_root e as outras linhas de raiz mostrarem "1 fora do data_dir" para o workspace que esta dentro. Os dois lados agora passam pela mesma forma comparavel antes de relativizar, o relativo sai sempre com /, e um symlink dentro do data_dir que aponta para fora continua aparecendo como fora. Achado na validacao do v0.4.10-rc1 (#1697).
  • No desktop, a tela "Pronto" do Assistente de Primeiro Uso avisa quando falta provedor de LLM (#1700). Antes, uma instalacao limpa pareava o WhatsApp e a tela anuncava "Garra conectado e pronto" — mas sem llm: no config o Garra recebia as mensagens e nunca respondia (o mesmo caso que o doctor do WhatsApp ja julga como "no provider configured"). Agora, depois do "Garra conectado", um comando le o MESMO config que o gateway le e devolve so o booleano (a chave nunca atravessa a webview); sem provedor, um aviso dourado explica que falta garraia init (com o caminho no Windows) antes do primeiro "oi" sem resposta. Quem tem provedor configurado ve exatamente a tela de antes. O passo de escolher o provedor dentro do assistente fica para a 0.5.0 (#1636).
  • O CI deixa de ficar vermelho com o RUSTSEC-2026-0387 (#1701). O backend do jsonwebtoken troca de rust_crypto para aws_lc_rs: como rust_crypto e uma feature monolitica (nao ha como desligar EC mantendo RS256/HS256) e o codigo so usa HS256/RS256, a cadeia RustCrypto EC/RSA inteira — 20 crates, incluindo o crypto-bigint 0.5.5 vulneravel, que so tem fix em 0.7.6 — sai do lockfile sem nenhuma excecao no deny.toml/audit.toml. Trade-off documentado no comentario do Cargo.toml: o aws-lc-sys compila C nativo (cmake + perl, presentes nos runners).

Security​

  • Revoga de garraia_app o UPDATE/DELETE que a ALTER DEFAULT PRIVILEGES da migration 007 vazava para nove tabelas. A ADP da 007 concede CRUD completo a toda tabela criada depois dela; varias migrations declararam um GRANT seletivo menor, mas GRANT e aditivo e nao restringe, entao o privilegio extra sobrevivia. A migration 036 (forward-only) revoga so o excesso: UPDATE em task_attachments/message_attachments/message_reactions/message_mentions/doc_page_mentions, DELETE em doc_pages/user_email_change_requests, e UPDATE+DELETE em doc_page_versions/account_deletion_requests. SELECT/INSERT e o privilegio de mutacao legitimo de cada tabela permanecem; o apagamento LGPD cross-tenant continua rodando em funcoes SECURITY DEFINER como garraia_purge, nunca pelo pool do app. O caso com peso de seguranca e account_deletion_requests: sem o revoke, um handler no escopo do proprio titular podia marcar o pedido como concluido sem o purge ou cancela-lo fora do fluxo.
  • Atualiza ctutils 0.4.2 -> 0.4.5 no Cargo.lock (RUSTSEC-2026-0385: geracao de mascara e comparacoes de u128 sem tempo constante). Entra por digest 0.11, usado por argon2/blake2 (hash de senha) e hmac. Sem mudanca de codigo.
  • Atualiza a familia wasmtime/cranelift/wiggle 49.0.3 -> 49.0.4 no Cargo.lock (RUSTSEC-2026-0379, critica, e RUSTSEC-2026-0378 no sandbox de plugins).
  • Atualiza rustls-webpki 0.103.15 -> 0.103.16 (RUSTSEC-2026-0382) e webpki-roots/webpki-root-certs 1.0.7 -> 1.0.10 (RUSTSEC-2026-0381/0383) no Cargo.lock.
  • Ignora com prazo (2026-11-20) RUSTSEC-2026-0381 para o shim webpki-roots 0.26.11 (reexporta o 1.0.10 corrigido) e espelha RUSTSEC-2026-0370/0371/0372 em .cargo/audit.toml (#1667).
  • Jail por sessao das tools MCP de filesystem (#1482) agora e fail-closed: uma ferramenta vinda do servidor @modelcontextprotocol/server-filesystem que o confinamento nao mapeia (uma tool nova ou renomeada de uma versao futura do pacote) e recusada em vez de repassada sem checar o caminho. Antes, operacao fora do mapa passava intocada em qualquer servidor; agora, so no servidor filesystem, ela so roda depois de ser mapeada em OPERACOES_COM_CAMINHO (revisao humana). Tools de outros servidores MCP seguem passando normalmente.
  • Jail e workdir por orquestrador no garra_agent do MCP HTTP (#1626, item 1). Cada orquestrador da ponte HTTP passa a ter uma subarvore gravavel propria sob <data_dir>/workspace, derivada da identidade dele por digest (SHA-256, 32 hex), que vira a unica raiz efetiva das file tools (jail sem raiz fixa) e o workdir do sandbox do bash. Antes o jail e o workdir eram globais (agent.file_roots), iguais para todo orquestrador: o que um escrevia o outro lia — disclosure e prompt-injection indireta cross-principal, a mesma classe que a #1449 fechou para os canais. agent.file_roots deixa de alcancar o agente da ponte (ela atende pedido de terceiro); sem workspace materializado o agente cai em fail-closed, com as file tools negadas e o bash recusado. As mensagens das file tools e do git_diff/code_review que iam ao orquestrador passam a mostrar o caminho RELATIVO ao workspace, nunca o caminho absoluto do host (que traria <data_dir> e o hash do proprio chamador) — principio da #1465. Limite conhecido: em isolated-pod todos os orquestradores compartilham o filesystem do pod, entao o workdir isola o bash so no sandbox docker/podman.
  • Corrigidos os advisories RUSTSEC-2026-0367, -0368 e -0369 (zip 4.6.1 e 8.6.0). O tauri-plugin-updater e o utoipa-swagger-ui passam a usar zip 9.0.5 por forks vendorizados em third_party/ (ver third_party/README.md). Os assets do Swagger UI continuam embutidos e nao sao baixados no build. (#1662)
  • Excecao temporaria, com prazo ate 2026-11-18, para tres advisories somente unmaintained do desktop (zbus_names, zvariant, zvariant_derive). Acompanhamento em #1667. (#1662)
  • Canais remotos (Telegram, mobile e demais superficies que nao sao do operador) passam a ter o teto de superficie Nivel::Read: nao executam bash, nao escrevem arquivo, nao enviam mensagem proativa, nao agendam, nao acessam dispositivo e nao gravam memoria, mesmo com /mode code ou com o auto. Respostas normais nao mudam. Operadores (web, api, vscode, desktop) nao mudam. (#1665)
  • Security: o fork vendorizado do tauri-plugin-updater (third_party/) nunca mais desliga a verificação de TLS do updater. dangerousAcceptInvalidCerts/dangerousAcceptInvalidHostnames continuam aceitos na config (sem quebrar instalações) mas são ignorados, com aviso. Resolve os 4 alertas rust/disabled-certificate-check do CodeQL no PR #1668, por correção em código (preferência §3 do ledger de supressões). Teste de guarda: updater_tls_nunca_desligado. (#1671)
  • website/: overrides no package-lock.json resolvem 6 dos 7 alertas Dependabot npm (tinypool 2.1.2, postcss-selector-parser 7.1.6, serialize-javascript 7.0.5, uuid 11.1.1) — todos transitivos do toolchain do Docusaurus; braces <= 3.0.3 permanece sem patch upstream (risco residual documentado na PR, revisão 2026-11-21).

[0.4.10-rc2] - 2026-10-23​

Segundo pre-lancamento da 0.4.10, nao e a versao estavel. O rc1 publicou a Release como devia (prerelease, sem latest, canal estavel intacto: /releases/latest, latest.json, garra update e os instaladores sem versao fixada seguem na v0.4.9), mas o Deploy da imagem Docker falhou: o Dockerfile nao copiava os forks vendorizados de third_party/ que o [patch.crates-io] usa desde #1668, e a imagem 0.4.10-rc1 nao existe. O rc2 corrige o Dockerfile e passa a construir a imagem (sem push) em todo PR que mexe no Dockerfile, no Cargo.toml/Cargo.lock da raiz ou em third_party/ (#1694). A validacao do rc1 achou tambem um falso amarelo no diagnostico de uma instalacao nova: tools.capabilities contava device_read e device_execute como indisponiveis quando nenhum adapter de hardware estava configurado, e o certo e "nao configurado"; o rc2 corrige isso, e adapter configurado sem dispositivo continua acusando (#1695). E, numa maquina sem Node.js (a CLI num Linux limpo, ou o desktop num Windows sem Node global, ja que o Node privado do instalador e so da ponte do WhatsApp), o boot deixa de provisionar um MCP filesystem via npx que nunca subia e deixava o diagnostico amarelo; o diagnostico, o garra doctor mcp e a tool garra_diagnostics passam a dizer que falta o npx, e o primeiro boot depois de instalar o Node provisiona sozinho (#1696). No Windows, o diagnostico deixa de dizer que o workspace esta "fora do data_dir" quando ele esta dentro: o data_dir canonico (\\?\C:\...) nao casava com a raiz crua do mcp.json (#1698). Fora essas quatro correcoes, o codigo e o mesmo do rc1. Os testes que falham no Windows e que o CI Windows nao executa (ele so compila os testes) estao documentados, com evidencia, na #1697. Como o rc1, este RC sai como prerelease e nao e oferecido a quem esta na v0.4.9.

Destaques. Os instaladores desktop (MSI/NSIS no Windows, .deb/AppImage no Linux, DMG no macOS Apple Silicon) passam a levar um Node.js v24.22.1 LTS privado, com procedencia verificada por GPG (chaves do release team do Node fixadas por fingerprint) e integridade por SHA-256, junto com as dependencias da ponte do WhatsApp ja instaladas: parear o WhatsApp deixa de exigir Node, npm ou terminal, e o runtime e chamado por caminho explicito, sem mexer no PATH nem em instalacoes existentes (#1683, #1685, #1689). No primeiro uso, o desktop abre um assistente com o QR do WhatsApp, a persona e o modo de acesso, e o modo dedicado ja autoriza o numero pessoal do dono para ele nao ficar trancado do lado de fora (#1690). A ponte e o sidecar nao deixam mais processos orfaos (Job Object no Windows, grupo de processos no Unix, garra start --exit-with-parent; #1687), e a ponte volta a subir no Windows, onde faltava SystemRoot no ambiente do processo filho (#1685). Seguranca: a migration 036 revoga privilegios que o ALTER DEFAULT PRIVILEGES da 007 concedia em nove tabelas (#1682); o jail do MCP filesystem passa a ser fail-closed, com o servidor fixado em 2.0.0 (#1684); RUSTSEC-2026-0379, 0381, 0382, 0383 e 0385 e os advisories do zip fechados; teto de superficie para sessoes nao-operador (#1665) e jail e workdir por orquestrador no MCP HTTP (#1642). Tambem nesta versao: schema de billing com o plano Free efetivo, sem cobranca ainda (#1652); diagnostico nativo sem shell, garra_diagnostics (#1669); catalogo versionado de funcionalidades (#1670); cadencia entre turnos no WhatsApp (#1650); cliente Chatterbox que segue a assinatura do endpoint Gradio; sonda do Whisper no boot; e o seletor de provedores do Web Console listando as instancias reais.

Compatibilidade. Sem breaking change de config ou de API. As migrations 035 (billing) e 036 (revoke) sao forward-only. O instalador desktop cresce de 31 a 58 MB, conforme a plataforma, por causa do runtime Node embutido.

Como testar. CLI no Windows: $env:GARRAIA_VERSION='v0.4.10-rc2'; irm https://github.com/michelbr84/GarraRUST/releases/download/v0.4.10-rc2/install.ps1 | iex. CLI no Linux/macOS: curl -fsSL https://github.com/michelbr84/GarraRUST/releases/download/v0.4.10-rc2/install.sh | sh -s -- --version v0.4.10-rc2. Docker: docker pull ghcr.io/michelbr84/garraia:0.4.10-rc2. Desktop: baixar o .msi, o .deb, o AppImage ou o .dmg desta pagina e instalar numa maquina sem Node nem npm globais.

Limites conhecidos. O pareamento real do WhatsApp pelo assistente, a instalacao manual dos bundles desktop (D5/D6), o D2 completo no Windows e o D10 sao o que este RC valida antes da estavel. Os instaladores nao tem assinatura de code signing, entao o SmartScreen do Windows e o Gatekeeper do macOS podem avisar. A #1636 continua aberta ate o teste real de pareamento.

[0.4.10-rc1] - 2026-10-23​

Pre-lancamento para validacao, nao e a versao estavel. Este RC existe para provar no mundo real, antes da v0.4.10, o Assistente de Primeiro Uso do desktop e o runtime Node.js privado embutido nos instaladores (#1636). Ele sai como prerelease: nao vira latest, nao e oferecido pelo garra update, pelo install.sh/install.ps1 sem versao fixada nem pelo auto-updater do desktop. Quem esta na v0.4.9 continua na v0.4.9.

Destaques. Os instaladores desktop (MSI/NSIS no Windows, .deb/AppImage no Linux, DMG no macOS Apple Silicon) passam a levar um Node.js v24.22.1 LTS privado, com procedencia verificada por GPG (chaves do release team do Node fixadas por fingerprint) e integridade por SHA-256, junto com as dependencias da ponte do WhatsApp ja instaladas: parear o WhatsApp deixa de exigir Node, npm ou terminal, e o runtime e chamado por caminho explicito, sem mexer no PATH nem em instalacoes existentes (#1683, #1685, #1689). No primeiro uso, o desktop abre um assistente com o QR do WhatsApp, a persona e o modo de acesso, e o modo dedicado ja autoriza o numero pessoal do dono para ele nao ficar trancado do lado de fora (#1690). A ponte e o sidecar nao deixam mais processos orfaos (Job Object no Windows, grupo de processos no Unix, garra start --exit-with-parent; #1687), e a ponte volta a subir no Windows, onde faltava SystemRoot no ambiente do processo filho (#1685). Seguranca: a migration 036 revoga privilegios que o ALTER DEFAULT PRIVILEGES da 007 concedia em nove tabelas (#1682); o jail do MCP filesystem passa a ser fail-closed, com o servidor fixado em 2.0.0 (#1684); RUSTSEC-2026-0379, 0381, 0382, 0383 e 0385 e os advisories do zip fechados; teto de superficie para sessoes nao-operador (#1665) e jail e workdir por orquestrador no MCP HTTP (#1642). Tambem nesta versao: schema de billing com o plano Free efetivo, sem cobranca ainda (#1652); diagnostico nativo sem shell, garra_diagnostics (#1669); catalogo versionado de funcionalidades (#1670); cadencia entre turnos no WhatsApp (#1650); cliente Chatterbox que segue a assinatura do endpoint Gradio; sonda do Whisper no boot; e o seletor de provedores do Web Console listando as instancias reais.

Compatibilidade. Sem breaking change de config ou de API. As migrations 035 (billing) e 036 (revoke) sao forward-only. O instalador desktop cresce de 31 a 58 MB, conforme a plataforma, por causa do runtime Node embutido.

Como testar. CLI no Windows: $env:GARRAIA_VERSION='v0.4.10-rc1'; irm https://github.com/michelbr84/GarraRUST/releases/download/v0.4.10-rc1/install.ps1 | iex. CLI no Linux/macOS: curl -fsSL https://github.com/michelbr84/GarraRUST/releases/download/v0.4.10-rc1/install.sh | sh -s -- --version v0.4.10-rc1. Desktop: baixar o .msi, o .deb, o AppImage ou o .dmg desta pagina e instalar numa maquina sem Node nem npm globais.

Limites conhecidos. O pareamento real do WhatsApp pelo assistente, a instalacao manual dos bundles desktop (D5/D6), o D2 no Windows e o D10 sao justamente o que este RC vai validar antes da estavel. Os instaladores nao tem assinatura de code signing, entao o SmartScreen do Windows e o Gatekeeper do macOS podem avisar. A #1636 continua aberta ate o teste real de pareamento.

[0.4.9] - 2026-10-15​

Duas frentes grandes e uma varredura de seguranca. A primeira frente sao os direitos dos titulares de dados, que saem do papel: GET /v1/me/export passa a devolver o conteudo pessoal do titular em todos os grupos dele (mensagens, mencoes, arquivos, memorias e tarefas) e declara o que ficou de fora; PATCH /v1/me aceita email sem nunca trocar o e-mail as cegas; DELETE /v1/me enfileira um apagamento com carencia de 30 dias executado por um worker com credenciais dedicadas (garraia_purge, migration 034). A segunda frente e o gateway como servidor MCP por HTTP com delegacao de tarefas: garra_ask (#1612), orquestradores externos com identidade propria, politica e auditoria por orquestrador (#1613), garra_agent (#1615) e a redacao extensivel por regex do operador (#1626, item 4) — tudo desligado por padrao, com orcamento proprio e fail-closed. O item 1 da #1626 (jail e workdir por orquestrador) fica para a 0.4.10.

Seguranca. Vinte e quatro advisories RUSTSEC fechados de uma vez no Cargo.lock (#1630): rustls 0.23.46, quinn-proto 0.11.18, wasmtime/wasmtime-wasi 49.0.3 (inclui o escape do sandbox de filesystem RUSTSEC-2026-0352) e chacha20 0.10.2, mais a troca do serde_yaml abandonado pelo fork mantido serde_norway — mesma API, sem mudanca de comportamento, e nenhuma excecao nova no deny.toml. Cada asset da release leva atestacao de proveniencia verificavel com gh attestation verify (#1606), e o request-path do gateway passa a negar unwrap()/expect() (#1569). A secao Security abaixo detalha advisory por advisory.

Tambem nesta versao. garra uninstall remove a CLI da maquina (--purge para levar a config junto); o auto-updater do desktop ganha o latest.json correto, o DMG do macOS Apple Silicon e as assinaturas do Windows estagiadas (#1568); dependencias de outubro atualizadas em bloco (tokio 1.53.3, opentelemetry 0.33.1, Tauri 2.12.2 + plugins, Playwright 1.64, rust:1.99-slim na imagem Docker); dois testes que piscavam no CI por corrida entre testes paralelos deixam de piscar; e os rascunhos juridicos e de precificacao sairam do versionamento — conformidade nao e anunciada antes de existir.

Compatibilidade. Sem breaking change de config ou de API. MSRV segue 1.96; binarios Linux seguem a baseline glibc >= 2.35 (Ubuntu 22.04+, Debian 12+). Quem usa serde_yaml so por tabela (config.yml) nao percebe a troca. O Garra Mobile acompanha a versao (0.4.9+12; a 0.4.8 nao bumpou o APK).

Como atualizar. garra update (ou garraia update) a partir de qualquer 0.4.x; instalacao nova com curl -fsSL https://github.com/michelbr84/GarraRUST/releases/latest/download/install.sh | sh ou, no Windows, irm https://github.com/michelbr84/GarraRUST/releases/latest/download/install.ps1 | iex. O desktop se atualiza sozinho pelo latest.json desta release. Plataformas: CLI para Linux x86_64/aarch64 (binario, .tar.gz, .deb, .rpm, AppImage), macOS Intel e Apple Silicon, Windows x86_64 e ARM64 (.exe, .zip) e Android/Termux (aarch64); desktop para Windows x86_64 (.msi, -setup.exe), Linux x86_64 (.deb, AppImage) e macOS Apple Silicon (.dmg); Garra Mobile .apk. Os pacotes alem do trio obrigatorio (linux-x86_64, windows-x86_64, macos-x86_64) sao best-effort: conferir a lista de assets da Release.

Limites conhecidos. As linhas manuais do gate de dogfood (D2 Windows, D5/D6 instalacao dos bundles desktop, D10 e o trecho WhatsApp do D1) ficaram para depois do tag, com aceite de risco registrado no PR de release, como na 0.4.6/0.4.7; a parte Linux (D1/D3a/D9) passou 12/12 contra o candidato. Novidades por tema na wiki: Novidades da v0.4.9 / What's New in v0.4.9.

Added​

  • Direitos dos titulares saem do papel: exportacao completa, correcao de e-mail com fluxo seguro e apagamento definitivo rastreavel. O GET /v1/me/export devolvia so dado de conta; agora inclui o conteudo pessoal do titular em todos os grupos dele — mensagens que escreveu (corpo completo), mencoes recebidas (trecho), arquivos enviados, memorias e tarefas — numa unica transacao que reaponta a GUC de tenant por grupo, com RLS fail-closed e filtro pelo id do titular nas duas pontas. O arquivo passa a declarar o que ficou de fora (exclusions) e o que bateu no teto (truncated_sections), porque exportacao incompleta em silencio e declaracao falsa de conformidade.
  • PATCH /v1/me aceita email sem nunca trocar o e-mail as cegas. users.email e a chave de login, entao trocar com base so num token de acesso e primitiva de account takeover. Como o repo nao tem infraestrutura de envio de e-mail, o pedido fica registrado em user_email_change_requests com estado pending + audit, e a aplicacao e passo manual do operador com verificacao de posse fora de banda (docs/legal/data-subject-requests.md). O endpoint tambem nao informa se o endereco pedido ja existe — responder isso viraria oraculo de enumeracao de contas.
  • DELETE /v1/me enfileira o apagamento que antes era "deferido a um worker futuro". A mesma transacao que marca a lapide e revoga as sessoes grava o pedido em account_deletion_requests com carencia de 30 dias; o novo account_purge_worker destroi conteudo e identificadores do titular (mensagem, comentario, memoria, arquivo + versoes + blob no object store) e deixa users como lapide status = 'purged'. A linha de users nao e removida porque cinco FKs NO ACTION apontam para ela e remove-la apagaria contexto de terceiros — mensagem de outro membro nunca e apagada por pedido de um. audit_events sobrevive desidentificado (actor_label, ip e user_agent viram NULL): e a unica prova de que o apagamento aconteceu. Sem object store ligado o pedido NAO e fechado — declarar completed com blob vivo seria mentir no audit.
  • O worker de apagamento roda com credenciais dedicadas e e retomavel. O role garraia_purge (migration 034) tem EXECUTE nas quatro funcoes SECURITY DEFINER de apagamento e mais nada — nenhuma privilegio de tabela, nao e BYPASSRLS; o pool valida SELECT current_user igual aos demais dedicados. O worker so nasce se GARRAIA_PURGE_DATABASE_URL estiver definida; sem ela o warn no log e explicito e o procedimento manual em docs/legal/data-subject-requests.md segue valendo. O progresso (db_purged
    • chaves de blob pendentes) e persistido na propria linha do pedido na mesma transacao dos deletes: uma queda no meio nao reexecuta a passada de banco nem fecha o pedido com blob vivo.
  • docs: guia de contribuição do usuário final (#1577). Faltava o lado do usuário do CONTRIBUTING.md: como rodar o pré-flight (garra config check, garra verify, garraia doctor) antes de abrir issue, qual template usar em cada situação, onde pedir ajuda (Discussions/Discord), por que vulnerabilidade nunca vai em issue pública e o caminho curto para virar contributor de código. Linkado a partir do SUPPORT.md. Fecha o item "Community" da operação — os demais itens da #1577 já saíram no #1588 (políticas de suporte, versões, depreciação e nota de decisão de SLA).
  • garraia doctor mcp valida que o MCP server autoprovisionado subiu de fato (#1595). O doctor confere instalação, mas nunca o plano MCP: uma falha de subida (cache npx corrompido, binário ausente, handshake morto) passava despercebida — o doctor reportava ok enquanto o filesystem estava morto e o agente ficava sem tools.

O novo doctor mcp spawna cada servidor stdio declarado pelo MESMO código do boot do gateway (build_mcp_tools: mesmo merge de config.yml/mcp.json, mesma resolução de vault:, mesma recuperação de cache npx), conta as tools do handshake e desliga os filhos antes de sair. Exit 0 quando tudo subiu, exit 2 quando algum servidor não subiu (útil em CI/scripts), 65 para config que não parseia. Servidor enabled: false é decisão do operador e não reprova; entrada HTTP fica marcada como não-verificada por este check.

O doctor raiz ganha um resumo CONFIG do plano MCP ([5/5], puro, sem spawn): quantos servidores declarados e se o filesystem está com a versão fixada (#1346) — o health-check de subida continua sendo a área explícita, para o onboarding rápido não virar download de npx.

Refs #1595

  • garra uninstall remove a CLI da maquina (binario, alias garra, wrappers do Termux e backups .old/.new); --purge inclui config, dados e o legado ~/.garraia; recusa com exit 78 quando o daemon pertence a uma unit systemd e com exit 64 sem terminal e sem --yes
  • garra_ask no servidor MCP HTTP (POST /mcp), com política própria (#1612). O orquestrador externo passa a perguntar ao LLM do Garra pela mesma ponte que já lê o estado e manda mensagens, com o envelope garra.ask.v1 do servidor stdio. A tool é desligada por padrão: só aparece com gateway.mcp_http.allow_ask: true, aceita só o modelo default do projeto enquanto ask_allowed_models estiver vazio, e tem orçamento próprio de ask_budget_per_minute (padrão 10) que nunca divide o de envio. O núcleo do garra ask foi extraído para o crate garraia-ask, compartilhado pela CLI e pelo gateway; garra ask e o garra_ask do stdio seguem iguais. Refs #1612
  • Expõe garra_send_message/garra_ask a orquestradores externos com identidade própria: mcp_http.orchestrators[] (nome + key_env, valor nunca em config), política por orquestrador (subseto de tools e destinos em interseção com a allowlist global), auditoria atribuível por chamada e mcp_http/redacao.rs (redação de padrões de segredo no payload e no erro). Lista vazia (default) mantém o comportamento de antes. Refs #1613
  • garra_agent no servidor MCP HTTP (POST /mcp), para delegação de tarefas (#1615). Um orquestrador externo passa a pedir ao Garra uma tarefa completa — ler e gravar arquivos dentro das raízes liberadas, buscar na web, ver o git e, só com sandbox, rodar comando — e recebe o envelope garra.agent.v1 com o resumo de cada tool usada. É a mudança de spec da issue: até aqui o agente completo existia só no stdio. A tool é desligada por padrão e só aparece com gateway.mcp_http.allow_agent: true e com agent na lista tools do orquestrador. Tem orçamento próprio de agent_budget_per_minute (padrão 2) e teto de tempo agent_max_seconds (padrão 300, máximo 600), e não divide cota com envio nem com garra_ask. Entrada e saída passam pela redação de segredos, e nenhum prompt, resposta ou saída de tool vai para o log. O núcleo do agente saiu da CLI para garraia-gateway/src/agente_mcp.rs, usado pelo stdio e pela ponte, sem mudança no garra mcp-server. Refs #1615
  • Redacao extensivel da ponte MCP HTTP (#1626, item 4). Nova chave opt-in gateway.mcp_http.redact_patterns: lista de regex do operador (ate 32, 512 caracteres cada, sintaxe do crate regex, tempo linear) que se somam aos padroes embutidos e viram [REDACTED] nas mesmas superficies da redacao embutida: garra_send_message, o erro de garra_ask e a saida de garra_agent (prompt, resposta, erro e resumos de tool). Padrao vazio, que casa o texto vazio, longo demais ou invalido e ignorado e reportado como erro pelo config check, sem derrubar os demais. Default vazio: comportamento inalterado.
  • Teste de contrato do bash pela ponte MCP HTTP (#1626, item 3). Dois testes em mcp_http_contract.rs exercitam garra_agent de ponta a ponta com um provider falso que pede a tool bash: o comando roda e a saida volta ao modelo, e um comando destrutivo e barrado pela denylist do BashTool. Sem mudanca de comportamento em producao.

Changed​

  • Decisoes do dono entram nos rascunhos GA de negocio e legal (#1565, #1574, #1575, #1576). Segunda leva dos documentos de GA, agora com as decisoes registradas — tudo continua marcado como rascunho/proposta e nada vale sem a revisao humana indicada em cada arquivo. docs/legal/tos-draft.md ganha os tres contextos (Community MIT self-host vigente, Garra Cloud planejado, distribuicao via site), divisao de responsabilidades BYOK, suspensao com criterios objetivos e proporcionalidade, forum brasileiro preservado e identificacao da entidade como pre-requisito; docs/legal/privacy-policy-draft.md ganha a tabela de retencao proposta da cloud (mensagens 90d com opcoes 7/30d, logs 30d, auditoria 90d, backups ≤30d) e registra a divergencia doc-vs-codigo do retention_days de 730 dias; docs/legal/dpa-template.md passa a classificar controlador/operador por fluxo com inventario de suboperadores; docs/compliance/lia.md nasce (LIA das sessions condicionado a revisao legal externa) e o DPIA o referencia; docs/compliance/incident-response.md alinha a notificacao ANPD a orientacao de 3 dias uteis e ganha anexo de tabletop de 1h (execucao pendente); docs/src/pricing.md recebe a grades Community/Beta/Pro R$39/Studio R$149 com quotas e regras de franquia LLM; SUPPORT.md e docs/operations/ ganham canais de cloud e de titulares com marcadores PENDENTE e a regra de atendimento por gravidade independente de plano.
  • Request-path do gateway nega unwrap()/expect() (#1569, primeira leva). Dez modulos que recebem input externo (rest_v1/{me,search,chats,groups,uploads,tasks/labels}, api, admin/store, diagnostics_handler, runs_handler) ganham #![deny(clippy::unwrap_used, clippy::expect_used)], e clippy.toml libera os dois em #[cfg(test)]. As 4 ocorrencias de producao viraram erro tipado (400 em vez de panic na criacao de chat DM) ou constante checada em compile-time (iteracoes PBKDF2).
  • Fixture de Postgres no CI destrava os 10 testes de integração do gateway (#1570). Job novo Gateway Integration (Postgres) no ci.yml: service container de postgres:16.8-alpine + as env vars que o bootstrap do GatewayServer exige (GARRAIA_JWT_SECRET, GARRAIA_REFRESH_HMAC_SECRET, GARRAIA_LOGIN_DATABASE_URL, GARRAIA_SIGNUP_DATABASE_URL — segredos fictícios de CI, mesmo padrão do job e2e) + --include-ignored. Os 10 #[ignore] de auth_test.rs, gateway_integration.rs e voice_e2e_test.rs (causa-raiz única: server.run() saía em silêncio sem Postgres desde o fix/ci-triage-2026-04-15) agora rodam de verdade no CI; a mensagem de cada #[ignore] passa a apontar o job e o comando local. O #[ignore] de throughput do markdown.rs permanece — é sonda, não dívida. Também neste PR: baseline .quality/baseline.json re-medido com freeze-baseline.py --adopt-current-file-metrics --reason '#1570' (campos de tamanho passam a descrever o repositório atual; coverage 72,0% e audit 0 critical/0 high preservados), workflow renomeado de "Quality Ratchet" para "Quality Report" para o nome não prometer bloqueio que o modo report-only (plan 0064) não faz, e healthcheck no serviço ollama do compose (ollama list) — o garraia e os demais serviços já tinham.
  • Rascunhos GA de documentos legais e operacionais (#1574, #1575, #1576, #1577). Primeira leva dos documentos que faltavam para vender, todos marcados como rascunho/proposta — nenhum é válido sem a revisão/decisão humana indicada no próprio arquivo. docs/legal/dpa-template.md (template de DPA com suboperadores e processo de atualização), docs/legal/privacy-policy-draft.md (política de privacidade com os direitos implementados no gateway) e docs/legal/tos-draft.md (termos de serviço); docs/operations/ com support-policy.md, version-support-policy.md, deprecation-policy.md e sla-decision-brief.md (decisão default registrada: self-host sem SLA, com opções para o maintainer); docs/business/cost-model-inputs.md (insumos BYOK de custo para a precificação, sem decidir preço). O DPIA ganha correção factual: os quatro endpoints de direitos do titular (GET /v1/me/export, PATCH /v1/me, POST /v1/me/anonymize, DELETE /v1/me) estão implementados no gateway — a lista de pendências do documento estava desatualizada.
  • Dependencias de outubro atualizadas em bloco (#1631, #1632, #1633, #1634). tokio 1.53.3, serde_json 1.0.154, uuid 1.28.0, libc 0.2.190, zeroize 1.9.1, aws-sdk-s3 1.152.0, a familia opentelemetry 0.33.1, o Tauri 2.12.2 com os plugins global-shortcut 2.4.1, shell 2.5.0, notification 2.7.0, dialog 2.8.3 e updater 2.14.0 (o desktop deixa de arrastar sigchld, o signal-hook 0.3 e a arvore windows 0.61), @playwright/test 1.64 nos testes E2E e a imagem base do Dockerfile em rust:1.99-slim. Substitui os quatro PRs do Dependabot abertos com o mesmo conteudo, que ficavam vermelhos no cargo-deny por causa do #1630.

Removed​

  • Rascunhos legais e precificacao saem do repositorio publico (#1616). docs/legal/ (ToS, Privacy, DPA, direitos do titular), docs/src/pricing.md e docs/business/cost-model-inputs.md deixam de ser versionados apos a auditoria de exposicao de 2026-10-09 ter encontrado documentos marcados "RASCUNHO — NAO PUBLICAR" e o modelo financeiro interno (precos propostos, margens, breakeven) na main. Os arquivos continuam no disco local, agora cobertos pelo .gitignore como privados por padrao; versoes finais revisadas publicam em garraia.org. Historico git e forks existentes nao sao afetados.

Fixed​

  • 1568 — Desktop: DMG no macOS, artefactos de updater assinados e latest.json publicado. O desktop nao e vendavel como distribuicao empacotada: nao havia DMG no macOS, o tauri-plugin-updater estava ligado com pubkey vazio e nenhum workflow publicava latest.json — o botao de atualizar nunca funcionou. O item que bloqueia o canal distribuidor (assinatura de codigo EV/OV) continua externo e rastreado na propria issue.

  • tauri.conf.json: bundle.createUpdaterArtifacts: true e bundle.macOS.targets: ["app", "dmg"] explicitos — sem eles o bundler nao emite o .app.tar.gz nem o DMG que o updater e o macOS precisam.

  • release.yml: job build-macos-desktop (macos-latest) compila o DMG pelo mesmo cargo tauri build, e o job release passa a gerar e publicar latest.json assinado ao lado dos binarios — o endpoint que o updater do app ja consultava.

  • desktop.yml: espelho em PR (build-macos-bundles) para que mudanca de desktop seja verificada no macOS antes de existir tag, mesma janela de drift que o workflow ja fecha para Windows e Linux.

  • docs/installation.md: secao "Garra Desktop on macOS" e tabela consolidada "CLI vs Desktop — what each one supports", com o aviso de Gatekeeper (app nao assinado) espelhando o aviso de SmartScreen ja documentado para o Windows.

Refs #1568

  • fix(release): latest.json sem a tag "v" duplicada (vv0.4.8), DMG do macOS entra na release e assinaturas do updater Windows estagiadas — o gerador normaliza a versão, o glob do DMG cobre o nome real do bundler Tauri e o build-installer.ps1 estagia o .sig vizinho do .msi/-setup.exe que o Tauri 2.x emite (#1568)

  • Domínio dos caminhos de contato e compra corrigido para garraia.org (#1574). O docs/src/pricing.md e o CONTRIBUTING.md apontavam para garraia.cloud, que não resolve (NXDOMAIN): link de compra, demo, política de privacidade e os e-mails enterprise@ e security@ estavam num domínio morto. O domínio real do projeto é garraia.org — resolve, /pricing, /demo e /privacy respondem 200, e o MX existe (mailserver.purelymail.com). O SECURITY.md já dizia security@garraia.org; o CONTRIBUTING.md contradizia. Correção estritamente factual, decidida pelo maintainer em 2026-10-07. Os termos comerciais anunciados na página de preços (SLA, SOC 2, janelas de suporte) não foram tocados — publicar ou retratar termos continua sendo decisão comercial, rastreada na #1574.

  • 1574/1576 — docs/src/pricing.md deixa de afirmar como fatos coisas que não existem. A página de preços anunciava como existentes cinco coisas que o repositório não tem: certificação SOC 2, SLA de 99,5%/99,99%, hospedagem gerenciada, backups automáticos e cobrança por cartão. Cliente empresarial que lesse a página compraria algo que não pode ser vendido — e o produto sequer tem os serviços que a página descreve.

  • Banner de status no topo: rascunho interno, não publicado, não vigente, com o inventário do que não existe e as issues que rastreiam cada lacuna (#1576 para decisão de negócio, #1575 para revisão jurídica, #1565 para conformidade).

  • Plano Pro e Enterprise reescritos: cada item marcado como proposto e, quando não operacional, dizendo isso explicitamente. SLA vira "propsto, não contratual"; conformidade vira "nenhuma certificação, conformidade em aberto".

  • Tabela comparativa ganha a coluna de status por célula — nenhum "Sim" sobrevive sem a marca de que não existe.

  • FAQ reescrita sobre o estado real: self-hosted é o único modo, não há cobrança nem dashboard de uso, e a política de privacidade ainda não foi publicada (o link antigo apontava para uma página que não existe, num domínio que não resolve).

Nada foi inventado no sentido inverso: os números da proposta continuam lá, rotulados como hipótese para decisão do maintainer. Corrigir afirmação falsa não é tomar decisão comercial.

Refs #1574, #1576, #1575, #1565

  • fix(agents/gateway): erro de execução (exit 126/EACCES) no Termux ganha diagnóstico do contexto restrito do launcher — a restrição W^X do Android veta execve em /data por filhos de processos lançados via linker64; o bash tool e o npm ci do bootstrap agora explicam a causa e apontam o lançamento pelo shell do Termux (#1591)
  • Allowlist do bash avalia encadeamento segmento a segmento (#1592). A allowlist do bash negava QUALQUER comando com |, &&, || ou ; — era a opcao fail-closed de #1105, e por isso nunca houve bypass, mas derrubava o pipeline legitimo: com git * e grep * declarados, um git status | grep modificado morria no tier arriscado. Agora os separadores de estrutura sao quebrados em segmentos (varredura ciente de aspas, a mesma de #1579) e a lista confere CADA segmento: o comando casa so quando TODOS casam. git status | sh segue negado enquanto sh nao estiver declarado, e um pipeline nao concede autoridade que o operador ja nao tenha dado — a | b com os dois declarados equivale a duas chamadas de tool seguidas, que o modelo ja podia fazer. O que NAO foi afrouxado: substituicao de comando ($(...), crase), redirecionamento (<, >), & solto (background escapa do timeout e da supervisao), nova linha e separador pendurado/colado continuam negando o comando inteiro, porque nenhum deles e um segundo comando que se possa conferir contra a lista. A denylist do safety_gate segue na frente de tudo. Mensagem de negacao e prompt do HostComAllowlist atualizados para a regra nova.
  • Timeout do LLM ganha janela por provider e erro que nomeia a chave certa (#1593). Um host lento (build Rust de 20-40 min no Termux, modelo grande sem GPU) batia nos 120s de timeouts.llm.default_secs e a resposta inteira era descartada — sem como aumentar a janela só para o provider lento, e com um erro que não dizia o que fazer.

Agora llm.<nome>.timeout_secs sobrescreve a janela global para aquele provider (ausente = global; config check avisa em 0, como no valor global), e o card de erro de timeout nomeia as duas chaves que o operador pode aumentar. A mensagem mantém o prefixo da classe e o texto do reqwest palavra por palavra (#1249).

Correção de semântica junto: timeouts.llm.default_secs: 0 (e o novo timeout_secs: 0) agora significa de fato "sem timeout" — antes virava Duration::ZERO, um sleep já vencido que fazia toda chamada LLM falhar em milissegundos, o oposto do que a mensagem do config check prometia.

Refs #1593

  • Telegram: falha de rede no GetMe deixa de ser reportada como token inválido, e ganha retry (#1594). O connect() fazia um único get_me() e tratava QUALQUER erro como "Telegram token inválido" — sob Termux/Android uma falha de SSL/DNS (Bionic sem SSL_CERT_FILE) mandava o operador trocar um token que estava certo, e o canal nem subia.

Agora o erro é classificado: Unauthorized/Not Found (a API respondeu) continua "token inválido" e falha de primeira; rede/IO é transitória e re-tenta com backoff exponencial (3 tentativas) antes de falhar — e a mensagem final diz que NÃO é o token e aponta o diagnóstico de rede (Termux: SSL_CERT_FILE, bloco Termux do garraia doctor). Cada tentativa loga classe, contagem e o erro cru para o diagnóstico em ambiente restrito. A política de retry é genérica e testada sem rede: transitório recupera, token inválido não gasta retry, esgotado devolve a contagem certa.

Refs #1594

  • garra update e garra rollback deixam de sair com exit 0 na falha (#1603). Os ramos de erro imprimiam a mensagem no stdout e o processo devolvia sucesso, escondendo a falha de scripts e automacao. A mensagem agora vai para stderr com a cadeia de erro completa e o processo sai com 70 (sysexits EX_SOFTWARE), no padrao ja usado por config check, doctor e desktop.

  • fix(cli/gateway): .env e lido apenas do diretorio atual, sem walk-up pelos pais — dotenvy::dotenv() e dotenvy::from_filename(".env") sobem os diretorios pais ate achar um .env, e um arquivo na raiz do repo injetava GARRAIA_CONFIG_DIR, GARRAIA_EXECUTION_PROFILE e credenciais em todo comando garra e em todo boot do gateway rodado de dentro de uma subpasta; CLI e gateway agora usam dotenvy::from_path(".env"), que e File::open puro: o .env do proprio diretorio continua valendo, o do pai deixa de valer (#1604)

  • docs(channels): channels.md para de dizer que whatsapp_linked nao existe no config.yml — o canal de dispositivo vinculado tem canal proprio na garraia-channels, validacao no config check e entrada documentada (type: whatsapp_linked) no whatsapp.md com a Access Policy v2 do ADR 0025; o texto antigo apontava para uma ausencia que ja nao existe (#1605)

  • 1611 — Workers de uploads e de recorrencia libertam o advisory lock na MESMA sessao que o pegou. Os dois workers adquiriam pg_try_advisory_lock numa conexao do pool e tentavam liberta-lo noutra, via tokio::spawn dentro do Drop de um guard. Advisory lock no Postgres e por sessao: pg_advisory_unlock chamado noutra conexao devolve false e nao faz nada. Como o sqlx devolve a conexao ao pool sem resetar a sessao (sem DISCARD ALL, e o ping() do test_before_acquire e so um write_sync), o lock sobrevivia a todo tick seguinte. Efeito real: a partir do primeiro tick as demais replicas ficam permanentemente bloqueadas de varrer, e na vencedora a varredura degrada — o lock e reentrante por sessao, entao um tick posterior que cair na MESMA sessao do pool segue funcionando. Perda silenciosa: nenhum dado, nenhuma violacao de RLS, so a janela contratada de expiracao/recorrencia que deixa de ser cumprida.

  • O padrao correto ja existia em account_purge_worker.rs (escrito no #1610): adquire UMA PoolConnection, fixa ela do acquire ao unlock, e se o unlock devolver false fecha a conexao para que o Postgres solte o lock junto com o backend. Os dois workers passam a seguir esse mesmo padrao, com o trabalho extraido para locked_tick(conn, ...) sobre a conexao ja fixada — assim, se a sessao morrer no meio do lote, o lock cai junto e a retomada e feita pelo proximo tick que conseguir o lock.

  • uploads_worker.rs e tasks_recurrence_worker.rs sao duas copias do mesmo defeito; corrigir so uma seria tratar sintoma. Literais LOCK_SQL/UNLOCK_SQL como const (o sqlx 0.9 so aceita &'static str, e manter lock e unlock no mesmo literal impede que os dois divergam).

  • Defeito independente achado pelo teste de regressao: tasks_recurrence_worker vinculava batch_size: i64 (bigint) contra due_task_recurrences(timestamptz, int) (migracao 033). O Postgres nao faz cast implicito de bigint na resolucao de funcao, entao toda varredura morria antes de varrer com "function due_task_recurrences(timestamp with time zone, bigint) does not exist". Corrigido com .bind(batch_size as i32), o mesmo cast que uploads_worker ja fazia.

  • tests/rest_v1_task_recurrence_worker.rs (novo) e tests/rest_v1_uploads_delete_worker.rs (estendido) contam os holders do lock via pg_locks e falham se ele nao voltar a zero. Ambos incluem um auto-teste do detector: eles primeiro provam que DETECTAM um lock segurado, para que uma refactor futura nao consiga deixar a query devolvendo sempre 0 e deixar as assercoes verdes sem provar nada. Ambos foram verificados contra o codigo antigo: falham com left: 1, right: 0.

Refs #1611

  • Ledger: o line das 4 entradas pré-existentes #40, #41, #42 e #115 volta ao span do alerta vivo (738/749/870/2201). O PR #1619 tinha comitado a linha derivada pelo check-ledger-anchors.py a partir do código atual (739/750/871/2205), mas o codeql-reapply-dismissals.sh --apply valida o line contra o span do alerta registrado no GitHub — e enquanto o CodeQL não reanalisar a main, esses números divergem e o --apply aborta fail-closed (exit 2), bloqueando o dismiss dos #181–#188 junto. Contrato dos dois ferramentas: anchors deriva do código, reapply exige o alerta vivo; o estado correto de uma entrada cujo alerta ainda não foi reanalisado é o span do alerta. O reancoragem automático só entra depois da re-análise (ou via codeql-rekey-ledger.py, que é feito pra isso).

Refs #1618

  • O loader valida GARRAIA_CONFIG_DIR na ingestão: valor placeholder de .env.example (ex.: /custom/config/path vindo do .env do CWD) ou vazio agora cai no config_dir padrão com aviso, em vez de derrubar qualquer comando com EACCES (os error 13) opaco — inclusive o garra config check (#1620).
  • Schema de garra_agent anuncia o teto do operador (#1626). timeout_secs passa a sair com maximum e default derivados de agent_max_seconds, em vez de 600/300 fixos; antes o schema dizia que um valor valia e a recusa vinha depois.
  • Dois testes que falhavam por corrida entre testes paralelos deixam de piscar no CI (#1633). uninstall_smoke copiava o binario para um tempdir e o spawn recebia ETXTBSY quando outro teste do mesmo processo ainda tinha o descritor de escrita da propria copia aberto entre fork e exec — agora o spawn tenta de novo nesse erro e so nele. hub_inacessivel_registry_fica_vazio obtinha uma porta "sem nada escutando" com bind + drop, e os hubs falsos dos testes vizinhos podiam herdar a mesma porta efemera no intervalo; o listener agora fica vivo e recusa cada conexao aceita.

Security​

  • security(release): cada asset da release ganha attestacao de proveniencia verificavel com gh attestation verify (#1606)
  • bump music-metadata de 11.15.0 para 11.16.1 na ponte WhatsApp, fechando os alertas Dependabot #47 (exaustao de memoria no parser EBML) e #48 (loop infinito sincrono em MP4 stsd com sample-entry size 0); dependencia transitiva de @whiskeysockets/baileys, dentro da faixa ^11.12.3 — so o lockfile mudou
  • Ledger do CodeQL recebe os 8 alertas rust/path-injection (high) de uploads.rs e session_workspace.rs (#181–#188). Auditoria por entrada concluiu falso-positivo nas duas causas-raiz: o caminho do staging nasce de Path<Uuid> tipado (extractor do axum rejeita não-UUID com 400 antes do handler) e o workspace de sessão nasce do digest SHA-256 hex de 32 chars do session_id — nenhum dos dois charset expressa traversal, e o CodeQL Rust pack não modela as duas transformações como sanitizer. As 8 entradas entram no .json e no .md com justificativa, disambiguators nos snippets ambíguos (#186/#187) e regressão nova o_nome_do_subdiretorio_e_sempre_hex_de_32_nunca_escapa_charset, que assera len/charset para ids adversariais. O checker derivou linhas novas para 4 entradas pré-existentes (#40, #41, #42, #115), mas o line de uma entrada espelha o span do alerta vivo (é o que o --apply valida) — enquanto o CodeQL não reanalisar, código e alerta divergem e o valor derivado derruba o --apply fail-closed; as 4 linhas seguem o alerta, e o reancoragem automático só entra após re-análise ou via codeql-rekey-ledger.py (corrigido no PR seguinte). Dismissals aplicados via codeql-reapply-dismissals.sh --apply após o merge.

Refs #1618

  • Vinte e quatro advisories RUSTSEC fechados de uma vez (seis do quinn-proto, quatro do rustls, treze da arvore wasmtime/wasmtime-wasi, um do unsafe-libyaml), e o cargo-deny volta a passar sem nenhuma excecao nova (#1630). rustls 0.23.45 -> 0.23.46 (RUSTSEC-2026-0346 a -0349: Debug de sessao vazando segredo TLS, dado bufferizado apos rejeicao de ECH, descompressao brotli de certificado ate 1 GiB antes da autenticacao e downgrade de key exchange por ServerHello nao autenticado); quinn-proto 0.11.16 -> 0.11.18 (RUSTSEC-2026-0338 a -0343: panics e exaustao de memoria acionaveis pelo peer remoto); wasmtime/wasmtime-wasi 49.0.2 -> 49.0.3 (RUSTSEC-2026-0352 no wasmtime-wasi, escape do sandbox de filesystem ao renomear symlink com barra final, e RUSTSEC-2026-0353 a -0364 no wasmtime: panics do host no component model, limites de handle table e tags desalinhadas — sem subir o MSRV, que segue 1.96); chacha20 0.10.0 (yanked) -> 0.10.2. O serde_yaml 0.9.34, que arrasta o unsafe-libyaml arquivado (RUSTSEC-2024-0449, sem versao corrigida), e trocado pelo fork mantido serde_norway 0.9.42 — mesma API, mesmo comportamento, unsafe-libyaml-norway por baixo — em garraia, garraia-config, garraia-gateway, garraia-learning e garraia-skills. Nenhuma entrada foi adicionada ao ignore do deny.toml nem do audit.toml.

[0.4.8] - 2026-10-07​

Seguranca no topo desta release. Quem esta na 0.4.7 executa bash no host sem fronteira de allowlist: a 0.4.8 estreia o modo HostComAllowlist (#1272), em que agent.bash_allowlist deixa de ser dispensa de confirmacao e vira limite real — todo comando fora dela e negado antes do tier arriscado, com a denylist do safety_gate sempre na frente. O wasmtime que alimenta esse sandbox sobe de 48.0.2 para 49.0.2 em dois passos (#1524, #1559), fechando onze advisories — quatro no salto para 49.0.1 e sete no patch para 49.0.2, este sem elevar o MSRV. A secao Seguranca abaixo detalha advisory por advisory.

Alem da fronteira de bash, esta release consolida a coleta de cobertura que o quality ratchet sempre perdia (#1566), religa os seis canais que o README ja anunciava (#1567), corrige o escape de aspas na allowlist do bash (#1579) e traz o restante das correcoes listadas abaixo.

Adicionado​

  • Bash roda no host restrito a agent.bash_allowlist (#1272). Nova exposicao HostComAllowlist: quando a config declara padroes de comando (prefixo* no fim ou comando exato), o BashTool entra em modo allowlist-only — a allowlist deixa de ser dispensa de confirmacao e vira FRONTEIRA, todo comando fora dela e negado antes do tier arriscado, com a denylist do safety_gate sempre na frente.
  • docs/voice.md ganha a secao "Known pitfalls with the stock app" (#1539). Duas armadilhas de quem sobe o multilingual_app.py do Chatterbox pela primeira vez: a sonda de saude que batia na rota errada (bug do gateway, corrigido na #1538 — a secao diz para atualizar, e so descreve o workaround para quem esta preso em 0.4.7 ou anterior) e os prompts de voz default, que sao URLs https:// passadas cruas ao librosa.load e quebram a sintese sem voz enviada. O passo de troubleshooting "TTS not responding" para de mandar consultar /health, rota que o Gradio nao serve.
  • Tool telegram_send_voice: a Garra fala por iniciativa propria (#1557). Sintetiza o texto (ate 1000 caracteres) com o TTS configurado e entrega como voice message no Telegram. O destino e a mesma decisao do telegram_send (chat da sessao, ou chat_id explicito so se estiver em proactive_chat_ids), compartilhada em resolver_alvo_telegram. Mapeada para MessageSend na tabela de capacidades, com teto proprio de 2 vozes por minuto por sessao. Falha de TTS vira erro explicito, sem fallback silencioso para texto; a tool some da lista quando falta TTS ou o canal esta offline. O adaptador Telegram aceita MessageContent::Audio apenas com caminho local.
  • Web Console: pagina global Agents & Permissions (#1433). Generaliza a pagina WhatsApp Access para um lugar so: principais e suas capacidades efetivas por canal, pelo mesmo motor de politica (ADR 0025) e pelo vocabulario do registro de capacidades (#1385), sem logica de autorizacao duplicada. Nova rota de leitura GET /admin/api/permissions/overview (guarda Channels/Read) devolve o perfil de execucao, a legenda das classes (cada uma marcada sensivel ou nao), e cada canal suportado com a sua fonte de politica — hoje so o whatsapp_linked tem motor por principal, os demais sao listados com honestidade (policy: not supported yet / defaults). Para o whatsapp_linked, a matriz por principal traz as classes efetivas (teto do principal interseccao registro) com as sensiveis destacadas; identidades so por ...1234. A pagina nao edita: leva ao caminho unico da pagina WhatsApp Access. Elevacao sensivel (conceder a um principal uma classe mutante que ele nao tinha) passa a exigir confirmacao explicita ENFORCADA no servidor: POST /admin/api/whatsapp/access recusa com 409 elevation_confirmation_required sem confirm_elevation: true. Responsivo (360px, matriz rola no proprio container) e acessivel (ARIA em regioes, tabela e avisos).
  • Exportar e importar a politica de acesso do WhatsApp, sem segredo (#1435). garraia whatsapp access export [--out <arquivo>] [--redact-pii] e GET /admin/api/whatsapp/access/export produzem um documento versionado garraia.access-policy (v1) montado campo a campo por allowlist da politica (admissao, default, niveis, grupos, bloqueios): nunca serializando a secao, entao token, cofre e material de sessao ficam de fora por construcao. --redact-pii mascara as identidades para os quatro ultimos digitos e marca o export como nao-importavel; na API o export e redigido por padrao e o export cru (?redact_pii=false) exige Channels/Update e e auditado. garraia whatsapp access import <arquivo> [--confirm-widening] [--dry-run] e POST /admin/api/whatsapp/access/import validam formato e versao (formato desconhecido e versao de major futuro sao recusados), recusam export redigido, substituem a politica inteira (limpando o legado e preservando enabled/default_mode/sessao), mostram o impacto por principal pelo mesmo motor do --dry-run e exigem confirmacao explicita para qualquer alargamento. A escrita e atomica com backup config.yml.import-bak (0600, sem seguir symlink) para rollback, e o import vai para o audit como a acao import.

Alterado​

  • garraia doctor mostra o shell do agente no Termux (#1560). O bloco Termux ganhou o item Tool bash, decidido pela mesma funcao do gateway e do mcp-server: sem docker/podman (sempre o caso no Android) e sem agent.bash_allowlist, o item aparece como aviso com a receita do modo allowlist-only no host (#1272), em vez de o operador so descobrir pelo proprio agente. A descricao do modo HostComAllowlist em doctor, /api/diagnostics e garra_status agora diz explicitamente "SEM sandbox". docs/installation.md documenta o shell no Termux.

Corrigido​

  • .quality/baseline.json deixa de dizer coverage_pct: null (#1566). O #1573 consertou a corrida que impedia o ratchet de achar o lcov.info e fixou os numeros num doc versionado, mas o baseline estruturado — o arquivo que o compare.py de fato le — continuava em {"coverage_pct": null, "status": "not_collected_yet"}, congelado em 2026-09-22 (af3227c). Agora traz 72,00% de linha (110.863 / 153.982, 451 arquivos), medidos pelo job Coverage (cargo-llvm-cov) do run 37538157886 em main no commit 13e6f1dc, e gravados pelo freeze-baseline.py a partir do lcov.info daquele run — nao a mao, o invariante anti-fraude continua valendo. No mesmo passo o ratchet estrito aperta o max_file_lines de 10.513 para 6.933, o valor real, e corrige o max_file_path para crates/garraia-config/src/check.rs: o antigo apontava para garraia-agents/src/runtime.rs, que ja nao e o maior arquivo do repo. Os contadores files_over_{700,1500,2500} ficam nos valores mais apertados (112 / 40 / 16) em vez de serem relaxados para a medicao de hoje (123 / 45 / 18) — o relatorio passa a registrar essas tres como regressao real, o que elas sao. O AI Quality Ratchet segue report-only: nenhuma PR e bloqueada por ele.

  • Piso de cobertura que derruba o build (#1566, criterio 2). O job Coverage (cargo-llvm-cov) media, publicava o comentario de PR e seguia — observabilidade, nao controle. Agora o scripts/ci/coverage_gate.py reprova o job abaixo de COVERAGE_FLOOR_PCT, hoje 70.0. Proveniencia do numero: 70% de cobertura agregada do workspace e uma decisao nova do maintainer (2026-10-07), guardrail adicional — nao e uma exigencia literal do ROADMAP, cuja meta ">= 70%" vale para crates de dominio (garraia-agents, garraia-db, garraia-security, garraia-workspace). A medicao de 72,00% deixa ~2 pp de folga. O gate le o lcov.info pelo mesmo parse-llvm-cov.py que escreve o baseline, para que os dois numeros nao possam divergir por existirem dois parsers; compara com >=, inclusivo; e e fail-closed — lcov.info ausente ou vazio reprova, porque um run nao medido nao e um run aprovado. Fica como ultimo step do job, depois dos artifacts e do comentario, para que a reprovacao nunca esconda a informacao de quem vai consertar. Nao e o AI Quality Ratchet e nao antecipa o PR-4 do plan 0064: sao controles independentes.

  • .mcp.json da raiz volta a subir o servidor MCP (#1536). A entrada apontava para ./target/release/garra, um caminho relativo a um binario que so existe depois de cargo build --release e so resolve se o cliente MCP abrir exatamente na raiz do repo — em worktree limpo o servidor nunca subia. Agora usa garra do PATH, como o mcp.json.example sempre usou. No mesmo arquivo faltava GARRAIA_MCP_ENABLE_TOOLS=1, sem o qual tools/list anuncia apenas garra_ask e garra_agent nunca chega ao host.

  • Chatterbox saudavel deixa de ser reportado como fora do ar (#1538). A sonda de fallback batia em /gradio_api/config, rota que nenhuma versao do Gradio serve (4.x a 6.x servem o config em /config): com a raiz indisponivel o gateway dizia ❌ tts-chatterbox e mantinha o modo voz desligado contra um multilingual_app.py de pe. A cadeia agora e / → /config → /gradio_api/info, para na primeira 2xx, e e a MESMA nos dois lugares que sondavam separado (ChatterboxClient::health_check e o check tts-chatterbox do /api/health), que antes podiam discordar. O WARN da falha nomeia cada rota e o que ela respondeu, entao "nada escutando" (connect failed) e "de pe na rota errada" (HTTP 404) deixam de sair com a mesma linha sem status. docs/voice.md troca o curl /health do troubleshooting — rota que o app stock nao serve — pelas tres rotas reais.

  • Dois perfis llm: do mesmo tipo param de disputar um slot de provider (#1540). Os arms openai e ollama do bootstrap registravam todo perfil com o id do tipo, entao duas entradas provider: openai (um llama-server local e um Ollama OpenAI-compat, p. ex.) colidiam em "openai": get_provider so achava a primeira e a ordem de registro vinha da iteracao do HashMap de config — o provider efetivo, e o default, eram sorteados a cada boot. Em 0.4.6 a mesma config alternava o /api/health.model entre dois modelos, e no boot em que o Ollama ganhava toda conversa com tools quebrava. Agora cada perfil e registrado pela chave (with_name), como os arms de nuvem ja faziam, e agent.default_provider / fallback_providers / model=<chave>/... passam a enderecar a instancia certa. Perfil cuja chave e o proprio tipo (openrouter) nao muda; um agent.default_provider que nomeia o tipo sem existir como chave no mapa llm: agora cai num WARN explicito em vez de resolver para um perfil aleatorio.

  • O fallback de provider usa o modelo DELE, e o SSE para de engolir o erro do provider (#1541, defeitos 3 e 4). (3) O laco de fallback repassava o LlmRequest do primario intacto, model incluso — e model e o nome que aquele backend entende. Um llama-server local servindo glm53-flash caia, o openrouter recebia glm53-flash e respondia 400 is not a valid model ID; o usuario via all providers failed, como se o fallback nem existisse. Agora o model so sobrevive quando nomeia o proprio fallback (openrouter/auto indo para o openrouter); nos demais casos vira string vazia, que desde sempre significa "provider, use o seu modelo configurado" em todos os adaptadores — ou seja, o fallback passa a usar o llm.<perfil>.model que a config ja dizia. Vale nos dois lacos, batch e streaming. (4) Em stream: true o Err do turno era descartado por um if let Ok(..): o canal de deltas fechava sem nenhum delta e o SSE terminava em finish_reason: "stop" mais [DONE] — uma resposta em branco com cara de sucesso, indistinguivel de um modelo que escolheu nao falar. O turno agora reporta o desfecho por um canal proprio e o stream emite {"error": {...}} (convencao que vLLM/llama.cpp/LiteLLM ja usam) em vez de mentir stop.

  • Modelo HuggingFace servido pelo Ollama para de ser mandado ao openrouter (#1541, defeito 2 — o que sobrou depois da #1540). Prefixo nao registrado com barra ia para o openrouter, porque ele de fato proxia minimax/..., yi/... e outros fora da tabela de tipos. Mas hf.co/unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL tem duas barras, e id do openrouter e sempre vendor/modelo — mandar assim era 400 garantido. Agora so uma barra qualifica para a tentativa via openrouter; o resto cai no provider default. Nenhum id real do openrouter sai do caminho.

  • POST /v1/chat/completions sem model deixa de mandar "gpt-4" ao backend (#1541). O handler fazia body.model.unwrap_or_else(|| "gpt-4") e entregava isso ao runtime como se o cliente tivesse pedido, entao um llama-server ou Ollama local respondia 404 model 'gpt-4' not found — a API OpenAI-compat era inutilizavel justamente para o cliente que omite model porque quer o default do servidor. Agora o corpo sem model chega ao provider como o sentinel vazio que ele ja sabe resolver com o proprio configured_model (mesma decisao que o A2A tomou), e o campo model do response/SSE ecoa o modelo que de fato atendeu. Os outros tres defeitos da #1541 (prefixo de chave de perfil, model do fallback e erro engolido no SSE) seguem abertos.

  • restart para de criar daemon orfao quando o gateway roda sob systemd (#1542). garraia restart matava o daemon da unit e subia um processo novo por fora dela: o orfao ficava com a porta e a unit entrava em crash-loop contra Address already in use — 4606 reinicios em ~8h numa instalacao 0.4.6, com o log parecendo um MCP reconectando sem parar. A CLI agora le o cgroup de quem esta na porta e, se for uma unit systemd, recusa antes de parar qualquer coisa (exit 78), nomeando a unit e o systemctl [--user] restart certo. Recusa em vez de delegar porque a unit sobe pelo ExecStart dela e as flags da invocacao (--host/--port/--with-voice) sumiriam em silencio. Escotilha: GARRAIA_ALLOW_SYSTEMD_RESTART=1.

  • Remediation do MCP deixa de mandar rodar um subcomando que nao existe (#1543). O registro de capacidades e o check tools.capabilities do /api/diagnostics diziam garraia mcp restart <nome>, mas garra mcp so tem list, inspect, resources e prompts — o operador tentava o comando, tomava erro de clap e perdia tempo justamente no momento em que um servidor MCP estava fora do ar. Os textos agora apontam para o caminho real, POST /admin/api/mcp/<nome>/restart (o mesmo do botao Restart na aba MCP Servers do console), e um teste varre o fonte do gateway para o comando fantasma nao voltar. O texto agora tambem distingue retrying (o supervisor reconecta sozinho) de failed (os max_restarts acabaram e so o restart manual traz de volta), para ninguem reiniciar a mao um servidor que ja ia voltar.

  • bind_fail_closed_cli deixa de flakear por porta compartilhada (#1544). porta_livre() pedia uma porta efemera ao kernel e a liberava na hora; o kernel reusa porta liberada, entao dois dos seis testes que rodam em paralelo podiam receber a mesma. Um deles sobe o gateway de verdade e o deixa vivo por 3s, e era esse listener que o outro encontrava ao provar que a recusa nao escutou — Address already in use sem nada errado no binario. As portas entregues agora ficam num registro process-wide. So alvo de teste.

  • Perfis llamacpp e anthropic voltam a resolver por chave, e o fallback por tipo volta a funcionar (#1554). O #1540 deu id por perfil aos arms openai e ollama do bootstrap e deixou anthropic e llamacpp para tras. O llamacpp tinha um agravante proprio: o id que ele registra (llama-cpp, com hifen) nao e igual ao provider: que o config usa para escolher o arm (llamacpp), entao um perfil unico ja bastava para quebrar — nao resolvia nem pela chave nem pelo fallback por tipo, e agent.default_provider apontando para ele era silenciosamente ignorado (so um WARN no log) enquanto o gateway seguia atendendo por outro provider, com a config aparentemente correta. Com dois perfis do mesmo tipo, os dois arms repetiam a colisao de slot do #1540. Agora AnthropicProvider e LlamaCppProvider tem with_name e os dois arms registram pela chave do perfil, e resolve_registered_provider_id conhece a traducao tipo -> id canonico a partir de uma constante publica, em vez de um literal repetido.

  • agent.default_provider que nomeia o tipo volta a resolver quando ha um unico perfil daquele tipo (#1554). Isto e uma mudanca de comportamento em relacao ao #1540, que transformou esse caso num WARN: ao registrar todo perfil pela chave, o #1540 quebrou sem perceber quem tinha default_provider: ollama com llm.local.provider: ollama, porque o registro passou a ser local. A correcao do #1554 teria repetido o estrago com anthropic e llama-cpp. Agora o nome pedido e procurado em tres lugares: id registrado, chave de perfil e — novo — tipo cujo perfil foi registrado pela chave. O terceiro passo exige exatamente um candidato: com dois perfis do mesmo tipo o nome e genuinamente ambiguo e a recusa com WARN continua, porque escolher um seria reintroduzir o sorteio por boot que o #1540 existiu para matar.

  • bash: aspas agora protegem a prosa do argumento na allowlist (#1579). A varredura META_SHELL olhava a string crua e tratava parenteses, ;, |, $ etc. dentro de aspas como "comando composto" — um forja-ask com "(transcreva o conteudo completo)" na mensagem era negado mesmo com o prefixo forja-ask * liberado pelo operador. Agora a checagem segue a tokenizacao do shell: aspas simples literalizam tudo; aspas duplas mantem ativos so crase, escape e $ que abre expansao (o $ de "R$ 50" e literal); aspas nao fechadas continuam negando. Composto real (;, &&, pipe, $(...), redirecao, newline) segue nunca casando. Fecha a lacuna de usabilidade do modo allowlist-only que esta release estreia (#1272).

  • A cobertura que o quality ratchet sempre perdia, e o baseline versionado (#1566). quality-report.md trazia coverage_pct: None em todo run, e a causa nao era falta de instrumentacao: o job ratchet (timeout 10 min, ~3 min de trabalho) sempre olha antes do job coverage do ci.yml (timeout 45 min, 21-30 min de execucao) terminar, e o filtro conclusion == "success" descartava o run ainda em andamento. A busca passa a varrer os ultimos 20 runs por um completed+success e, quando o SHA da PR ainda nao fechou, cai para o ultimo run verde de main, gravando a procedencia do lcov.info no step summary (com NOT this PR explicito no fallback). Continua best-effort: nenhuma falha ali derruba o job. Novo baseline versionado em docs/coverage-baseline-2026-10.md fixa os numeros reais — 71,99% de linha no agregado, por crate, com a politica de exclusao aceita e a meta do ROADMAP reavaliada.

  • A tabela de canais volta a dizer o que o build default liga (#1567). README, README.pt-BR e ROADMAP §4.4 diziam que Google Chat, Microsoft Teams, Matrix, LINE, IRC e Signal estavam "implementados no crate, wiring no gateway pendente", e o badge anunciava "5 channels". Os seis estao ligados no build default desde antes disso: garraia-gateway liga as features de canal incondicionalmente e o server.rs instancia os seis no boot, sem cfg — Google Chat, Teams e LINE por rota de webhook, Matrix, IRC e Signal por conexao no boot com retry em backoff. Badge passa a 11, a tabela de status distingue 🔹 (ligado, cobertura unitaria no crate) de ✅ (ligado e coberto por teste de integracao do gateway), e a §4.4 do ROADMAP fica fechada com a pendencia real nomeada: falta teste de integracao do gateway para os seis.

Segurança​

  • wasmtime 48.0.2 → 49.0.1 fecha quatro advisories, e o piso de MSRV sobe para 1.96 (#1524). O 49.0.1 corrige GHSA-m63x-6p34-q65x e GHSA-jqpg-j7w6-42pr (combustivel gasto por callees de call_ref e por lifting de record dinamico deixava de ser contabilizado), GHSA-c9gc-w9vx-w86p (exaustao de memoria do host na escrita de corpo HTTP de saida) e GHSA-j2g9-4prp-pf6h (panico com datetime fora de faixa no set-times/set-times-at do filesystem WASI). Sem migracao de API no garraia-plugins/src/runtime.rs desta vez. A arvore do cranelift 0.136.1 declara rustc 1.96.0, entao o rust-version do workspace e o job MSRV check acompanham — quinto bump com essa forma, e o piso real continua ditado pela arvore do wasmtime, nao pelas demais deps.
  • wasmtime 49.0.1 -> 49.0.2 fecha sete advisories, e o piso de MSRV NAO sobe (#1559). Tres no wasmtime — RUSTSEC-2026-0325 e RUSTSEC-2026-0326 (corrupcao da heap do GC: import de tag WebAssembly com tipo errado, e rooting ausente para valores do GC vivos atraves de try_call) e RUSTSEC-2026-0327 (contagem de resultados do callback async-lifted de componente sem validacao, com estouro de buffer na pilha nativa) — e quatro no wasmtime-wasi: RUSTSEC-2026-0321 (o poll_oneoff do WASI preview 0 burlava a contabilidade de combustivel), RUSTSEC-2026-0322 (memoria do host alocada em excesso quando o guest nao tem stdio), RUSTSEC-2026-0323 (fd_readdir copiava padding de struct nao inicializado para a memoria do guest) e RUSTSEC-2026-0324 (guest derrubava o host com timestamp de filesystem anterior a epoch no wasip3). O Security - cargo audit agendado ficou vermelho em 2026-10-03 com os sete. Patch de lockfile apenas: o Cargo.toml ja pedia wasmtime = "49", entao nao houve mudanca de manifesto nem migracao de API no garraia-plugins/src/runtime.rs. E o primeiro bump de wasmtime em seis que nao eleva o rust-version — a arvore do 49.0.2 (cranelift 0.136.2, pulley/wiggle 49.0.2) segue declarando rustc 1.96.0, igual a do 49.0.1, entao o job MSRV check (1.96) e o piso do workspace ficam como estao.

[0.4.7] - 2026-09-28​

Release de correção curta, cortada para levar aos usuários um bug observado em produção na v0.4.6: um lote paralelo de ferramentas furava o orçamento de execução e derrubava o turno depois de tudo já ter rodado (#1523). O orçamento só era conferido antes de cada chamada ao modelo, então uma resposta com 15 ferramentas rodava inteira contra um teto de 10; no modo search — o piso do WhatsApp pessoal — o turno morria em execution budget exceeded sem o modelo ver um único resultado, e o canal respondia "Tente de novo em instantes" para um pedido que falharia de novo. Agora o orçamento é conferido por chamada: a que passaria do teto não executa e volta ao modelo dizendo por quê, e com o orçamento esgotado o modelo ganha uma volta final, avisada, para responder com o que já coletou.

Junto sai o que já estava verde na main desde a v0.4.6: os presets nomeados de permissão (#1434), que fecham mais uma peça da Access Policy v2 dando nome canônico (chat_only, read, developer, full_pod) a combinações de level/write pelo mesmo caminho único de mutação — apelido calculado ao vivo, não campo novo guardado no config.yml —, e a atualização das famílias utoipa (#1526) e opentelemetry (#1527) em bloco, com grupos novos no Dependabot para que facades acoplados parem de subir pela metade.

A v0.4.6 publicada não foi tocada: sua tag, sua Release e seus 55 assets continuam exatamente como saíram (docs/releasing.md §Rollback — nunca reutilizar tag publicada). Quem está na v0.4.6 recebe a correção por garra update.

Added​

  • Presets nomeados de permissao no WhatsApp pessoal (#1434). garra whatsapp preset <numero> chat_only|read|developer|full_pod e a acao preset da API admin gravam level e write de uma vez, pelo mesmo caminho unico de mutacao da Access Policy v2 (ADR 0025 §4bis): um preset e apelido para uma combinacao canonica, nao um campo novo no config.yml — nao ha rotulo guardado que possa divergir do que o portao aplica. chat_only nao libera ferramenta nenhuma e read so leitura, os dois seguros para contexto remoto; developer e full_pod compilam para o mesmo full, que por construcao nao tem teto proprio — quem decide e o modo mais o execution.profile (ADR 0024). O mesmo nome serve para o default do desconhecido e para os grupos — garra whatsapp access default|groups default|group ... --preset <nome> e o campo preset nas acoes default, group-default e group da API —, com as guardas de sempre: developer e full_pod sao full e continuam recusados em access.default. O documento de politica efetiva em JSON (garra whatsapp access --json e GET /admin/api/whatsapp/access) passa a trazer um rotulo preset calculado ao vivo do level/write, com custom para o que nenhum preset cobre.

Changed​

  • utoipa 6.0 e utoipa-swagger-ui 10.0 sobem juntos (#1526, #1529). As duas crates trocam o tipo utoipa::openapi::OpenApi entre si, entao bumpar so uma quebra a compilacao: a utoipa-swagger-ui 9.0.2 declara SwaggerUi::url(self, url, openapi: OpenApi) contra a serie 5.x, e com a utoipa em 6.0.0 o gateway parava num E0308: mismatched types — dois facades incompativeis no mesmo grafo. A utoipa-swagger-ui 10.0.0 foi re-lancada justamente para casar com a utoipa 6.0, entao o par so anda em conjunto. Nenhuma mudanca de codigo foi necessaria: as breaking changes da utoipa 6 (fim do ignore por expressao em ToSchema/IntoParams, utoipa-gen sobre syn 3) nao tocam nada que o gateway use. O build offline do Swagger UI segue pela feature vendored, agora via utoipa-swagger-ui-vendored 0.2. Para nao repetir o bump partido, o .github/dependabot.yml ganhou um grupo utoipa, no mesmo molde dos grupos wasmtime e opentelemetry.
  • Familia opentelemetry sobe para 0.33 numa unica mudanca (#1527, #1528, #1530, #1531). opentelemetry, opentelemetry_sdk e opentelemetry-otlp vao de 0.32 para 0.33 e tracing-opentelemetry de 0.33 para 0.34 no garraia-telemetry. O Dependabot abriu uma PR por crate e as quatro ficaram vermelhas: as versoes sao acopladas e uma sozinha deixa duas versoes incompativeis do mesmo facade no grafo. Um grupo opentelemetry no dependabot.yml passa a casa-las por pattern em qualquer update-type, como ja era feito para o par wasmtime/wasmtime-wasi desde os PRs #833/#838.

Fixed​

  • Lote paralelo de ferramentas deixa de furar o orcamento e de derrubar o turno (#1523). O modelo pode pedir varias ferramentas numa resposta so, e o orcamento so era conferido antes de cada chamada ao modelo: um lote de 15 rodava inteiro contra um teto de 10. No modo search (piso do WhatsApp pessoal, 10 chamadas por tarefa) o turno ainda caia em execution budget exceeded depois de tudo executado, sem o modelo ver os resultados, e o canal respondia "Tente de novo em instantes" para um pedido que falharia de novo. Agora cada chamada confere o orcamento antes de rodar: a que passaria do teto nao executa e volta ao modelo dizendo por que. Com o orcamento esgotado, o modelo ganha uma volta final, avisada, para responder com o que ja coletou, e nada que ele pedir nela roda.

[0.4.6] - 2026-09-27​

Release que fecha o acesso ao WhatsApp pessoal de ponta a ponta e faz o runtime parar de mentir sobre o que ele pode. A peca central e a Access Policy v2 (#1388, ADR 0025): channels.whatsapp_linked.access ganha modo de admissao explicito, default declarado para quem nao esta na lista, piso e teto por principal e politica independente por grupo, num motor unico que a CLI (garraia whatsapp access|level|write|block|unblock|users|remove|owner|unowner), a API admin e a nova pagina "WhatsApp Access" do Web Console compartilham — nao tres copias da mesma regra. Quem administra ve o efeito antes de confirmar, identidade aparece so pelos quatro ultimos digitos em toda tela e todo --json, e cada comando de permissao deixa rastro no audit. O garraia doctor whatsapp e o "Test WhatsApp" do console rodam o MESMO motor de diagnostico, e o wizard garraia whatsapp link termina mostrando a politica em vigor em vez de um "pronto" que nao dizia quem podia falar.

A segunda frente e a honestidade do runtime (#1381, #1387, #1416; ADR 0025 §5). Um registro de capacidades unico passa a ser a fonte de garra_status, do /api/diagnostics, do GET /admin/api/capabilities e da nota que o modelo le: cada capacidade tem estado (visible, denied, unavailable, unhealthy, not_configured), motivo legivel e remediacao, e o prompt proibe inferir ausencia a partir de invisibilidade — denied e "existe e nao esta liberada", nunca "nao existe". Junto vem a classificacao de toda ferramenta em classes fechadas de capacidade com teto por principal (#1385, #1392): sem classe declarada, modo restrito falha fechado. Ferramenta registrada mas nao operacional sai da lista chamavel em vez de ser oferecida e falhar (#1425).

Na seguranca, o MCP filesystem passa a ser confinado ao mesmo jail de sessao que governa as file tools nativas (#1482, #1383) — antes havia dois portoes com regras diferentes no mesmo turno; uma ferramenta MCP deixa de virar slash command, que era bypass do ToolGate (#1386); um session_id escolhido pelo cliente nao alcanca mais a conversa de um canal com humano do outro lado nem a do app mobile, na escrita e na leitura por id (#1462); o /api/diagnostics para de publicar caminho absoluto do host (#1465); e o mascaramento de telefone cobre os separadores comuns (#1514). Uma sessao sem projeto ganha um workspace padrao seguro isolado por sessao com diretorio 0700 em vez de NoRoots (#1378, #1449, #1463), e /project seleciona um projeto cadastrado por sessao, persistido no sessions.db e reconfinado pelas raizes do operador a cada boot (#1379, #1424).

Confiabilidade e operacao: circuit breaker por sessao e ferramenta no unico ponto de despacho — falha determinista pausa a ferramenta ate o fim do turno, timeout abre cooldown com backoff 15s→120s (#1417); registro local de confiabilidade de tools, MCP e canais, sem nada sair da maquina (#1438); fatia de dados da retencao de memoria e do ledger (#1436); e o gateway passa a servir MCP por HTTP em POST /mcp (#1513). O CI volta a subir sem depender de registry para o MinIO — o quay.io fechou a distribuicao anonima em 2026-09-24 e a imagem agora e construida do fonte da tag fixada (#1458) — e o runbook de release ganha o gate de dogfood D1-D10 obrigatorio antes de todo tag (#1439), com a parte Linux automatizada por scripts/dogfood/linux-clean-install.sh (#1426).

Sem mudanca de formato de config nem de sessao: garraia update a partir da v0.4.5 e direto. Quem ja tinha allow/owners no config.yml continua valendo — a Access Policy v2 le o legado e o access.groups.enabled declarado vence o reply_in_groups antigo (#1501).

Added​

  • Projeto ativo por sessao, persistido, e comandos de barra no WhatsApp por principal (#1379, #1424; ADR 0025). /project [list|<nome ou id>|clear] seleciona um projeto cadastrado para a conversa: o caminho e confinado de novo pelas raizes de projeto do operador (GARRAIA_PROJECT_ROOTS), vira o working_dir das file tools e do repo_search, e o vinculo fica no sessions.db (sessions.project_id): um garraia restart restaura o projeto na hidratacao da sessao — e um projeto que ficou fora das raizes nao volta (fail-closed). A resposta mostra nome e id curto, nunca o caminho. POST /api/projects e GET /api/projects passam a gravar e ler o banco quando ha sessions.db (antes viviam so em memoria). No WhatsApp pessoal, uma mensagem que comeca com / e decidida por principal ANTES de ir ao modelo: /help para todo admitido, /project para dono e usuario, /mode e /goal so para o dono (o nivel dos demais vem da politica de acesso); comando registrado fora dessas listas e recusado com motivo; o que nao e comando segue como texto. Selecionar projeto nao muda poder: o que se pode fazer nele continua sendo o portao do turno (modo e teto do principal).
  • Registro de capacidades do runtime (#1381, #1387, #1416; ADR 0025 §5). Uma funcao so (capacidades_registro::registro) reune o inventario do runtime, o portao do turno (nome e classe), a disponibilidade de cada ferramenta, o estado dos servidores MCP e a exposicao do bash numa visao por capacidade com estado — visible, denied, unavailable, unhealthy, not_configured — e motivo legivel por maquina e por humano (mais remediacao). garra_status passa a devolver capabilities (e a nota do prompt manda o modelo responder a partir dela: denied e "existe e nao esta liberada", nunca "nao existe"; unavailable repete a remediacao); o /api/diagnostics ganha tools.capabilities (contagens por estado e o que esta indisponivel/fora do ar/nao configurado); GET /admin/api/capabilities expoe o mesmo registro ao console. device_read/device_execute ficam indisponiveis (no_devices) sem dispositivo registrado; device_list continua, porque e ela que explica.
  • Classes de capacidade e teto por principal (#1385, #1392, ADR 0025). Toda ferramenta declara o que faz em classes fechadas (filesystem.read, filesystem.write, process.execute, network.read, message.send, device.*, memory.*, runtime.inspect, scheduling, mcp.read, mcp.write): nativas por tabela fechada, MCP pela operacao de filesystem conhecida ou pelas anotacoes do servidor; sem classe e fail-closed em modo restrito. ToolPolicy ganha allowed_capabilities, denied_capabilities e no_tools; o ExecContext ganha um teto (TetoDeCapacidades) composto por E com o modo em todo ponto do runtime — a lista que o modelo ve, o despacho e o garra_status. Os niveis chat|read|full e o write on|off do WhatsApp compilam para esse teto (politica_do_nivel): write mexe so em escrita de arquivo, nativa e MCP, e nunca desliga sandbox, jail nem confirmacao. A recusa diz se foi o modo ou a politica de acesso. O inventario de ferramentas expoe as classes.
  • Access Policy v2 do WhatsApp pessoal: channels.whatsapp_linked.access (#1388, #1390, #1391, #1392, #1399, #1412, #1421, #1423; ADR 0025). Quem fala com o agente e ate onde cada um vai, por principal: admission (restricted | open), default (o que um desconhecido recebe em open, nunca full), users (por numero ou JID: level: chat|read|full, write: on|off, role: owner, blocked: true) e groups (enabled, default e politica por JID de grupo). O nivel e o write compilam para um teto de capacidades composto por E com o modo da sessao: o teto so tira, nunca poe, e write mexe so em escrita de arquivo (nativa e MCP) — shell, dispositivo, mensagem e agenda sao controles independentes. Grupo nunca herda o papel do dono; bloqueado vence open, allow e pareamento; nivel desconhecido e chat; a secao inteira e relida por turno (um bloqueio vale na mensagem seguinte) e cada normalizacao deixa um aviso sem numero. Compatibilidade: allow, owners e reply_in_groups continuam valendo e, sem access:, nada muda — nivel e write so existem onde foram declarados; a excecao e quem entrou por codigo do /pair, que passa a ter teto read (credencial fraca). O log do turno ganha principal e alcance (etiquetas fixas, nunca identidade).
  • garra whatsapp users lista quem pode falar com o GarraIA (#1393). O status dizia so QUANTOS estavam autorizados, e quem tinha liberado tres numeros meses atras precisava abrir o config.yml a mao para saber quais eram. O comando novo mostra o papel (allow ou owners) e os quatro ultimos digitos de cada identidade — nunca o numero inteiro, nem na tela nem no --json, que sai como documento unico ({enabled, authorized, owners, users[]}) para script.
  • garra whatsapp remove <numero> revoga o acesso pela CLI (#1394). Ate aqui autorizar tinha comando e revogar so existia editando o config.yml. O remove e o espelho do allow: tira a identidade de allow e de owners pela mesma chave que o portao do gateway compara (o celular com e sem o nono digito e o mesmo), preserva as outras chaves da secao e nao desliga o canal. Remover um DONO exige confirmacao explicita — --yes/-y fora do terminal, pergunta com default NAO dentro dele —, e quem nao estava na lista sai 0, porque revogar e idempotente.
  • garra whatsapp owner / unowner administram o papel de dono (#1395). Ate aqui virar dono so acontecia junto com o allow --owner (ou dentro do link), e deixar de ser dono so existia editando o config.yml a mao — ou passando o remove, que tira o acesso junto. O owner <numero> grava em owners pela mesma escrita do allow --owner, com as mesmas duas portas: execution.profile = isolated-pod obrigatorio (64 fora dele) e confirmacao explicita, --yes/-y fora do terminal. O unowner <numero> tira o papel sem nunca tirar o acesso: quando a identidade so existia em owners, a entrada passa para allow na mesma escrita, entao nao ha instante no disco em que a pessoa fique fora das duas listas. Rebaixar funciona em qualquer perfil, de proposito — e em standard que um dono esquecido e um privilegio latente —, e a confirmacao vale para o ultimo dono, o unico rebaixamento que deixa a configuracao sem dono nenhum. Os dois comandos sao idempotentes, so imprimem os quatro ultimos digitos de cada identidade e aparecem no whatsapp users e no status na mensagem seguinte, sem reiniciar o gateway.
  • garraia whatsapp access, level, write, block, unblock: a Access Policy v2 pela CLI (#1396, #1397, #1398, #1399, #1400, #1401, #1413, #1414; ADR 0025). access imprime a politica efetiva inteira (admissao, default do desconhecido, grupos, e cada principal com piso, nivel e o que pode de fato — calculado pelo MESMO ToolGate do turno, nunca por um parser paralelo), com --json e, so localmente, --reveal. access open|restricted troca a admissao (open avisa e pede confirmacao ou --yes); access default chat|read [--write] define o que um desconhecido recebe em open (full e recusado); level <numero> chat|read|full e write <numero> on|off gravam nivel e escrita por identidade (write mexe so em escrita de arquivo, nativa e MCP); block/unblock; access groups on|off|default <nivel> e access group <jid> <nivel>; access reset volta ao seguro preservando donos e bloqueios (confirmacao ou --yes, idempotente). Toda mutacao aceita --dry-run: mostra o que mudaria e o impacto por principal (o que ganha e perde: escrita, shell, dispositivo, mensagem, MCP) sem gravar nem auditar. Toda mutacao aplicada vai para <data_dir>/audit/whatsapp-access.jsonl (0600, rotacao por tamanho, audit_max_bytes na secao): quando, quem, por onde, acao, alvo ...1234 e o resumo antes/depois — nunca identidade inteira, chave ou mensagem; access audit [--json] [--limit N] le a trilha. Um unico caminho de mutacao no gateway (whatsapp_linked_politica::mutacao), que a API admin e o Web Console reutilizam. Exit codes: 0 · 1 cancelado · 64 uso · 65 dado invalido · 70 config · 73 gravou mas o audit falhou.
  • API admin da Access Policy v2 (#1402, #1412, #1413, #1414; ADR 0025): GET /admin/api/whatsapp/access devolve a politica efetiva — o MESMO documento de garraia whatsapp access --json, produzido por whatsapp_linked_politica::visao — mais hot_reload (se o gateway rele a config a quente); POST /admin/api/whatsapp/access aplica uma mutacao (action: open | restricted | default | level | write | block | unblock | groups | group-default | group | reset, com identity/jid/level/ write/enabled e dry_run) pelo mesmo mutacao::aplicar da CLI — validacao antes de gravar (400 com a mensagem acionavel), escrita atomica 0600, impacto por principal (o que ganha e perde) e audit com origem: admin_api e o username do admin; GET /admin/api/whatsapp/access/audit le a trilha. Leitura com Channels/Read (viewer le), mutacao com Channels/Update (viewer recebe 403); cookie e CSRF dos layers do /admin. A API nunca revela identidade (so ...1234). O check whatsapp.access do /api/diagnostics passa a avisar access.admission: open (com o passo para fechar) e secao access com valor invalido.
  • Web Console — pagina "WhatsApp Access" (#1402, #1403, #1404, #1405, #1406, #1407, #1408, #1411, #1413; ADR 0025). Terceiro consumidor do caminho unico: le GET /admin/api/whatsapp/access (o mesmo documento da CLI) e muda por POST /admin/api/whatsapp/access. Resumo (canal, perfil de execucao, piso do dono, hot reload, contagens, avisos), admissao restricted / Anyone (open), default do desconhecido, grupos (liga/ desliga, default, politica por JID), formulario "Add phone / identity" com nivel e write, matriz por principal (piso, nivel, write, o que pode de fato) com seletor chat|read|full, toggle de write, Block/Unblock, Make owner/Demote e Remove, botao "Reset to safe defaults" e a trilha de audit. Toda mutacao passa por um preview (dry_run: mudancas + o que cada principal ganha e perde) antes de confirmar; elevacoes perigosas (open, owner, reset) trazem aviso. A pagina so conhece ...1234: age sobre uma linha mandando identity_last4, que o gateway resolve entre as identidades declaradas (ambiguo = 409); a identidade inteira nunca desce ao navegador. data-testid estaveis + spec Playwright tests/playwright/whatsapp-access.spec.ts. Motor: Mutacao::Papel (owner/unowner, preservando o acesso ao rebaixar o dono legado) e Mutacao::Remover (tira de allow, owners e access.users), tambem expostos na API (owner, unowner, remove).
  • Web Console mostra, por sessao, o principal do WhatsApp pessoal (dono, usuario, pareado, grupo...), o nivel e o modo EFETIVO do turno (piso do canal e escolha da sessao), e o projeto ativo sem caminho; GET /admin/api/sessions traz principal, level, write, chosen_mode, effective_mode, project_name e has_workspace (#1409). Botao "Capabilities" por sessao abre o painel daquela conversa (GET /admin/api/capabilities?session_id=), com o portao real do turno: o que esta visivel, negada, indisponivel, fora do ar ou nao configurada, com motivo e passo (#1415).
  • Web Console mostra o perfil de execucao no header (#1410). O perfil do ADR 0024 (standard | isolated-pod) so aparecia como uma linha dentro da pagina Diagnostics, e isolated-pod e justamente o perfil que da ao agente poder total dentro do pod — o operador precisava navegar para descobrir em qual dos dois o gateway esta. Agora um badge fixo no header le /api/settings/effective (auth-free, secret-free), mostra o valor e a origem (default | file | env) e, em isolated-pod, muda de tom e carrega no tooltip o mesmo aviso do boot: o pod e a fronteira de seguranca, nao o Garra, e como reverter.
  • Circuit breaker para ferramentas que falham repetidamente (#1417). No dogfood da v0.4.5 o modelo repetia repo_search varias vezes no mesmo turno depois de uma falha que nao ia mudar (sem repositorio) e de timeouts. O runtime ganha um breaker por sessao e por ferramenta (garraia_agents::tools::breaker, estado puro, relogio injetado), no unico ponto de despacho: falha deterministica (sem raiz para as file tools, repo_search sem repositorio) poe a ferramenta em pausa ate o fim do turno; timeout abre um cooldown que dobra a cada timeout seguido (15s ate 120s) e atravessa turnos; erro generico so abre depois de tres iguais no turno — e a recusa de caminho fora das raizes e generica de proposito, porque vale para um caminho e o modelo corrige o caminho na chamada seguinte. Em pausa, a chamada nao roda e o modelo recebe o motivo estruturado ("temporariamente indisponivel nesta conversa (no_roots | no_repository | timeout | repeated_error)") com a instrucao de nao repetir. Sucesso fecha o breaker da ferramenta; working_dir diferente num turno novo limpa a sessao. garra_status devolve breaker (tool, reason_code, reason) e breaker_means; a nota do prompt manda o modelo respeitar a lista. O agregado no /api/diagnostics fica para a #1438.
  • garraia doctor whatsapp (#1419). O caminho do WhatsApp pessoal de ponta a ponta numa passada: vinculo (LinkHealth, a mesma tabela do status e do console), chave da sessao (origem e prova de que abre), gateway e ponte (com o gateway de pe, o que o /api/diagnostics diz; sem ele, a linha diz que nao sabe), acesso (contagens de autorizados e donos), perfil de execucao (isolated-pod e sempre aviso, dizendo o que o dono ganha), raizes das file tools (o mesmo resolvedor do boot), MCP visivel no piso search (servidor inteiro, so operacoes de leitura, ou escondido — com a sintaxe para liberar) e provider padrao. Cada linha traz o proximo passo; --json fala o vocabulario do /api/diagnostics (ok/warning/error/not_configured); exit 0 tudo verde, 2 aviso sob --strict, 69 algo vermelho. Nenhuma linha carrega numero, LID, chave ou URL com credencial.
  • Web Console: "Test WhatsApp" com o MESMO motor do garraia doctor whatsapp (#1420). A tabela do doctor (fatos → linhas, agregado, exit code) saiu da CLI e mora em garraia_gateway::bootstrap::whatsapp_linked_doctor; a CLI virou consumidora e a saida dela (--json, tabela humana, exit 0/2/69) nao mudou. O gateway ganhou GET /admin/api/whatsapp/doctor?lang=pt|en (cookie do /admin, Channels/Read — viewer le), que colhe os fatos em processo — vinculo com a view REAL da ponte, chave da sessao pela mesma resolucao do boot (so quando ha sessao; nunca materializa arquivo numa rota de leitura), config viva, e as linhas do proprio /api/diagnostics por chamada de funcao, sem HTTP — e devolve { status, version, lang, checks } com checks identico ao report.checks do --json. Na pagina WhatsApp Access, um card no topo roda o teste: resumo, uma linha por check (vinculo, chave, gateway e ponte, acesso, perfil de execucao, workspace, MCP visivel no piso, provider) e, para cada linha nao-verde, uma acao segura — rolar ate a matriz de acesso, abrir MCP Servers / Providers / Configuration, ou copiar o passo de terminal (mostrado, nunca executado). Funciona com o canal parcialmente configurado (e o caso do CI). Nenhum segredo, chave de sessao ou numero sai: teste de integracao alimenta chave de gateway e de provider e varre o corpo; spec Playwright varre o HTML do card.
  • WhatsApp pessoal: toda mensagem que o portao recusa e contada por motivo (restricted_policy, unresolved_lid, blocked_user, channel_disabled, prompt_injection), com o final da identidade (...1234) e o instante, nunca o numero inteiro nem o texto. O Web Console (pagina WhatsApp Access) mostra as contagens e as ultimas 50, com a acao certa por motivo (autorizar, parear, desbloquear, ligar o canal) e um reset auditado; GET /admin/api/whatsapp/access traz rejections e POST /admin/api/whatsapp/access/rejections/reset zera; o /api/diagnostics (sem autenticacao) so ve as contagens. Retencao definida: desde o boot, zera no restart ou no reset (#1422).
  • scripts/dogfood/linux-clean-install.sh automatiza o dogfood Linux (D1/D9 da matriz de release) num container limpo (#1426, #1439). A v0.4.4 e a v0.4.5 sairam com CI verde e quebraram no caminho real de quem instala; o CI prova que o codigo compila, nao que o PACOTE funciona para uma pessoa. O script obtem um .deb x86_64 da CLI (artefato garraia-linux-packages de um run do release.yml via gh run download, ou build local empacotado com o mesmo nfpm pinado e o mesmo packaging/nfpm.yaml da release), sobe um ubuntu:24.04 cru com --network host, instala com apt-get install ./garraia.deb e prova o fluxo inteiro: garraia --version (e o alias garra), doctor --json (exit 0, JSON valido), doctor whatsapp --json (exit 69 sem WhatsApp, linha whatsapp.linked presente), config set-model apontando para o Ollama do host sem chave paga, start -d na 3899 ate /api/health healthy, sessao REST com resposta real do modelo, restart -d com sessao e config sobrevivendo (historico readotado do sessions.db, provider/modelo no health do processo novo) e stop com porta fechada e pid file removido. O curl/jq ficam do lado do host, entao o container nao ganha nenhuma ferramenta que um usuario nao teria. A evidencia (logs, JSONs, resposta do modelo, .deb testado, resumo.txt e resumo.json PASSOU/FALHOU por passo) vai para dogfood/linux/<data>/, gitignored. docs/releasing.md §1.5 registra o comando na matriz.
  • O wizard garra whatsapp link oferece a politica de acesso depois do QR (#1429; ADR 0025). Depois de perguntar o primeiro numero (e o dono, em isolated-pod), o wizard passa a perguntar, com defaults seguros: o nivel de quem acabou de entrar (chat | read | full, default read) e, fora de chat, a escrita de arquivo (default nao); e a admissao — so quem voce autorizar (restricted, default) ou qualquer numero (open). Escolher open mostra o default que um desconhecido recebe NESTA config e pede a mesma confirmacao do access open, com default nao. Cada resposta e gravada pelo mesmo motor do access (whatsapp_linked_politica::mutacao: validacao, escrita atomica 0600, audit com origem cli) — o wizard nao tem uma segunda forma de escrever politica, e Enter em tudo deixa o numero com read sem escrita e o canal fechado. Dono nao recebe pergunta de nivel (dono nao tem teto). link --allow <numero> [--owner] continua identico e scriptavel: nada novo e perguntado, o numero entra sem teto como o allow gravaria. O resumo final passa a ser o da politica efetiva (as linhas do access), no lugar do resumo do users.
  • O garra init oferece WhatsApp ao lado do Telegram (#1430). O passo de canal do wizard so sabia perguntar por Telegram, e quem instalava o GarraIA para usar no WhatsApp tinha de adivinhar que existe um garra whatsapp link separado. Ele virou uma lista: nenhum canal (o default — Enter sem ler continua nao conectando nada), Telegram, WhatsApp pelo numero pessoal, ou os dois. O rotulo do WhatsApp ja diz na lista que o caminho e o de aparelho conectado por um cliente NAO oficial, e escolher a opcao entrega ao mesmo fluxo de sempre — tela de consentimento com default nao, QR e a pergunta de quem pode falar, nada reimplementado no wizard. A entrega roda DEPOIS de o config.yml ser escrito, porque o vinculo grava na mesma secao channels.whatsapp_linked; um vinculo que nao complete (sem Node, QR nao lido, desistencia) nao derruba o init, que so diz como tentar de novo. Instalacao sem terminal nao chega ao passo.
  • Plano da chave de sessao e modos apertados no link do WhatsApp (#1431). SessionKey::plan diz o que o resolve faria no diretorio da conta (passphrase do cofre, session.key existente ou nova) sem criar arquivo nem derivar PBKDF2 — o mesmo predicado (usable_passphrase, passphrase vazia conta como ausente) governa plano e resolve, e a configuracao do wizard passa a poder mostrar o caminho seguro antes de criar qualquer coisa. SessionStore::harden_modes aperta o diretorio da conta para 0700 e todo arquivo regular dentro dele para 0600 (incluido o recusas-lid.json que o gateway grava) e devolve o que estava mais aberto, para a CLI avisar sem falhar; o salt passa a ser apertado ao ser lido, como a session.key ja era. Symlink dentro do diretorio nao e seguido nem reportado, modo mais fechado que o exigido fica como esta e fora de Unix a lista volta vazia.
  • Retencao de memoria e ledger: fatia de dados (#1436). garraia-config::retention passa a ser o dono das faixas de memory.retention e runs.retention_days: o config check delega para ele e o futuro PATCH /admin/api/retention vai recusar o que esses mesmos achados chamam de Error, com patch campo a campo (deny_unknown_fields) e a janela da limpeza manual. garraia-db::retention conta o que uma limpeza apagaria sem apagar (mesma clausula do DELETE, cobrada por teste), mede o tamanho dos dois bancos e grava a ultima limpeza no proprio banco: compact e prune_agent_runs anotam a execucao, entao worker, CLI e console passam a ver o mesmo lugar. A pagina e o endpoint do console vêm em fatia propria.
  • Registro local de confiabilidade de tools, MCP e canais (#1438). Modulo puro garraia_agents::observabilidade: por ferramenta, chamadas por desfecho (sucesso, erro, timeout, negada pela politica, indisponivel, recusada pelo breaker, aguardando confirmacao), aberturas do breaker, latencia em baldes fixos (p50/p95 aproximados) e ultima falha; por servidor MCP, quedas e reconexoes (o health monitor relata o transporte a cada tick, uma queda por transicao vivo -> morto, e o desfecho de cada reconexao automatica); por canal, conexoes, quedas e reconexoes; series de tamanho de armazenamento com teto. Sessao nao e dimensao, nome fora do registro cai em (unknown), tetos de cardinalidade, Instant injetado e lock sem panico. Nada sai da maquina: so nome de servidor e booleanos — nunca last_error, comando ou caminho. O endpoint autenticado que expoe o snapshot vem em fatia propria.
  • scripts/setup-toolchain.sh fixa a toolchain Rust localmente (#1452). O workspace exige rustc 1.95+, mas nada instalava/ativava essa versao automaticamente para quem clonava o repo — o sintoma era cargo check/cargo test falhando com "is not supported" ate rodar rustup update manualmente. Um rust-toolchain.toml commitado foi tentado (PR #1454) e quebrou os jobs de CI que fazem cross-compile com targets extras (Android, Windows ARM64): o arquivo tem precedencia sobre a toolchain que dtolnay/rust-toolchain acabou de instalar com esses targets. O script evita isso: usa rustup override set, que grava a preferencia em ~/.rustup/settings.toml (no HOME de quem roda), nunca em um arquivo do repo, entao o CI nao ve nem herda a mudanca.
  • O gateway virou servidor MCP por HTTP, em POST /mcp (#1513). O Garra ja falava MCP como cliente (secao mcp:) e como servidor por stdio (garra mcp-server, uma tool), mas nao a combinacao que um orquestrador externo precisa — servidor, por URL. Sem ela nao havia como apontar o Paperclip (ou qualquer host MCP que so aceite endereco) para este Garra. A ponte usa o rmcp que ja estava no workspace, e expoe cinco tools: garra_status, garra_list_chats, garra_read_history (com segredos redigidos pela mesma redact_secrets do log), garra_pair_status e garra_send_message. Transporte stateless com resposta em JSON, entao um curl confere o endpoint sem parsear SSE. Tudo desligado por default: gateway.mcp_http.enabled liga a rota, e ela recusa subir sem gateway.api_key — o gate de /api/* e passa-direto sem chave, o que serve para o console local e nao serve para uma ponte que entrega a lista de conversas do dono. O envio exige duas acoes independentes do operador (allow_send mais o destino em proactive_chat_ids), e uma tool que nao pode enviar nem e anunciada. O garra config check avisa nos dois erros silenciosos: ponte ligada sem credencial e envio ligado sem allowlist. docs/gateway-mcp-http.md.

Changed​

  • O modo search enxerga a leitura do MCP filesystem (#1384). O piso de todo remetente do WhatsApp pessoal escondia TODA ferramenta do servidor filesystem — inclusive read_text_file e list_directory — porque a whitelist so listava as nativas, e o modelo dizia nao ter como ler arquivo com o servidor conectado. A allowed ganha uma terceira forma de entrada, */<operacao> (a operacao exata, em qualquer servidor), e search passa a listar as dez operacoes somente-leitura do @modelcontextprotocol/server-filesystem (read_file, read_text_file, read_media_file, read_multiple_files, list_directory, list_directory_with_sizes, directory_tree, search_files, get_file_info, list_allowed_directories). Escrita (write_file, edit_file, create_directory, move_file) e qualquer nome desconhecido continuam fail-closed, e denied segue vencendo tudo. Os outros modos somente-leitura nao mudam nesta fatia.
  • docs/security/threat-model.md ganha a secao 5.18 (politica de acesso por principal do WhatsApp pessoal, ADR 0025): fronteira, as tres decisoes por turno, tabela de ameacas com a prova de cada uma e o risco residual (#1388).
  • garraia whatsapp status (e users) passam a dizer a admissao (restrita ou ABERTA, com o default que um desconhecido recebe) e o --json ganha admission e default_access; com a admissao aberta o aviso de "ninguem autorizado" nao sai, porque nao e verdade (#1399).
  • POST /admin/api/whatsapp/access (e o formulario Add phone do console) valida identity com a MESMA regra da CLI, agora no gateway (whatsapp_linked::numero): + e codigo do pais obrigatorios (formatos BR/US aceitos), 6 a 15 digitos, ou <digitos>@lid; entrada invalida da 400 com error e error_code estavel. Antes a API aceitava numero sem +, sem como saber se o codigo do pais veio — e um numero gravado sem ele nunca casa com o remetente (#1403).
  • Matriz por principal (garraia whatsapp access, GET /admin/api/whatsapp/access, pagina WhatsApp Access): ganha as colunas web (web_fetch), memory_read e memory_write (pela classe memory.*; hoje nenhuma ferramenta nativa a carrega, e por isso a coluna e falsa no piso search, que nao a nomeia), ao lado de arquivos, MCP, shell, dispositivos e mensagem — o minimo que a #1411 pede (#1411).
  • garraia whatsapp allow, remove, owner e unowner passam a auditar (#1414; ADR 0025). Os comandos legados gravavam allow/owners no config.yml sem deixar rastro, e a trilha de access audit so contava as mutacoes novas (level, write, block, open...). Agora cada escrita que de fato mudou algo vai para o mesmo <data_dir>/audit/whatsapp-access.jsonl, com o nome do subcomando como acao (allow, remove, owner, unowner; allow --owner e allow, e e o resumo depois que diz que o alvo entrou como dono), origem cli, o usuario do SO como ator e o alvo mascarado (...1234) — nunca a identidade inteira, chave ou mensagem. Repeticao idempotente (autorizar quem ja estava, remover quem nao estava) nao gera evento. O passo pos-QR do link, que e a mesma escrita do allow, audita do mesmo jeito. Mudanca gravada com o audit indisponivel sai 73, como nos comandos novos; no link o exit continua 0 (o vinculo valeu) e o aviso sai em stderr.
  • garra_status diz se as file tools tem raiz nesta sessao (#1416, #1418). O relatorio ganha o bloco file_tools — ready (bool), source (session_workspace | session_working_dir | declared | none), roots (contagem, so quando declaradas) e means (so quando ready = false: a frase que o modelo deve dizer em vez de prometer ler arquivo). Sem caminho nenhum, por isso sai inteiro tambem no turno restrito do WhatsApp, onde session.working_dir e retido. O resolvedor e o mesmo do boot e do /api/diagnostics (raizes_das_file_tools): so le metadado.
  • Registro de capacidades (garra_status, /api/diagnostics, GET /admin/api/capabilities): file_read, file_write e list_dir numa sessao sem raiz nenhuma saem como unavailable com reason_code: no_roots, e repo_search sem repositorio como no_repository, os dois com a remediacao /project <nome> — falta de contexto e um estado distinto de negada pela politica (que continua vencendo) e de nao configurada (#1416, #1381, #1387).
  • Ferramenta registrada mas nao operacional fica fora da lista chamavel do modelo (#1425, opcao B; #1418). Tool::disponibilidade() diz se a ferramenta esta utilizavel AGORA; o runtime consulta isso ao montar a lista de cada turno (junto com o portao de nome e classe) e antes de despachar: se o modelo pedir pelo nome uma ferramenta indisponivel, ela nao roda e a explicacao volta como resultado de ferramenta, com codigo (not_configured, channel_offline, ...) e motivo, distinta de "negada pela politica". telegram_send e a primeira a usar: so e chamavel com o Telegram configurado na config viva e conectado; desligar o canal na config tira a tool na mensagem seguinte, sem restart. NoRoots das file tools ganha mensagem propria e acionavel — "nenhuma raiz esta configurada para esta sessao ... selecione um projeto com /project <nome> ou configure agent.file_roots" — em vez da recusa generica de caminho fora das raizes (que continua identica para "fora" e "nao resolveu", sem oraculo de existencia).
  • O garra whatsapp link termina mostrando a politica de acesso em vigor (#1429). O wizard fechava com "pronto para receber mensagens" sem dizer quem, afinal, podia falar com o GarraIA por aquele WhatsApp: as contagens so apareciam no meio do fluxo, e quem acabara de parear (ou de re-vincular um aparelho com allow herdado) saia sem ver o portao. Agora, antes da linha final, ele imprime o MESMO resumo do garra whatsapp access — canal, perfil de execucao, admissao, default do desconhecido, grupos, contagens e cada principal com piso, nivel e o que pode de fato, identidades so pelos quatro ultimos digitos, nunca o numero inteiro. E a mesma funcao de formatacao da outra tela, e nao uma segunda copia; com o portao vazio o aviso de "ninguem autorizado" sai uma vez so, como linha final.
  • O runbook de release ganha um gate de dogfood manual antes do tag (#1439). docs/releasing.md §1.5 fixa a matriz que alguem com maquina e telefone executa contra o candidato — instalacao limpa nos dois instaladores, WhatsApp de ponta a ponta, restart sem QR, update e rollback, os bundles do desktop, isolamento do workspace por sessao e diagnostico sem aviso espurio — com data, sistema e executor registrados no corpo da PR de release. Linha sem data, release nao sai. E a resposta ao padrao das v0.4.4/v0.4.5, verdes no CI e quebradas no caminho real; a automacao do que da para automatizar segue na #1426.
  • iMessage: o campo da sala chama-se room_id (#1461). Ele sempre foi message.cache_roomnames do chat.db (chat<digitos>, o identificador que o servidor gera e que iguala chat.chat_identifier), nunca o chat.display_name que os participantes renomeiam — mas chamava-se group_name, e foi esse nome que fez uma revisao de seguranca ler "texto livre renomeavel" onde ha um id estavel. Rename puro, sem mudanca de comportamento; o doc do campo diz o que ele e e o que nao e.
  • Instalacao limpa nao nasce com aviso espurio no /api/diagnostics (#1471). tools.bash com agent.sandbox.mode = off — o default documentado do perfil standard — passa de warning a not_configured, com o mesmo passo de como ligar o sandbox (fora de unix, idem); sandbox configurado e inutilizavel (sem binario, sem backend, ssh, tool elevada ou fora da allowlist) continua warning. runtime.channels sem canal de mensageria passa de warning a not_configured: chat web, CLI e API nao entram no registry de canais e o WhatsApp vinculado tem a linha whatsapp.linked; o passo antigo mandava procurar erro de registro de um canal web que nunca existiu ali, e o novo diz como declarar um canal.
  • Quality Ratchet: runtime.rs volta para baixo da baseline (#1254, plan 0064). O mod tests de crates/garraia-agents/src/runtime.rs (6 763 das 10 881 linhas, o maior .rs do repositorio) vira runtime/tests/, um arquivo por tema com ate ~530 linhas — nao um runtime_tests.rs unico, porque o ratchet conta todo .rs rastreado e tambem quantos passam de 700/1500/2500 linhas. Os submodulos aninhados viram arquivos irmaos sem mudar de caminho; helpers ficam pub(super) no tema que os criou. As tres guardas que varrem o fonte leem agora um runtime.rs que e so producao. O mesmo para tools/repo_search_tool.rs (840 → 433, testes em repo_search_tool/tests.rs) e para o smoke tests/whatsapp_smoke.rs da CLI (822 → 418 + whatsapp_smoke_acesso.rs), os dois que tinham cruzado as 700 linhas desde a baseline. max_file_lines 10 881 → 6 901 e files_over_700 114 → 112; files_over_1500/_2500 seguem um acima da baseline por garraia-cli/src/whatsapp/acesso.rs (1 650) e garraia-cli/src/whatsapp/tests.rs (3 726), registrados para decisao.

Fixed​

  • Sessao sem projeto ganha um workspace seguro em vez de nenhuma raiz (#1378). Uma sessao do WhatsApp recem-vinculada nasce com working_dir = null. Com agent.file_roots vazio — o default de toda instalacao limpa — o conjunto de raizes do jail das file tools ficava vazio, e vazio significa negar tudo (#1244): file_read, file_write e list_dir apareciam registradas, o modo as anunciava, e toda chamada voltava recusada. A capability existia no papel e nao existia na pratica. Agora, quando nada foi declarado, a raiz default e <data_dir>/workspace/<sessao> — um subdiretorio por sessao dentro do endereco que o ADR 0024 ja nomeia como o workspace do proprio Garra —, igual nos dois perfis de execucao: execution.pod_root muda so a raiz do servidor MCP filesystem e nao e herdado pelas file tools nativas. O boot cria esse diretorio — e so ele: uma raiz declarada com typo continua nao sendo materializada, para nao plantar diretorio no host por efeito colateral da subida. Nunca / e nunca o $HOME: um caminho mais largo segue sendo escolha explicita do operador. Nada muda para quem declarou raiz em agent.file_roots ou em GARRAIA_FILE_ROOTS — a declaracao vence e o default nem e consultado —, caminho fora da raiz continua recusado com a mesma mensagem unica, e se o workspace nao puder ser criado o jail volta ao fail-closed anterior, com aviso no boot. O /api/diagnostics passou a trazer a linha files.workspace, que diz qual e o workspace efetivo e por que ele e esse (declaracao do operador, workspace padrao, ou nenhuma raiz). O workspace novo nasce 0700 (so o dono), um symlink plantado no lugar dele e recusado em vez de seguido — senao o jail herdaria o alvo do link, que poderia ser justamente / ou o $HOME —, e a linha do /api/diagnostics relativiza o caminho tambem quando o data_dir passa por symlink, onde antes ela caia no fallback e imprimia o caminho absoluto do host numa rota auth-free. Vale notar que este default tambem passa a alcancar a RunTestsTool: a combinacao file_write + run_tests que a #1272 (SANDBOX-1) marca como adjacente ao shell do host agora tem um diretorio onde operar, dentro do jail.

    O workspace padrao e isolado por sessao (#1449). A primeira versao desta correcao punha <data_dir>/workspace como raiz fixa do jail, e raiz fixa e a mesma para toda sessao, canal e principal: um contato do WhatsApp escrevia ali e o turno de outro lia. Isso e disclosure cross-principal, e tambem um vetor persistente de injecao indireta — conteudo escrito por um principal voltando no contexto de outro sem passar pelo guard de entrada. Agora o workspace padrao nao e raiz do jail: ele e o pai de um subdiretorio por sessao, e e esse subdiretorio que entra como working_dir da chamada, pelo mesmo mecanismo (session_dir) que uma sessao com projeto ja usava desde a #1244. No caso "workspace padrao" o jail fica literalmente sem raiz fixa, entao a sessao A nao tem como alcancar o diretorio da sessao B — nem o pai, que enumeraria as sessoes existentes. O nome do subdiretorio e um hash do identificador da sessao, e nao o identificador: hashear e o que impede tanto ../separador de caminho vindos de um id nao confiavel quanto gravar em disco (e no log) um identificador que no WhatsApp e o contato. O diretorio de cada sessao nasce preguicosamente, fecha em 0700, e recusa symlink no lugar dele — inclusive no pai, verificado de novo no momento do uso —, caindo no mesmo fail-closed de sempre em vez de herdar o alvo do link. Raiz declarada em agent.file_roots/GARRAIA_FILE_ROOTS continua vencendo sozinha, sem escopo por sessao: um diretorio compartilhado ali e escolha explicita do operador. A linha files.workspace do /api/diagnostics passa a mostrar <data_dir>/workspace/<sessao>, sem nunca publicar o identificador da sessao.

    O working_dir sintetizado alcanca toda invocacao de ferramenta, nao so as quatro file tools: bash (sandboxed), git_diff, code_review e repo_search tambem passam a operar dentro do diretorio da sessao. No bash sandboxed isso troca a recusa fail-closed de antes (mount vazio sem working_dir, #1272 SANDBOX-2/5) por execucao de verdade dentro de um diretorio novo e vazio; fora do sandbox, e nas outras tres tools, o efeito e estritamente mais estreito — antes caiam no CWD do processo do gateway, que expunha o checkout onde o gateway roda.

  • repo_search recusa na hora quando nao ha repositorio ativo (#1380). Sem working_dir a busca herdava o diretorio do processo e varria a arvore inteira ate estourar o timeout de 15s, para no fim nao responder nada util — o caso da sessao remota sem projeto selecionado, em que o diretorio do processo e /, o $HOME ou o diretorio de dados. Agora a tool usa o RepoDir::decidir para separar os dois sentidos de "sem working_dir": o diretorio herdado que E (ou esta dentro de) um repositorio segue sendo buscado, como no garra chat local; sem .git/.hg/.svn/ .jj nele ou acima dele, a resposta sai em milissegundos, dizendo onde a tool olhou e que basta selecionar um projeto. Sessao que escolheu um working_dir nao muda de comportamento. Num turno restrito (o piso search dos canais remotos, o mesmo em que o garra_status retem session.working_dir) a recusa nao cita o caminho do host; o operador local continua vendo onde a tool olhou, porque ali o caminho e acionavel. Para isso o runtime passou a publicar o bit de "turno restrito" para toda ferramenta, e nao so para o garra_status. Mudanca de comportamento: uma arvore de codigo SEM metadado de VCS no diretorio do processo (por exemplo um COPY de container ou um tarball exportado) era buscavel e agora recebe a recusa — selecione o projeto (defina o working_dir da sessao) para buscar nela.

  • garra_status explica o que withheld significa (#1382, #1387). O relatorio ja retinha o que so interessa ao operador num turno restrito e listava os campos retidos em withheld, mas nada dizia ao modelo como ler isso: um campo retido sai null, e com mcp_servers: null o modelo respondia que este Garra nao tem MCP. A description() da tool e o proprio relatorio (campo withheld_means, presente so quando ha algo retido) agora dizem que campo citado em withheld esta OCULTO POR POLITICA desta conversa, e nunca deve ser lido como capacidade ausente, desligada ou nao suportada. O texto tambem nao afirma o contrario: withheld e lista fixa, entao o null significa apenas "nao divulgado nesta conversa" e nao prova que o recurso exista. A nota de sistema que acompanha a tool (NOTA_GARRA_STATUS_PT/EN, no runtime.rs) carregava a mesma afirmacao na forma curta ("e nao esta ausente") e foi corrigida junto, para que prompt e relatorio nao digam coisas diferentes sobre o mesmo campo. Nenhum dado novo e exposto: e so honestidade de texto sobre o que o relatorio ja fazia.

  • garra whatsapp allow '*' diz por que nao vale (#1389). O curinga caia no erro generico de caractere ("o numero so pode ter digitos"), que faz um recurso inexistente parecer erro de digitacao. Agora ha uma variante propria: a mensagem explica que este canal nao tem "autorizar todo mundo" e que o portao e fail-closed, identidade a identidade. O exit code segue 65 e a semantica de acesso aberto continua NAO implementada.

  • WhatsApp pessoal: ligar ou desligar os grupos pela secao access (garraia whatsapp access groups on/off, console) passa a valer na mensagem seguinte, nos dois sentidos — o filtro de grupo usava os settings do boot, entao ligar so valia depois de um restart e desligar a quente nao impedia o turno do grupo. Testes ponta a ponta provam tambem que mensagens concorrentes a trocas de politica viram turno ou recusa uma vez so, sem janela de portao aberto, que promover/rebaixar/remover valem na mensagem seguinte e que no grupo manda a politica do grupo, nunca o nivel de quem fala (#1412, #1423).

  • garraia stop nao trata mais um zumbi como processo vivo (#1426). Achado pelo dogfood em container limpo: quando o pai do daemon nao colhe filhos (um container cujo PID 1 e sleep, um docker exec sem --init), o gateway encerrava limpo no SIGTERM — o garraia.log termina em "gateway shut down gracefully" — mas virava zumbi, e kill(pid, 0) continuava devolvendo 0. O stop entao esperava os 5 s de graca, mandava SIGKILL e anunciava "survived SIGTERM and SIGKILL" de um processo ja morto. No Linux, is_process_running agora le o estado em /proc/<pid>/stat e um Z conta como parado; fora do Linux o comportamento e o de sempre. Teste de unidade forka um filho, espera ele virar zumbi de fato e exige false.

  • Diagnostics separa "nunca configurado" de "quebrado" (#1437). O GET /api/diagnostics tinha quatro estados (ok/warning/error/skipped) e um subsistema opcional que o operador nunca montou saia igual a um subsistema montado e falhando: uma instalacao local recem-feita acendia amarelo por TLS ausente, por .env inexistente e por um GARRAIA_JWT_SECRET que o proprio texto da linha chamava de opcional. Entram duas variantes aditivas — disabled (ha um interruptor e ele esta desligado: modo voz) e not_configured (subsistema opcional que esta instalacao nunca montou: TLS, .env, secret de JWT, tokens de Telegram/Discord, aparelho de WhatsApp vinculado). As quatro originais mantem nome e significado, e as tres neutras (skipped, disabled, not_configured) nunca tiram o agregado do relatorio de ok. O console mostra as neutras em cinza, com o estado por extenso ao lado do rotulo, e trata status desconhecido como neutro — nunca como erro.

  • cargo clippy -D warnings limpo no stable 1.95 (#1447). Duas expressoes booleanas pre-existentes disparavam clippy::nonminimal_bool a partir do rustc 1.95: o predicado de retain em whatsapp_linked.rs (De Morgan) e uma asserção de teste em safety_gate.rs (!x.is_ok() → x.is_err()). Sem mudanca de comportamento; nenhum #[allow(...)] novo.

  • Hook pre-tool-use deixa de bloquear toda remocao por caminho (#1453). Os padroes rm -rf /, rm -rf ~ e rm -rf ./ eram casados como substring literal, entao rm -rf /tmp/scratchpad/x e rm -rf ./pintest — limpezas legitimas de uma rodada autonoma — caiam como "comando perigoso". O rm passa a ser julgado por uma regex ancorada no ALVO: raiz, home (~, $HOME), diretorio atual e pai, sozinhos ou com glob, precedidos de qualquer flag (-rf, -r -f, --) e de sudo/xargs/&&. Um caminho DENTRO desses diretorios nao casa. Os demais padroes literais (fork bomb, DROP TABLE, force push em main, dd if=, mkfs.) continuam iguais, e scripts/test-hooks.sh ganha os casos dos dois lados.

  • CI volta a subir o MinIO sem depender de registry (#1458). O Docker Hub removeu minio/minio (#1230), o quay.io passou a exigir login para toda tag em 2026-09-24 e o dl.min.io responde 410 — o check obrigatorio Clippy Linting falhava no docker pull em toda PR, e o retry do #1457 nao tinha como ajudar. Agora scripts/ci/build-minio-image.sh compila o minio a partir da tag upstream fixada (RELEASE.2025-02-28T09-55-16Z, commit conferido antes do build, tag movida e recusada), embala em debian:bookworm-slim e etiqueta a imagem com o nome:tag que o testcontainer procura — que so faz pull quando a imagem nao existe localmente. O binario compilado fica no cache do Actions por tag, entao so o primeiro run depois de um bump paga o build. O docker-compose.minio.yml de dev usa o mesmo script e deixa de depender da imagem do mc, tambem fechada: o bucket passa a ser criado com o aws-cli.

  • As raizes das file tools sao resolvidas uma vez, no boot (#1459). O /api/diagnostics (rota auth-free) e o garra_status chamavam raizes_das_file_tools a cada request/chamada: um canonicalize por raiz declarada e um warn! por raiz que nao resolve, amplificaveis por quem quisesse. Agora o AppState guarda o resultado calculado na subida, da mesma config e pela mesma funcao que monta o jail, e as duas superficies leem dele — descrevendo o jail que o turno usa, e nao o disco do momento. Guardas de fonte impedem a volta do padrao.

  • O workspace padrao e o diretorio de cada sessao nascem fechados em 0700 no proprio mkdir (#1463). Antes o diretorio era criado com a umask do processo (tipicamente 0755) e so depois fechado por set_permissions, com uma janela em que qualquer usuario local o lia; e o create_dir_all seguia um symlink plantado no proprio caminho. Agora a criacao e de um componente so, ja com o modo certo, e um link no caminho faz o mkdir falhar em vez de ser seguido. A sequencia symlink → nao-diretorio → mkdir passa a existir uma unica vez, em SessionWorkspace::garantir_raiz, chamada pelo boot do gateway (para <data_dir>/workspace) e pelo turno (para o subdiretorio da sessao), em vez de duas copias que podiam divergir (#1460). O boot cria o data_dir antes, porque numa instalacao limpa ele ainda nao existe nesse ponto. A guarda de fonte que exige um unico ponto de montagem de ToolContext em runtime.rs passa a casar o construtor em qualquer forma (numa linha so, com caminho de modulo, com ..base), e nao so a linha exata ToolContext { (#1464).

  • WhatsApp pessoal: access.groups.enabled: false escrito a mao na config passa a VENCER o reply_in_groups: true legado (antes o legado ganhava em silencio e o canal seguia respondendo em grupo, sem teto); a precedencia gera aviso no boot/config check e esta na ADR 0025 (#1501).

  • garra mcp-server volta a entregar as tools ao Claude Code (#1518). Quem negocia a spec MCP 2026-07-28 (o Claude Code, via server/discover) valida o tools/list contra o schema novo, que torna ttlMs e cacheScope obrigatorios (SEP-2549). O rmcp 3.x (#1162) modela os dois como Option e o ListToolsResult::with_all_items os deixa fora do fio, entao o cliente recusava a lista inteira ("Invalid result for tools/list") e o /mcp mostrava o servidor conectado, mas sem nenhuma tool. O handler agora manda ttlMs: 0 e cacheScope: "private" quando a versao negociada e 2026-07-28 ou mais nova; peer legado segue recebendo o tools/list de antes, byte a byte.

  • codeql-rekey-ledger.py casa a linha do sink dentro do span do alerta. O CodeQL reporta o span do statement (start_line..end_line) e o ledger ancora a linha do sink, que num println! multi-linha e a ultima do span; o rekey comparava start_line exato e deixava a duplicata aberta como "sem entrada" — foi o que manteve o alerta #176 (mesmo sink do #173 dispensado, garra whatsapp link) aberto desde a v0.4.5. Agora a regra e a mesma do codeql-reapply-dismissals.sh, com testes, e o ledger reaponta #173 → #176.

Security​

  • Ferramenta MCP nao vira mais slash command, que era bypass do ToolGate (#1386). O boot registrava um comando /mcp_<tool> por ferramenta MCP conectada, e o fechamento desse comando chamava McpManager::call_tool direto — sem ToolGate, sem o modo da sessao, sem ToolApproval e sem HardwareGate. O unico controle era o Role::User do comando, que todo mundo tem: uma sessao criada em modo search (read-only) executava ferramenta de mutacao por POST /api/sessions/{id}/messages, e GET /api/slash-commands listava os nomes de graca. O caminho legitimo nao muda — as ferramentas MCP continuam chegando ao modelo pelo AgentRuntime, despachadas atras do ToolGate.
  • Leitura de sessao por id separada em cliente e operador (#1462, opcao 3). GET /api/sessions/{id}/history devolvia o historico verbatim de qualquer sessao em memoria — e a de um canal esta em memoria no caso normal, porque a hidratacao do canal a poe la —, numa rota /api/* auth-free por padrao, sem LLM no meio e com ids adivinhaveis por construcao. Agora a leitura de cliente por id (GET /api/sessions, GET /api/sessions/{id}/history, DELETE /api/sessions/{id} e o resume sem token do /ws) so alcanca sessao das superficies locais do operador (api, vscode, web, parrot), como a escrita ja fazia desde a PR #1468, e a regra mora num lugar so (AppState::sessao_da_api): sessao de canal ou do mobile responde o mesmo 404 session not found de id inexistente, byte a byte, e nao e hidratada, desconectada nem listada — em memoria e no sessions.db. A leitura do operador ganha GET /admin/api/sessions/{id}/history (cookie do /admin + manage_sessions; viewer recebe 403), que le qualquer sessao, em memoria ou so no disco, sem hidratar — a hidratacao reescreveria a linha do canal — e registra cada leitura na auditoria. O Export da pagina Sessions do Web Console passa a usar essa rota (e a listagem administrativa) quando o navegador esta logado no /admin; sem login, mostra so as sessoes locais e diz onde entrar. O historico do mobile (GET /chat/history) continua vindo do sub do JWT — cada usuario le so a propria sessao.
  • Um session_id escolhido pelo cliente deixa de alcancar a sessao de outra superficie (#1462). POST /v1/chat/completions aceitava X-Session-Id verbatim e POST /api/sessions/{id}/messages aceitava o id no caminho, e nenhum dos dois conferia de quem era a sessao: um id forjado — whatsapp-linked-<numero>, telegram-<chat>, adivinhaveis por construcao — hidratava a conversa da vitima (resumo + ate 100 turnos) para dentro do request do atacante e gravava o turno dele na conversa dela. Por id, agora so se alcanca sessao das superficies locais do operador (api, vscode, web, parrot); sessao de canal com humano do outro lado ou do mobile responde 404 session not found sem confirmar que existe, e nao e hidratada nem escrita. (A leitura por id, GET /api/sessions/{id}/history, foi fechada em seguida pela opcao 3 — ver o item irmao desta secao.)
  • /api/diagnostics nao publica mais caminho absoluto do host para raiz do MCP filesystem fora do data_dir (#1465). As linhas execution.profile (em isolated-pod) e mcp.filesystem_root caiam no fallback que imprimia o caminho como esta — no caso legado mais comum, o $HOME do host com o nome de usuario — numa rota auth-free. Agora uma raiz fora do data_dir sai so como contagem (1 fora do data_dir), e a raiz ofensora e apontada pela posicao na lista do servidor (raiz #2 do servidor filesystem), nunca pelo caminho; e a mesma regra que files.workspace ja aplicava a raiz declarada. O caminho continua no mcp.json/config.yml e no log de boot.
  • Chamadas MCP de filesystem confinadas ao jail da sessao (#1482, nucleo da #1383). Em standard a raiz do servidor filesystem autoprovisionado e <data_dir>/workspace, o pai de todo diretorio de sessao (#1449); com a leitura MCP liberada no piso search (#1384), um contato admitido listava o pai e lia o que o agente escreveu para outra pessoa. Agora McpTool passa path/paths/source/destination das operacoes de filesystem pelo MESMO FileJail das file tools nativas, com o working_dir da sessao; caminho relativo resolve contra o diretorio da sessao; fora do jail a recusa e a frase unica das tools nativas; list_allowed_directories responde as raizes efetivas da sessao sem chamar o servidor. O gateway entrega o jail ao McpManager no boot (guarda de fonte); a CLI local segue sem jail.
  • Ledger CodeQL (docs/security/codeql-suppressions.{md,json}): a entrada do alerta #174 (rust/path-injection, bridge.rs:639, caminho sob o diretorio da ponte derivado do data_dir da config) passa a ser o #179 — o GitHub fechou o #174 como fixed quando o sink mudou de linha e abriu o #179 no mesmo lugar; justificativa preservada e re-conferida no SARIF, sem supressao nova.
  • mascarar_numeros_longos agora cobre telefone com separador comum (#1514). A funcao que o RedactingWriter usa para mascarar PII em log so contava sequencia contigua de 10+ digitos: um telefone com espaco, hifen ou parenteses ("55 11 98765 4321", "55-11-98765-4321", "+55 (11) 98765-4321", o celular BR com o nono digito separado ("55 11 9 8765 4321") e o celular NANP com DDI separado ("+1 555 123 4567") atravessava inteiro. Achado por um test-engineer durante a revisao independente do PR de #1513. A ponte entre grupos exige que o grupo ancora nao esteja colado a letra nem seja parte de um UUID, rejeita grupo colado a letra do outro lado, para de crescer assim que o total ja basta, exige que ao menos um grupo tenha 4+ digitos, e recusa — so depois de a ponte parar de crescer, avaliando os grupos completos — intervalo numerico ("100000-200000") e numero com separador de milhar ("1 234 567 890"). Para na hora se o trecho ja fechado for uma data (AAAA-MM-DD, a forma que o CLAUDE.md manda usar em prosa narrativa, ou DD-MM-AAAA), a menos que a ponte, espiando adiante ate o que resta do teto de grupos, alcance um grupo de 4+ digitos que complete um telefone plausivel ("55-11-98-76-5432"). Revisado em tres rodadas por code-reviewer e security-auditor independentes: a primeira versao publicada tinha uma regressao na protecao de UUID e nao cobria os formatos de celular BR/NANP mais comuns; a segunda corrigiu isso mas criou uma regressao nova (apagava todo celular NANP) e nao fechava de vez o bypass por prefixo de data; a terceira corrigiu ambos e recebeu veredito MERGE_READY (risco R1), com duas limitacoes residuais aceitas e documentadas em codigo: um numero real cujos grupos apos o primeiro sejam todos de exatamente 3 digitos ainda escapa (indistinguivel de numero com separador de milhar sem contexto semantico), e uma combinacao especifica de data + numero curto + grupo de 4+ digitos pode ser sobre-mascarada (nunca vaza, so reduz legibilidade de log). Nao cobre numero partido entre duas MessagePart::Text diferentes — isso fica para quem redige o conteudo agregado, fora do escopo desta funcao.

[0.4.5] - 2026-09-22​

Release que tira o bash irrestrito das superficies sem humano no laco e faz funcionar o que a v0.4.4 deixava pela metade: o "sim" a um pedido de confirmacao, o onboarding do WhatsApp pessoal e o MCP filesystem de uma instalacao nova. A correcao P1 e a #1272: em execution.profile = standard o garraia mcp-server e o gateway so registram bash dentro de um sandbox docker ou podman valido, com todo comando no container e sem fallback para o host. Com o sandbox desligado (o default), com ssh ou sem o binario, a tool nao existe; antes, um cat /etc/shadow pedido pelo modelo rodava no host. Poder total no host so em isolated-pod explicito, nunca por deteccao de container. O run_tests do gateway segue a mesma regra, o container ganha --cap-drop ALL, --pids-limit 512 e o uid do operador, o git de git_diff/code_review nao executa programa plantado no repositorio e file_write recusa caminho com .git. Instalacoes existentes continuam subindo: o boot avisa uma vez, o /api/diagnostics ganha o check tools.bash, e o garraia chat, onde o humano confirma no terminal, nao muda.

O "sim" volta a aprovar em todo canal com um humano do outro lado (#1343): o pedido pausado fica em memoria e a mensagem seguinte, se for inteira uma palavra de aprovacao, roda o pedido uma vez, dentro de 5 minutos, so se vier do mesmo remetente, na mesma sessao e no mesmo canal. Aprova a mesma conexao no Web Console e no desktop, o sub do JWT no app mobile, o id da plataforma nos canais de mensagem, o terminal no garraia chat e, em /v1/chat/completions, o dono com o mesmo Authorization e a mesma X-Session-Id nos dois requests. Em grupo, o "sim" de outro membro nao aprova e encerra o pedido; qualquer mensagem no meio tambem encerra (#1340), e reiniciar o gateway cancela os pendentes. O pedido chega ao humano sem o marcador interno (#1373), e o log passa a mascarar o telefone de quem fala pelo WhatsApp e pelo Signal.

No WhatsApp pessoal, o garraia whatsapp link pergunta quem pode falar com o GarraIA e so diz "pronto" com alguem autorizado (#1345); o novo garraia whatsapp allow <numero> [--owner] [--yes] autoriza sem terminal, com dono so em isolated-pod, e o gateway rele allow, owners e enabled a cada mensagem. Depois de um garraia update, o gateway regrava no boot a ponte embutida no binario e so roda npm ci quando nada prova que a arvore instalada e a do lock (#1373). O MCP filesystem de instalacao nova fixa a versao do pacote, se recupera uma vez de um cache do npx corrompido e, esgotadas as tentativas, loga o erro uma vez so (#1346). O garra_status chega aos modos restritos, enxerga o whatsapp_linked e os canais push com o status do /api/channels, e o modelo e instruido a consulta-lo antes de negar acesso a um canal (#1347). Em turno restrito ou em sessao que nao e provadamente do operador (WhatsApp, app mobile, A2A), o relatorio retem o que e do operador e lista o que reteve em withheld.

O boot passa a recusar o que falharia aberto. Um bind exposto sem credencial de gateway sai com exit 78 antes do socket, antes do fork e antes de o restart derrubar o daemon (#1261); a saida e garraia init, gateway.api_key, a nova env GARRAIA_GATEWAY_API_KEY ou --host 127.0.0.1. Imagem Docker, compose e RunPod precisam da env antes de subir a imagem v0.4.5, e Helm e Terraform ganharam o segredo. gateway.host/gateway.port do arquivo ficam deprecados, e o restart passa a ler HOST/PORT. O garraia config check roda em todo start, restart e start -d (#1247): cada achado vai para o log, e so uma lista fechada recusa o boot (hoje, o TLS pela metade), com a escotilha GARRAIA_ALLOW_INVALID_CONFIG=1.

Com agent.sandbox.mode: all, run_tests, git_diff, code_review e repo_search tambem rodam no container, por argv e sem shell (#1225). O ledger de runs ganha retencao (runs.retention_days, default 0 = nunca apaga), GET /api/runs e garraia runs list (#1227). As crates mortas garraia-tools e garraia-runtime saem, e o workspace fica com 22 crates (#1226). O detector de loop avisa o modelo uma vez antes de abortar (#1295), e o garraia max-power --goal passa a rodar, inclusive offline (#1228). Do smoke de instalacao limpa da v0.4.4 vieram: o install.sh nao tenta mais o wizard sem terminal em container e CI (#1369); o garraia start -d abre o garraia.log em append em vez de apaga-lo, e garraia status | head sai em silencio em vez de panico (#1371); sessoes REST sobrevivem ao restart do gateway (#1372); e a chave de um provider so vai para o endpoint da propria entrada llm: (#1370). Na infraestrutura, o freeze-baseline.py ganha um re-baseline auditado do Quality Ratchet, usado logo antes da tag para que o ratchet deixe de repetir a deriva de meses em todo PR (#1254), PR sem fragmento em changelog.d/ fica vermelho, o Swagger UI entra vendored, e a release volta a subir o AppImage aarch64 da CLI.

Added​

  • GET /api/runs le o ledger de runs pelo gateway (#1227). Somente leitura, com status filtrado no SQL (valor desconhecido e 400 de corpo constante) e limit preso em [1, 200]. Os instantes saem em UTC ISO 8601 com Z e o conteudo sai so como previa de ate 120 caracteres, com segredo de formato conhecido redigido e caractere de controle trocado; o trecho completo do ledger nao sai por HTTP. O acesso e mais estrito que o resto de /api/*: com gateway.api_key exige o bearer; sem chave, so responde a peer loopback com Host de loopback (LAN recebe 503, DNS rebinding recebe 403, mesmo sem Origin, e pedido com cabecalho de proxy reverso como X-Forwarded-For recebe 503 — atras de proxy, configure a chave). Os leitores de instante e de previa passaram para o garraia-db, compartilhados com garraia runs list, que agora explica que run interrupted de tarefa agendada e reexecutado sozinho pelo scheduler — por isso nao ha runs resume.
  • Retencao do ledger de runs com runs.retention_days (#1227). A tabela agent_runs ganhava uma linha por run agendado e nada a encurtava. A nova secao de topo runs.retention_days liga uma varredura no gateway (no boot e a cada 24 horas) que apaga runs terminais mais velhos que a janela. O default e 0 = nunca apaga, entao uma atualizacao nao remove historico; desligada, o gateway avisa uma vez no boot quantos runs existem. Run running nunca e apagado, qualquer que seja a idade, e instante ilegivel tambem fica. A chave e de topo porque agents e um mapa de agentes nomeados. garraia config check recusa valor acima de 3650, e o log da varredura leva so contagem, nunca conteudo do run.
  • garraia runs list le o ledger de runs de agente (#1227). A tabela agent_runs ja era povoada pelo scheduler do gateway e ja marcava interrupted na subida depois de uma queda, mas nao havia como olhar para ela sem abrir o SQLite na mao. O comando abre o mesmo sessions.db sem falar com o gateway — igual ao garraia logs —, entao ele responde "o que estava em voo?" justamente quando o gateway esta fora. --status (running, done, error, cancelled, interrupted) filtra no SQL, --limit corta a janela (padrao 50) e --json devolve um array com chaves estaveis e instantes em UTC ISO 8601 com Z. Ledger vazio nao e erro: sai 0 com uma linha amigavel (ou []). A listagem nao cria o arquivo — numa instalacao que nunca rodou nada ela sai 0 e deixa o disco como estava, sem um sessions.db vazio como efeito colateral de uma leitura — e tampouco escreve no ledger existente: converter running residual em interrupted continua sendo do hook de subida. Campo vindo do banco e higienizado antes de chegar ao terminal (controle de terminal vira U+FFFD) e os trechos de provider saem redigidos no --json.
  • PR sem fragmento em changelog.d/ fica vermelho (#1228). O job de fragmentos do CI validava so o formato do que existia; um PR que nao escrevia fragmento nenhum passava, e como as notas de release saem do CHANGELOG.md o silencio so aparecia no dia da release. O novo workflow Changelog presence exige que o PR adicione ou edite um changelog.d/<secao>/<nome>.md de secao valida. Isentos: a label no-changelog (aplicar ou tirar reexecuta o check), o dependabot, branches release/vX.Y.Z abertas deste repositorio (de fork o nome da branch nao isenta, porque quem escolhe e o autor) e eventos fora de pull_request. Roda em pull_request com contents: read, sem segredo, sem ler titulo nem corpo do PR, e le os arquivos do proprio merge commit com git diff -z, para que fragmento com acento no nome nao saia entre aspas e deixe de contar.
  • O runtime passa a suportar a retomada de um pedido de confirmacao entre turnos (#1343). Ate aqui um "sim" dado no turno seguinte nunca aprovava nada nos canais de producao: a aprovacao GAR-187 so e lida do historico, e gateway, CLI e API compativel com OpenAI guardam o historico como texto puro, entao o resultado de ferramenta com o marcador nunca volta e a ferramenta pede confirmacao para sempre. Esta fatia so traz o mecanismo: quando o turno traz ExecContext::approval_scope (canal, sessao e um remetente derivado pelo servidor), o AgentRuntime grava em memoria o nome da ferramenta e a impressao digital HMAC do pedido, e o proximo turno aprova so se a mensagem for exatamente uma palavra de aprovacao, do mesmo remetente, na mesma sessao e no mesmo canal, dentro de 5 minutos. A aprovacao vale para um unico turno (o registro e consumido na leitura), e nao para uma unica execucao: dentro desse turno, chamadas identicas ao pedido aprovado rodam, como no caminho pelo historico. Qualquer outra mensagem na sessao encerra o pedido (a regra do #1340, inclusive em grupo), marcador copiado ou forjado no historico deixa de pesar, e reiniciar o processo continua cancelando tudo, porque a chave do HMAC vive so na memoria. O assunto cru (o comando do bash) nunca e guardado. Caminho sem escopo continua exatamente como antes. Quais canais aderem, e com qual remetente, esta na entrada de Fixed do #1343.

Changed​

  • Com agent.sandbox.mode: all, a imagem do sandbox precisa da toolchain das tools de repositorio (#1225). run_tests, git_diff, code_review e repo_search rodam no container. A imagem default (debian:bookworm-slim) so tem grep: repo_search cai para ele dentro do container, e run_tests/git_diff/code_review respondem que o programa nao existe na imagem (so quando o proprio runtime diz isso no stderr: um exit 127 do script de teste devolve a saida real da suite). Para migrar, aponte agent.sandbox.image para uma imagem com git/cargo/rg, ou liste a tool em agent.sandbox.elevated para ela seguir no host. mode: off (o default) nao muda nada. O aviso de subida e o garraia config check deixaram de dizer que essas tools rodam no host.
  • O snapshot de capacidades do garraia max-power passa a anunciar o tool_program (#1226). A lista estatica de tools do snapshot nao tinha a ferramenta intrinseca que o runtime ja expoe nos modos auto, code e ask, e o teste que devia fixa-la so comparava o tamanho da lista com ele mesmo. Agora o teste fixa a lista inteira, em ordem. As tools device_* e schedule_* continuam fora de proposito: so existem em algumas instalacoes. Em docs/src/modes.md fica registrada a decisao de nao incluir tool_program na whitelist dos perfis nativos search, architect, debug, orchestrator, review e edit; um perfil customizado pode liga-la.
  • Cobertura e Quality Ratchet editam um comentario so por PR (#1228). Os dois postavam um comentario novo a cada push, e num PR com dez pushes a conversa de review sumia no meio de vinte relatorios quase iguais. Agora scripts/ci/upsert_pr_comment.py edita no lugar o comentario achado pelo marcador e pelo autor github-actions[bot] — um comentario humano que cite o marcador nunca e sobrescrito. O comentario de cobertura encolheu para a linha TOTAL e o link do run; a tabela por arquivo vai para o job summary. Permissoes, gatilhos e a guarda de fork continuam os mesmos.
  • O Swagger UI do gateway entra pela feature vendored, sem download no build (#1228). O build.rs do utoipa-swagger-ui baixava o zip do Swagger UI do GitHub a cada build limpo, o que quebrava build offline e punha um download sem pino de conteudo na cadeia de build (o CI contornava com um cache e SWAGGER_UI_DOWNLOAD_URL=file://). Agora o zip vem da crate utoipa-swagger-ui-vendored (mesma versao 5.17.14, MIT OR Apache-2.0), e a build-dependency reqwest que so servia para baixar sai. A rota /docs continua igual. Um teste prende a feature no manifesto e confere que a pagina embutida e servida. A action swagger-ui-cache do CI fica uma release como vestigial: o download dela virou best-effort (avisa em vez de abortar o job), entao uma queda do GitHub nao derruba mais uma release por um zip que nada le.
  • O garraia config check passa a rodar em todo boot (#1247). garraia start, garraia restart e garraia start -d rodam o mesmo run_check do comando, uma vez, antes do fork, do PID file e do stop do daemon atual. Cada Error sai no log uma vez em nivel de erro e cada Warning em nivel de aviso, com um resumo apontando para garraia config check; no start -d as mesmas linhas vao para o stderr do terminal antes do fork, porque depois dele o log e invisivel. Antes, uma config com Error subia em silencio, e o check so valia para quem lembrasse de roda-lo. Recusar o boot fica restrito a uma lista fechada de achados que falham abertos (hoje so o TLS pela metade): todo outro Error, como uma entrada llm sem chave, e dito mas nao derruba instalacoes que funcionam hoje. Os achados de gateway.host/gateway.port nao se repetem no boot: quem julga o bind e a recusa do #1261, sobre o endereco real. Migracao: quem precisa subir apesar de um achado bloqueante usa GARRAIA_ALLOW_INVALID_CONFIG=1 (exatamente 1; outro valor conta como ausente), e o achado segue logado como erro; o garraia config check lista a escotilha em env_vars_detected quando ela esta no ambiente. A retencao da memoria com interval_hours ou max_age_days fora da faixa nao derruba mais o worker com panic de interval(0) nem apaga por um corte que ninguem pediu: a varredura nao sobe, com error! no log.
  • freeze-baseline.py ganha um re-baseline auditado: --adopt-current-file-metrics --reason '#NNN' (#1254). Ate aqui a ferramenta so sabia apertar o ratchet: com o baseline de 2026-05-05 muito atras do main, qualquer proposta saia igual ao baseline velho, e a unica saida seria editar .quality/baseline.json a mao, o que e proibido. A flag adota do current-metrics.json apenas as metricas de tamanho de arquivo; audit, cobertura e clippy continuam no ratchet estrito e audit.critical segue 0. O --reason tem de citar uma issue, o arquivo gerado registra adopted_reason, source_git_sha e source_collected_at para qualquer um reproduzir, e a flag recusa --seed e um --out que aponte para o baseline: continua escrevendo so o baseline.proposed.json. Sem a flag, nada muda.
  • O Quality Ratchet volta a comparar contra o estado real do codigo (#1254). O baseline estava congelado em 2026-05-05 (maior arquivo com 3240 linhas, 334 arquivos .rs) e todo PR recebia as mesmas quatro "regressoes", que eram deriva de meses e nao culpa de ninguem. O freeze-baseline.py --adopt-current-file-metrics --reason '#1254' foi rodado no SHA de main logo antes da tag e o baseline.json e o arquivo que ele gerou, sem edicao: so as metricas de tamanho de arquivo mudaram (hoje 608 arquivos, maior com 10513 linhas); auditoria, cobertura e clippy seguem a catraca estrita, e audit.critical continua 0. O teto de thresholds.toml (3500 linhas) nao subiu. Os 16 arquivos acima de 2500 linhas ficam registrados como divida aceita no .quality/README.md, com o comando que reproduz o baseline.
  • gateway.host/gateway.port do arquivo ficam deprecados, e restart le HOST/PORT (#1261). As duas chaves nunca alimentaram o bind (garraia start liga em flag > HOST/PORT > 127.0.0.1:3888), mas o wizard escrevia 0.0.0.0 nelas e os comandos cliente as liam como "onde esta o meu gateway". Agora: o garraia init para de escreve-las e as remove num re-run; os docs/deployment/config.*.yml nao as trazem mais; status, stop, doctor e admin usam o mesmo endereco do start (com 0.0.0.0/:: trocados por loopback); o Web Console mostra o bind como somente-leitura, com origem runtime; e o config check as reporta como Warning de deprecacao quando diferem do bind efetivo. garraia restart ganhou env = "HOST"/"PORT" como o start — antes, reiniciar um daemon de RunPod religava em loopback em silencio. As chaves seguem no schema (o arquivo antigo continua parseando); remove-las de vez fica para um release com quebra, porque hoje so trocaria um aviso util por um no-op silencioso.
  • O detector de loop de ferramenta avisa o modelo uma vez antes de abortar o turno (#1295). Na primeira vez que uma tarefa repete a mesma chamada (mesma ferramenta, mesmos argumentos) tres vezes seguidas, a terceira continua sem rodar, mas o turno nao morre mais: o modelo recebe no lugar do resultado uma observacao corretiva com o nome da ferramenta, a contagem e o resumo redigido do input repetido, pedindo que leia o erro anterior e mude de abordagem. Qualquer deteccao seguinte na mesma tarefa, do mesmo loop ou de outro, aborta como antes, com a mensagem do #1318, e a chamada avisada aborta tambem na proxima vez que voltar na tarefa, mesmo com outra chamada no meio ou depois do reset do teto por turno. A chamada barrada, avisada ou abortada, nunca roda: antes do aviso a chamada em loop roda no maximo duas vezes seguidas (a mesma janela de tres chamadas de antes), depois dele nao roda mais na tarefa, e o resto segue limitado por max_per_turn/max_per_task. O custo extra e no maximo uma volta de LLM. O aviso so e rearmado por uma mensagem nova do usuario, nunca pelo reset do teto por turno, e nao ha chave de config para desliga-lo. Dentro de tool_program (e no programa que falha antes do primeiro passo) vale a mesma cadencia, com o motivo rotulado como loop e nao como gate. A chamada barrada, avisada ou abortada, agora emite o par tool_started/tool_finished com success=false e o veredito, entao aparece no /tool do garraia chat ao lado das chamadas identicas anteriores e do erro original. O sub-item de "diff dos payloads" fica sem codigo: a assinatura e nome mais hash dos argumentos, entao um loop detectado tem payloads identicos por definicao.

Removed​

  • As crates mortas garraia-tools e garraia-runtime saem do workspace (#1226). Nenhum caminho alcancavel as usava: o unico consumidor era codigo do gateway que nunca foi roteado (runtime_handler.rs e o campo RuntimeSettings do AppState, sem chamador). Elas carregavam um segundo executor com limites diferentes do ExecutionBudget real, o ToolRegistry::execute_program ja marcado deprecated e uma segunda RepoSearchTool/ListDirTool sem jail, um risco latente se alguem a ligasse. Nenhum binario, rota, chave de config ou asset de release muda. O workspace passa de 24 para 22 crates.

Fixed​

  • garraia whatsapp --help e garraia memory add --help param de exibir notas de implementacao (#1228). O clap publica o doc comment inteiro no --help, e os dois comandos mostravam ao usuario a justificativa interna (o #[command(name)], o nome do teste que o prende, o historico do memory add). As notas viraram comentario comum, a ajuda do whatsapp passa a citar garraia init em vez do alias, e um teste renderiza a ajuda longa de todos os comandos e falha se uma nota assim voltar.
  • Erro lendo a resposta de um provider OpenAI-compativel passa a dizer a causa, e o garraia max-power passa a gravar no garraia.log (#1228). O dogfood parou numa etapa com failed to read response body: error decoding response body: o reqwest da esse mesmo rotulo para qualquer falha lendo o corpo (conexao fechada no meio, corpo truncado) e guarda a causa real em source(), que o texto descartava. A mensagem agora desce a cadeia de causas. E o max-power nao inicializava o tracing, entao o pipeline nao deixava rastro no log; agora deixa, como os outros comandos.
  • garraia max-power para de anunciar como futura a execucao que ja existe (#1228). A ajuda dizia que a execucao do pipeline "lands in GAR-495..GAR-501", mas ela existe desde o PR #1218: com provider padrao resolvido cada etapa e uma chamada ao LLM, e sem ele a execucao e deterministica (offline) — a linha execution: da saida diz qual rodou. O exemplo do menu e a dica do garraia runs list sem ledger passam a nomear o binario em execucao em vez do alias fixo garra, que nao existe numa maquina com so o garraia (imagem Docker, cargo install).
  • garraia max-power sem provider utilizavel roda offline, como a ajuda promete, em vez de parar na primeira etapa (#1228). Numa instalacao sem provider configurado o comando dizia execution: provider-backed e morria com ollama error status: 404 Not Found: o palpite final da deteccao (Ollama com o modelo padrao) era tomado como provider, entao o caminho offline nunca rodava. Agora, sem nada alcancavel, com o Ollama sem o modelo ou sem conseguir listar os modelos, o pipeline roda deterministico e diz o que fazer (ollama pull <modelo> ou garraia init). O erro de status do Ollama passa a trazer o motivo que o servidor mandou (por exemplo model 'qwen3.8:latest' not found), a dica de cada etapa offline nomeia o binario em execucao em vez do alias garra, e uma etapa que falha fica registrada no garraia.log.
  • garraia max-power --goal para de abortar com "Cannot start a runtime from within a runtime" (#1228). O comando ja roda dentro do runtime tokio da CLI, mas max_power.rs e AgentTeam::run criavam um runtime proprio e chamavam block_on por dentro, o que o tokio recusa com panic antes da primeira etapa. Com provider configurado ou sem ele (execucao offline), o goal agora roda de ponta a ponta: a cadeia inteira ficou async e usa o runtime da CLI. Um teste varre max_power.rs e team.rs e falha se algum deles voltar a montar runtime ou a bloquear fora dos testes.
  • A aprovacao de um pedido de confirmacao nao sobrevive a uma mensagem humana no meio (#1340). detect_confirmation_approval aceitava o pedido mais recente em qualquer ponto das ultimas 6 mensagens, e a janela dizia so "recente". Na pratica: o turno 1 pausava pedindo para apagar uma pasta, o humano respondia "nao", o modelo perguntava outra coisa em texto, e um "ok" dado a ESSA pergunta ainda aprovava o apagamento. Agora o marcador so vale quando o historico termina no resultado pausado, com no maximo a narracao do assistente depois dele — a forma da retomada GAR-187 em todo canal. Uma mensagem humana entre o pedido e o "ok" encerra o pedido, e um resultado de ferramenta posterior sem marcador tambem, sem depender de o turno humano estar no historico (no caminho compativel com a OpenAI ele vem do corpo do request). A ordem dentro de cada mensagem e a neutralizacao de marcador em saida de ferramenta, das #1339 e #1226, seguem como estavam.
  • O "sim" a um pedido de confirmacao volta a aprovar em todo canal com um humano do outro lado (#1343). Quando uma ferramenta pedia confirmacao (um comando arriscado do bash com agent.tool_confirmation_enabled, um device_execute R3/R4), o turno pausava e o "sim" da mensagem seguinte nao aprovava nada: o gateway, a CLI e a API compativel com OpenAI guardam o historico como texto, o pedido pausado nao voltava, e a ferramenta perguntava de novo para sempre. Agora o pedido fica guardado em memoria e o "sim" (a mensagem inteira) aprova o pedido para o turno seguinte, uma vez, dentro de 5 minutos, so se vier do mesmo remetente, na mesma sessao e no mesmo canal: a mesma conexao no Web Console e no desktop; em /v1/chat/completions, o dono com o mesmo Authorization e a mesma X-Session-Id nos dois requests (sem X-Session-Id cada request e uma sessao nova e o "sim" nao retoma; vale com e sem "stream": true); o sub do JWT no app mobile; o id do usuario na plataforma no Telegram, Discord, Slack, WhatsApp, Matrix, IRC, Signal, LINE, Teams, Google Chat, iMessage e no WhatsApp pessoal; e o terminal no garraia chat. Em grupo, o "sim" de outro membro nao aprova e encerra o pedido. Qualquer mensagem no meio encerra o pedido, um segundo "sim" pausa de novo, e reiniciar o gateway cancela os pendentes. A2A, OpenClaw, POST /api/sessions/{id}/messages, a resposta do agente no chat do workspace, as tarefas agendadas, garraia ask e o garra_agent do garraia mcp-server continuam sem retomada, de proposito: ali a pausa segue terminal.
  • A release passa a subir o AppImage aarch64 da CLI (#1344). O release.yml copiava garraia-linux-aarch64.AppImage para release/ e o SHA256SUMS o listava, mas a lista files: do upload nunca teve a linha dele: na v0.4.4, primeira release a produzir o arquivo, so o .sha256 subiu (pelo glob) e o asset foi anexado a mao depois, com o hash conferido contra o SHA256SUMS. A linha entrou, e scripts/release/check_files.py — que le o workflow e exige que todo nome copiado para release/ case com um padrao do upload — roda no CI de todo PR, com testes proprios.
  • WhatsApp pessoal: o link passa a perguntar quem pode falar com o GarraIA, e autorizar ou revogar vale sem reiniciar (#1345). Depois do QR, o garraia whatsapp link deixava allow e owners vazios e dizia "pronto para receber mensagens": o portao do gateway (fail-closed, o que continua certo) descartava toda mensagem em silencio. Agora o link pede o numero autorizado com o codigo do pais (normalizado como o gateway compara; so os 4 ultimos digitos na tela; aviso quando e o proprio celular vinculado, cujas mensagens sao ignoradas) e so diz "pronto" com alguem autorizado. O novo garraia whatsapp allow <numero> [--owner] [--yes] autoriza sem terminal, sem mudar outro valor da config (o arquivo e reescrito: comentarios nao ficam) e sem ligar o canal (exit 65 para numero invalido, 64 para --owner fora de isolated-pod ou num pipe sem --yes); dono so e oferecido em isolated-pod, com default nao. O numero exige + e codigo do pais (6 a 15 digitos, a faixa da ponte), e <id>@lid tambem e aceito. O gateway rele allow/owners/enabled da config viva a cada mensagem quando vigia o config.yml: entrar, sair e perder o piso de dono valem na mensagem seguinte, e enabled: false recusa todo mundo, codigo de pareamento incluso. Quem pareou por codigo e estava no allow perde os dois ao sair da lista; quem so pareou segue ate o restart, e um config.yml que nao parseia mantem a lista anterior. Celular brasileiro com e sem o nono digito casa como o mesmo numero. A ponte passa a entregar o numero de remetente @lid quando o servidor o manda (remoteJidAlt/participantAlt do Baileys 7); sem ele o portao compara o LID, e status e /api/diagnostics contam essas recusas. status, /api/diagnostics (whatsapp.linked vira warning, tambem com a ponte conectada e o canal desligado na config viva) e o log de boot avisam quando ninguem esta autorizado. Estranhos continuam recusados em silencio e nao ha auto-claim.
  • O MCP filesystem deixa de ficar preso num cache do npx corrompido e para de inundar o log (#1346). A provisao de instalacao nova agora fixa @modelcontextprotocol/server-filesystem@2026.8.31 (testada com handshake em node 20 e 22; o mesmo valor vale para o template do admin e o marketplace) em vez de baixar o build mais novo do registry a cada cache frio (so o pacote de topo: as dependencias dele seguem os ranges semver publicados); um mcp.json existente nunca e reescrito, e o novo check mcp.filesystem_pinned do /api/diagnostics avisa quem ficou sem versao (dist-tag como @latest e range como @^1 contam como sem versao) com os args exatos para colar. O stderr do processo filho passa a ser capturado (so em debug), e um ERR_MODULE_NOT_FOUND dentro de <cache npm>/_npx/<16 hex>/ apaga so aquela entrada e tenta de novo uma vez: comando npx, do stderr so o hash da entrada (o diretorio e remontado a partir do cache que o proprio gateway deu ao filho, o que tambem cobre perfil com espaco no nome), caminho canonico, nunca symlink, package.json do pacote configurado, uma vez por servidor por processo. Esgotados os max_restarts, o erro sai uma unica vez em vez de a cada 30 s. O /api/mcp/health passa a listar servidores que falharam no boot (antes respondia no_mcp_configured), com status, cause e last_error (que nunca leva caminho; em servidor HTTP reflete a ultima tentativa e some quando ele reconecta), e o check mcp.servers do diagnostico traz o proximo passo por causa, nomeando o diretorio so quando o gateway o validou.
  • garra_status passa a chegar ao modelo nos modos restritos, com a instrucao de consulta-lo antes de negar uma integracao (#1347, fatia 1). Com o whatsapp_linked conectado, o Garra respondia "nao tenho acesso ao WhatsApp": o piso do canal e o modo search, cuja allowlist nao tinha garra_status, e o runtime tira da lista tudo que o modo nao permite; alem disso o prompt do modo substitui a persona, o unico lugar que mandava usar a tool. Agora garra_status (leitura R0 do proprio runtime, sem I/O nem rede) entra na allowlist de search, architect, debug, orchestrator, review e edit — denied num modo customizado continua tirando-a —, e o AgentRuntime acrescenta ao prompt de sistema que venceu, depois dele e sem substitui-lo, uma linha pedindo que o modelo chame garra_status antes de dizer que nao tem acesso a um canal ou integracao: um canal presente na lista channels do relatorio esta conectado, um campo status por canal (quando existir) so conta como conectado em active, e um canal ausente da lista nao basta para negar o acesso. A linha nao fala de ferramentas, porque a lista oferecida no turno e a fonte de verdade delas, e o tools do relatorio do garra_status passa a trazer so as ferramentas que o portao do turno libera, e nao mais toda tool registrada: no piso search o relatorio nao lista mais bash nem file_write, que o turno nega. A linha so entra quando a tool esta entre as oferecidas no turno, entao a CLI (garraia chat/garraia ask, que nao registram a tool) nunca a recebe. O sintoma do WhatsApp nao some so com esta fatia: o relatorio ainda nao enxerga o whatsapp_linked nem os canais push, e isso fica para a fatia do gateway.
  • garra_status enxerga o whatsapp_linked e os canais push, com o mesmo status do /api/channels (#1347, fatias 2 e 3). O relatorio lia so o ChannelRegistry, onde o whatsapp_linked (supervisionado a parte) e os canais push (WhatsApp Cloud, Teams, LINE, Google Chat) nunca entram, entao o Garra conectado ao WhatsApp respondia que nao tinha acesso ao WhatsApp mesmo depois da fatia 1. A tabela de canais e a regra de status sairam do router.rs para um modulo compartilhado, e o /api/channels e o garra_status chamam a mesma funcao: cada item de channels agora traz id, name e status (active / offline). No relatorio so entram canais de mensagens que o operador ligou na config: numa instalacao nova a lista vem vazia (antes vinham oito offline, que o modelo repetia como "configurado e fora do ar"), e um canal ligado e caido sai offline mesmo quando nao precisa de segredo; o /api/channels mantem a regra dele. Web chat, API, CLI e MCP nunca entram na lista. O relatorio ganha execution_profile (standard / isolated-pod), mcp_servers (so nome, connected e contagem de ferramentas, nunca comando, env ou erro) e session.channel, e o session.id sai sempre mascarado nos 4 ultimos digitos (no WhatsApp ele e o numero de telefone inteiro). Num turno de portao restrito (o piso search do WhatsApp) ou numa sessao que nao e provadamente do operador, o relatorio retem o que e do operador (working_dir, project_id, a lista providers, os nomes dos servidores MCP e a versao exata, que cai para major.minor) e lista o que reteve em withheld, para o modelo nao confundir "retido" com "nao ha". Quem fala na sessao sai do que os pontos de entrada gravam (a superficie de cada turno, que so acumula, e as fontes de chat_session_keys), e nao do prefixo do id: a sessao do Telegram resolvida por UUID nao tem prefixo e passava por local. So web chat, API, VS Code e Desktop (e, sem chegar ao gateway hoje, cli e mcp) contam como do operador, e so com a porta fechada (loopback ou gateway.api_key); sessao desconhecida, app mobile (conta aberta em /auth/register), A2A e gateway exposto pelo opt-out sem chave ficam restritos. A tool passa a ser registrada depois de montar os canais push e antes do primeiro canal pull conectar, e guarda so as contagens dos push (guardar os canais fecharia um ciclo de Arc com o estado do gateway). A instrucao do runtime acompanha o formato novo: um canal de mensagens ausente da lista nao esta ligado, a ausencia de web chat, API, CLI e MCP nao diz nada, e a superficie da conversa esta em session.channel. Um teste ponta a ponta pergunta "voce tem acesso ao WhatsApp?" pela ponte de teste e pelo piso do canal e recebe "sim" com o canal conectado e "nao" sem ele ou com a ponte caida.
  • O turno em streaming aplica o prompt do modo, e a persona padrao nao manda mais o modelo chamar garra_status (#1347). O caminho de streaming do AgentRuntime montava o prompt de sistema so com override, prompt do runtime e persona, ignorando o system_prompt_template e o max_tokens do modo que o caminho batch (process_message_with_agent_config) ja aplicava: o mesmo turno no piso search recebia prompts diferentes conforme o ramo. Agora os dois montam o mesmo pedido: o prompt com a mesma precedencia (chamador > modo > runtime > persona) e o mesmo max_tokens (chamador > runtime > modo), e um teste prende que os dois ramos mandam o mesmo prompt, max_tokens e temperature num modo customizado. A temperature do modo continua fora dos dois ramos, e de proposito nesta versao: so o process_message_impl a manda, e ele so roda pelo heartbeat, que nunca tem modo, entao na pratica nenhum turno manda a temperature de um modo. Passar a manda-la mudaria o pedido de todo turno com modo, porque os embutidos declaram de 0.3 a 0.7, e isso fica para quando o provider souber omitir o parametro nos modelos que o recusam; docs/src/modes.md registra o limite. A persona padrao (PT e EN) citava garra_status pelo nome, e ela vale tambem em garraia chat e garraia ask, que nunca registram essa ferramenta; a linha ficou neutra ("use as ferramentas disponiveis nesta conversa"), e a instrucao de consultar garra_status continua vindo do runtime, so quando a ferramenta esta entre as oferecidas no turno.
  • O erro do garraia whatsapp link quando o bridge morre antes de conectar volta a trazer as ultimas linhas do stderr do Node (#1368). A cauda so era lida com o que a task do stderr ja tinha encaminhado; com o filho recem-terminado, a ultima linha, justamente a que explica o erro, ainda estava no pipe, e a mensagem saia so com "o bridge encerrou (codigo 1) antes de conectar". O CI de cobertura pegou a corrida em main. Agora, quando o filho ja saiu, o erro espera o fim do stderr por ate 2 s antes de montar a cauda, que continua redigida.
  • install.sh deixa de tentar o wizard sem terminal em container e CI (#1369). O teste era [ -r /dev/tty ], que so le os bits de permissao; o /dev/tty e crw-rw-rw- em qualquer Linux, inclusive num container ou runner sem terminal de controle, onde abrir o dispositivo falha com ENXIO. O smoke de instalacao limpa da v0.4.4 imprimia main: line 631: /dev/tty: No such device or address e depois "Wizard exited non-zero" para um wizard que nem chegou a rodar. O has_usable_tty agora abre o /dev/tty de verdade (so leitura, dentro de um subshell, com -c exigindo dispositivo de caractere), e sem terminal o instalador segue o caminho nao interativo documentado sem linha de erro. Um CI nao vazio tambem conta como sem terminal, como o Test-InteractiveSession do install.ps1 ja fazia (regra 16): um job de CI com pty nao tem ninguem para digitar. O install.ps1 nao tinha a falha ([Console]::IsInputRedirected consulta GetConsoleMode, nao permissao); a suite dele ganhou os casos espelhados que rodam a sonda real em processos filhos.
  • GET /admin/api/logs passa a ler so a cauda do garraia.log e deixa de responder 500 com byte que nao e UTF-8 (#1371). O handler fazia read_to_string do arquivo inteiro a cada requisicao para devolver as ultimas limit linhas. Com o log do daemon crescendo entre restarts, isso custava memoria e CPU proporcionais a toda a historia do daemon, e a escrita crua que chega pelo descritor herdado (panic, filho com stderr herdado) nao tem garantia de ser UTF-8, o que derrubava a leitura inteira. Agora o /admin/api/logs usa a mesma leitura do GET /api/logs: no maximo 512 KiB do fim do arquivo, com teto que vale mesmo se o daemon escrever durante a leitura, em UTF-8 lossy (o byte invalido vira U+FFFD), e sem a linha partida pelo corte.
  • garraia status | head deixa de terminar em panico (#1371). O runtime do Rust ignora SIGPIPE, entao um println! num pipe cujo leitor ja fechou recebia EPIPE e o processo morria com "failed printing to stdout: Broken pipe" e exit 101 (ou, no garraia logs, com "Error: Broken pipe" e exit 1). Nos comandos que so leem e imprimem, o SIGPIPE volta ao padrao do Unix antes da primeira escrita: o comando sai em silencio no primeiro write sem leitor, como ls | head, e o shell ve 141. Entram status, about, logs (inclusive --follow), doctor, runs list, config check, memory stats|list|search, mcp list, channel list|status, skill list, glob, whatsapp status, ask e desktop --status|--no-launch. O gateway (start, restart, o daemon), o mcp-server e o REPL do chat ficam com o sinal ignorado, porque la um leitor que some tem de virar erro tratado e nunca a morte do processo, e o mesmo vale para os comandos que mudam estado. A decisao e um match exaustivo ate o subcomando aninhado, sem curinga: um subcomando novo, de primeiro nivel ou aninhado (um memory ou config novo, por exemplo), nao compila sem alguem decidir de que lado ele fica, e um teste varre o corpo da funcao para que ninguem troque isso por matches! ou _ =>. No Windows nada muda.
  • garraia start -d deixa de apagar o garraia.log a cada start e de rasgar a cabeca dele (#1371). O daemon abria o log com File::create, que trunca, e sem O_APPEND. Com isso cada start do daemon apagava o log da execucao anterior, justo o que se quer ler ao reiniciar depois de uma queda. E esse descritor vira stdout e stderr do daemon pelo dup2, entao tudo que escrevia cru nele (a mensagem de um panic, um eprintln!, um filho com o stderr herdado) caia no offset proprio do descritor, que comeca em 0, por cima das linhas que o tracing ja tinha gravado: o smoke de instalacao limpa da v0.4.4 achou "Secure MCP Filesystem Server running on stdio" na primeira linha, seguido de meia linha de tracing. O log agora abre em append: cada escrita vai para o fim do arquivo, atomicamente, e as execucoes anteriores ficam. O arquivo cresce sem rotacao, como ja crescia no garraia start em foreground, que sempre abriu o mesmo arquivo em append.
  • Sessao REST sobrevive ao restart do gateway: GET /api/sessions/{id}/history, POST .../messages e DELETE /api/sessions/{id} deixam de responder 404 para sessao que esta no sessions.db (#1372). Os tres handlers so olhavam o mapa em memoria, que nasce vazio a cada subida, e davam {"error":"session not found"} antes da hidratacao que carregaria as mensagens do disco (achado do smoke de instalacao limpa da v0.4.4, em todos os cenarios). Agora a sessao fora da memoria volta do banco, o historico e servido e o turno novo grava ao lado dos antigos; o DELETE volta a revogar os tokens depois de um restart. So volta a sessao que so a superficie REST gravou: canal da linha api no tenant default, nenhuma chave do Chat Sync, tokens so de api e o canal de cada mensagem so api. Sessao de Telegram, WhatsApp, web, mobile ou do garraia chat --persist segue 404 por esta rota ate a propria superficie a trazer de volta, e nada dela e reescrito (o --resume latest do CLI continua achando a sua). Id que nao existe segue 404 sem criar linha, e banco ilegivel da 500 sem readotar nada. O gate de api_key e o origin_guard rodam antes do handler e nao mudam. O working_dir da sessao continua so em memoria: depois do restart a sessao REST volta sem ele. Sessao encerrada pelo DELETE nao volta: revogar os tokens so esvaziava session_tokens, e a linha de uma sessao encerrada ficava igual a de uma viva. O DELETE agora grava api_logout no metadado da linha (so o metadado; tenant, canal e usuario ficam) e a readocao recusa quem a carrega com o mesmo 404 de id desconhecido, como era antes desta mudanca depois do TTL ou do restart. Se a marca nao pode ser gravada, o DELETE responde 500 failed to record logout em vez de ok, com os tokens ja revogados. E POST /api/mode/select deixa de reescrever tenant, canal e usuario da linha que ja existe: o upsert com o X-Session-Id de qualquer id reetiquetava a sessao de outra superficie (uma whatsapp-<numero> ainda sem mensagem, por exemplo) como da API, e a readocao passava a aceita-la. Agora ele so cria a linha que falta e grava apenas o modo. Limite conhecido: a marca so existe para DELETE feito a partir desta versao. Uma sessao REST encerrada numa versao anterior ficou sem ela, e depois da atualizacao volta a ser lida por esta rota como uma sessao viva. A rota e do proprio operador (loopback, ou o gate de api_key num bind exposto) e o historico ja esta no sessions.db dele; quem quiser apagar de vez faz um novo DELETE, que agora grava a marca.
  • O pedido de confirmacao chega ao usuario sem o marcador interno (#1373). Quando um comando de risco pausava o turno, o texto da ferramenta subia inteiro como resposta, e o usuario do WhatsApp (e de todo canal e do web chat) recebia [CONFIRM_REQUIRED:6b2e7f7e9f135cbc] O comando a seguir requer confirmacao.... O marcador sai no unico ponto em que o pedido vira texto do humano, o despacho de ferramenta do runtime, que alimenta as quatro copias do loop e o tool_program - e sai tambem da linha da ferramenta nos sinks de eventos (o resumo e a saida do tool_finished que a garraia chat desenha e que o /ws envia), que o mesmo despacho mandava antes da pausa com o conteudo cru. A frase que fica continua dizendo o comando e "Responda sim para executar". A aprovacao nao muda: a impressao digital segue no ToolResult do historico e no registro de pedidos pendentes, e digitar o marcador nunca aprovou nada.
  • O log do WhatsApp vinculado mostra o final certo do numero conectado (#1373). Jid::last4 contava todos os digitos do JID, inclusive o sufixo de aparelho: 5511555554321:7@s.whatsapp.net virava phone_last4=3217, o final de um numero que nao existe, justamente na linha em que o operador confere qual conta conectou. Agora so a parte de usuario conta (antes do : e do @), o mesmo corte do phoneLast4 da ponte, para JID de telefone, @lid, grupo e JID sem dominio.
  • O gateway sobe a ponte do WhatsApp vinculado deste binario, e nao a da versao anterior (#1373). So o garraia whatsapp link materializava o bridge.mjs, o package.json e o package-lock.json embutidos; depois de um garraia update o gateway seguia lancando o bridge.mjs que estava no disco (o da 0.4.4 nao tem o conserto do @lid) ate alguem vincular de novo. Agora o supervisor do canal regrava, antes de lancar a ponte, so os arquivos que diferem do embutido (diretorio 0700 e allowlist de nome mantidos, I/O em spawn_blocking), e roda o npm ci so quando falta node_modules ou quando nada prova que a arvore e a do lock embutido. Provam o carimbo .garraia-deps-sha256 (gravado quando o npm ci do GarraIA sai 0, e posto em pending antes de um manifesto ser reescrito e antes de cada npm ci) ou o registro do proprio npm, node_modules/.package-lock.json, quando lista exatamente os pacotes do lock embutido e nao e mais velho que a arvore - o que adota sem npm uma instalacao da 0.4.4 cujo npm ci terminou (e nao mais uma pela metade) e o npm ci que o usuario roda a mao. Se o npm do gateway falhar, a ponte nao sobe e a arvore que ele deixou pela metade sai do disco; sem npm na PATH do gateway, a ponte nao sobe e nada e apagado. Nos dois casos o /api/diagnostics e o garraia whatsapp status mostram "sem dependencias" com o passo rode npm ci em <dir> e reinicie o gateway, que agora funciona: o boot seguinte adota a arvore. A sessao vinculada nao e tocada. O garraia whatsapp link usa a mesma regra, e ja nao roda npm so porque o bridge.mjs mudou.
  • O job de cobertura do CI deixa de ser cancelado por tempo (#1377). Ele levava de 21 a 24 minutos ate a v0.4.4, e com os testes da v0.4.5 passou do teto de 30: em main, depois do trem da onda B, saiu cancelado com todos os testes verdes e o relatorio ja gerado. O teto sobe para 45 minutos, que continua pegando um teste travado.

Security​

  • agent.sandbox passa a conter run_tests, git_diff, code_review e repo_search (#1225). Ate aqui so o bash consultava a policy: com mode: all essas quatro tools continuavam spawnando no host. Agora elas passam por um spawn que monta o argv do docker run/podman run sem shell nenhum (argumento hostil do modelo chega literal ao programa), com as mesmas recusas fail-closed do bash (sem backend, binario ausente, fora de unix, imagem com -, mount invalido) e mais uma: backend: ssh e recusado para elas, porque o host remoto nao tem o diretorio de trabalho. No timeout o container e removido (rm -f garra-sbx-<uuid>), e o bash sandboxado passa a fazer o mesmo (antes o container dele seguia rodando depois do timeout, com o workdir montado rw). O working_dir que o modelo passa ao run_tests fica no jail das file tools antes de virar o mount do container, e no gateway o run_tests so existe em standard quando o sandbox docker/podman o cobre (#1272). O ssh + container remoto fica como won't-do: backend: docker com um docker context ssh:// ja entrega isso.
  • TLS configurado pela metade recusa o boot em vez de servir HTTP puro (#1247). Com so gateway.tls_cert_path ou so gateway.tls_key_path no arquivo, o gateway caia em serve_plain sem aviso: o operador pediu TLS e recebia texto claro, credencial de gateway inclusive. Agora garraia start, restart e start -d saem com exit 78 (EX_CONFIG) nomeando o campo que falta, antes de ligar o socket e antes do fork. Escotilha consciente: GARRAIA_ALLOW_INVALID_CONFIG=1 (exatamente 1), que sobe e continua logando o achado como erro. Quem embute o GatewayServer sem a CLI recebe um warn! nomeando o campo ausente no ponto de uso.
  • garraia start recusa subir num bind exposto sem credencial de gateway (#1261). Quebra de compatibilidade para deploys expostos sem chave: start, start -d e restart saem com exit 78 (EX_CONFIG) quando qualquer endereco resolvido do bind nao e loopback e nao ha gateway.api_key nem GARRAIA_GATEWAY_API_KEY. Antes o boot so avisava e subia com o /ws (tools de arquivo e de dispositivo) e o /api/mcp/marketplace/install abertos para quem alcancasse a porta. A recusa vem antes do bind, antes do fork em -d (a mensagem chega ao terminal) e antes de o restart derrubar o daemon atual; TLS nao isenta, e um nome que nao resolve tambem e recusado. A mensagem diz como corrigir usando o binario instalado: garraia init, gateway.api_key, GARRAIA_GATEWAY_API_KEY ou --host 127.0.0.1. Deploy aberto de proposito atras de proxy que autentica tem o opt-out gateway.allow_unauthenticated_network_bind: true, so no arquivo (sem env nem flag, para que a mesma injecao de HOST que expoe o bind nao desligue a guarda), com aviso alto em todo boot. Migracao (faca ANTES de subir a imagem v0.4.5, latest incluso): a imagem Docker, os tres docker-compose*.yml (o .env.example traz a linha vazia, para preencher) e os pods RunPod precisam de GARRAIA_GATEWAY_API_KEY no ambiente (openssl rand -hex 32); sem ela o container sai com 78 e o restart: unless-stopped o reinicia em loop. O chart Helm ganhou gatewayApiKey (Secret proprio, gerado no install e preservado no upgrade, ou existingSecret/value; helm template/ArgoCD precisam de um dos dois) e recusa renderizar a chave via secretEnv. O modulo Terraform/ECS ganhou a variavel obrigatoria gateway_api_key_secret_arn (Secrets Manager ou SSM), injetada como GARRAIA_GATEWAY_API_KEY, e a role de execucao passou a ler os ARNs de secrets — um terraform plan sem ela falha antes de trocar a imagem. Instalacoes do install.sh e a unit systemd ligam em loopback e nao mudam. O desktop, o cookie de sessao (Secure), o alerta do admin e o hot-reload do config.yml passam a enxergar a chave que so vem da env. Nova env GARRAIA_GATEWAY_API_KEY: vence o arquivo, vazia conta como ausente, e mora num campo que o serde nunca le nem grava, entao um save() depois do load() nao a escreve no config.yml. garraia config check passa a dizer Error onde o start recusaria, e reporta a env so por presenca.
  • O bash do garraia mcp-server e do gateway deixa de rodar no host em standard (#1272). Em execution.profile = standard a tool bash so e registrada quando agent.sandbox a coloca dentro de um container docker ou podman que existe no host (todo comando passa por wrap_command, sem fallback para o host); em execution.profile = isolated-pod explicito ela roda no host do pod, com a denylist e o tier arriscado ligados. Em qualquer outro caso (sandbox desligado, o default; ssh, que e execucao remota; bash em elevated; allowlist sem bash; binario ausente) ela simplesmente nao existe: antes, um cat /etc/shadow ou um echo x > /qualquer/lugar pedido pelo modelo rodava no host, porque o tier arriscado so pega o que parece perigoso. A decisao nunca vem de detectar container. Instalacoes existentes continuam subindo: o boot do gateway e do garraia mcp-server avisa uma vez com warn! dizendo por que e como religar, /api/diagnostics ganha o check tools.bash, e o system prompt do garra_agent diz ao modelo que nao ha shell. garraia chat (humano no terminal, com confirmacao) nao muda. A regra vale para toda tool que executa codigo do repositorio: o run_tests do gateway (que roda o scripts.test do package.json, o build.rs e o conftest.py que o file_write consegue escrever) tambem nao e registrado em standard sem sandbox. Em isolated-pod com sandbox exigido e inutilizavel a tool fica de fora, em vez de ser anunciada como rodando no host do pod.
  • O container do sandbox passa a ser fronteira de verdade (#1272). docker run ganha --cap-drop ALL, --pids-limit 512 e --user <uid>:<gid> do operador (podman usa --userns=keep-id), entao nada que o comando deixa no diretorio montado pertence a root nem e device. O mount e o caminho canonico e absoluto do diretorio de trabalho da sessao; relativo, inexistente, com :, ausente (sessao sem working_dir: o cwd do processo nunca e montado no lugar), /, o $HOME ou um ancestral dele agora e recusa fail-closed, em vez de rodar sem o mount ou com o disco inteiro.
  • git_diff e code_review nao executam programa plantado no repositorio (#1272). O git dessas tools roda com core.fsmonitor=false, safe.bareRepository=explicit, core.hooksPath=/dev/null, --no-textconv, todo filter.<driver> da config anulado, nenhuma descida em submodulo (--ignore-submodules=all, diff.submodule=short, submodule.recurse=false: um submodulo tem config propria) e GIT_CONFIG_NOSYSTEM=1; e file_write recusa qualquer caminho com componente .git, para o modelo nao plantar a config que o proximo diff executaria no host.
  • O log deixa de gravar o telefone de quem fala com o GarraIA pelo WhatsApp ou pelo Signal (#1343). O id de sessao desses canais embute o remetente (whatsapp-<telefone>, signal-<telefone>, whatsapp-linked-<jid>), e os spans do AgentRuntime gravam session_id em todo evento do turno: o canal whatsapp_linked ja so logava os 4 ultimos digitos por conta propria, mas o span passava o numero inteiro por baixo. O RedactingWriter do stderr e do garraia.log agora troca toda sequencia de 10 ou mais digitos que nao esteja colada a letra por … e os 4 ultimos; hash hexadecimal e UUID passam intactos. O Debug do escopo de aprovacao mascara a sessao do mesmo jeito, e o do registro de pedidos pausados mostra so quantos ha. O user_id do LINE, que nao e numerico, continua legivel.
  • A chave de um provider so vai para o endpoint da propria entrada llm: (#1370). garraia ask -p openai (e o garra_ask/garra_agent do MCP com provider=openai) lia llm.openai.api_key e descartava llm.openai.base_url: a chave de um endpoint OpenAI-compativel proprio era enviada para https://api.openai.com. O smoke de instalacao limpa da v0.4.4 pegou isso, e a auditoria dos outros pontos de construcao da CLI achou a mesma classe de defeito em mais quatro lugares: -p anthropic e -p openrouter tambem ignoravam a base_url; a autodeteccao (sem agent.default_provider) fazia o mesmo com os tres; o caminho do agent.default_provider lia a base_url da entrada padrao mas a chave de llm.<tipo> (com default_provider: lmstudio e um llm.openai ao lado, a chave do llm.openai ia para o LM Studio) e largava a base_url de um default anthropic; e o --url avulso mandava OPENAI_API_KEY ou GARRAIA_EMBEDDING_API_KEY para o endereco digitado. Agora endpoint e credencial saem sempre da mesma entrada (crates/garraia-cli/src/provider_binding.rs): dentro dela o api_key vence a variavel de ambiente do tipo; sem entrada, o endpoint e o padrao do tipo e a chave so vem do ambiente, nunca de outra entrada; o --url usa LLM_API_KEY ou a chave da entrada com a mesma base_url. A variavel de ambiente do tipo (OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY, inclusive a de um .env do diretorio corrente) so vai para o host padrao do tipo: ela preenche uma entrada sem api_key apenas quando a entrada nao tem base_url ou tem o proprio host padrao (o https://openrouter.ai/api/v1 que o garraia init grava continua funcionando com a chave na env). Mudanca de comportamento: uma entrada com base_url propria e sem api_key deixa de receber a variavel do ambiente, inclusive pelo agent.default_provider (onde ela ja era enviada para a base_url antes desta versao); o OpenAI-compativel manda o marcador not-needed, e anthropic/openrouter recusam com erro. Quem dependia disso poe o api_key na propria entrada. -p <alias> (uma entrada llm: com outro nome, ja aceita pela policy do MCP) passa a funcionar em vez de falhar com "Provider desconhecido", e o garra_agent resolve o provider pelo id com que ele foi registrado, entao um alias de anthropic/ollama/llamacpp (e o proprio llamacpp) nao falha mais com "provider '' not found". Na autodeteccao, um llm.openrouter que declara provider: openai com chave propria volta a ser escolhido, vinculado inteiro (a chave dele vai para a base_url dele); sem chave propria, o candidato cai para a OPENROUTER_API_KEY do ambiente no host padrao, em vez de sumir.

As versoes anteriores a essa ficam no CHANGELOG.md completo no GitHub.