Skip to main content

Changelog

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

The format is based on Keep a Changelog.

[Unreleased]​

[0.5.0-rc1] - 2026-10-30​

Candidato a release da v0.5.0, publicado como pre-release: nao e servido por /releases/latest, nao e oferecido pelo garra update nem pelo updater do desktop, e a imagem Docker sai sem a tag latest. Instalacao com pin — Windows: $env:GARRAIA_VERSION='v0.5.0-rc1'; irm https://github.com/michelbr84/GarraRUST/releases/download/v0.5.0-rc1/install.ps1 | iex; Linux/macOS: GARRAIA_VERSION=v0.5.0-rc1 curl -fsSL https://github.com/michelbr84/GarraRUST/releases/download/v0.5.0-rc1/install.sh | bash.

O que muda de comportamento desde a v0.4.10 (os fragmentos em changelog.d/ viram o changelog completo na estavel): garra chat deixa de ter teto de parede de 120 s por padrao, --timeout-secs vira opt-in e o texto ja transmitido fica no historico marcado como parcial (#1751); o /model aceita presets do OpenRouter @preset/<slug> (#1752) e variantes de roteamento :floor/:nitro/:online/:exacto (#1764), validados por uma chamada minima de 1 token; a API diz quem respondeu (provider/model/fallback no corpo e headers X-AI-Provider/X-AI-Model, #1733) e modelo invalido vira 4xx sanitizado em vez de 500 com o corpo cru (#1726); fallback_providers entra quando o primario recusa a credencial (#1735) e o /api/health deixa de mostrar verde com chave recusada (#1743); o agente sabe a data e a hora atuais (#1725) e ganha a ferramenta somente leitura cli_inspect (#1712); o TTS da Web volta a tocar (#1754); /clear apaga o historico persistido (#1723); no Windows, o MCP filesystem (#1722), o file_jail (#1737), o garraia rollback (#1720) e o diagnostico de voz (#1697) foram corrigidos. Supply chain: wasmtime 50.0.0 com MSRV 1.97 (#1714) e sem a feature cache, tirando o zstd vulneravel do lockfile (#1719); excecoes de advisories renovadas ate 2026-12-31 e ignores mortos removidos (#1667); todos os caminhos de erro do provider passam pelo sanitizador (#1737). CI: a suite unitaria roda tambem em Windows e macOS (#1697, #1762).

[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.

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