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. Novoworkflow_dispatch(inputref, runnerubuntu-latest,permissions: contents: read) que instala o Ollama pinado por versao + SHA-256 verificado, sobe o servidor e puxa o modelo, e rodascripts/dogfood/linux-clean-install.sh --source localcontra o candidato. O job falha se oresumo.txtdisser FALHOU e publicadogfood/linux/**como artifact. Semcontinue-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 sidecargarraia whatsapp link --machine(fatia 1) emite em NDJSON — o payload cru (credencial) nunca chega a webview. A escolha "proprio" vs "dedicado" persiste emonboarding.json(emapp_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" aplicawhatsapp access restrictedewhatsapp 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) viraagent.system_promptpelo novoConfigLoader::set_agent_system_prompt. Oconcluidoso 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 dadefaultampla. - 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 emgarraia-desktop-core::onboarding(puro, com testes). A UI tem um teste Playwright (crates/garraia-desktop/tests/) que abreonboarding.htmlsem Tauri e sem gateway, com umwindow.__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. Odesktop.ymlganhou o jobonboarding-uie um tripwire de grep noassert-ui-assets. O roteiro manual do pareamento real (com celular) esta emdocs/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/.appno 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 emcrates/garraia-desktop/src-tauri/node-runtime/antes docargo tauri build, e otauri.conf.jsonmapeia esse diretorio para os resources do Tauri. Onode/e obridge-whatsapp/ficam exatamente num dos caminhos do contrato do resolver (fatia A), relativos ao sidecargarraia: ao lado do exe no Windows, emContents/Resources/no macOS e em/usr/lib/garraia-desktop/no.deb/AppImage. O Node privado e usado sempre por caminho explicito, sem mexer noPATHglobal 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.ymlganhou jobssmoke-windows/smoke-linux/smoke-macosque, por plataforma, pegam o instalador gerado, extraem a arvore num caminho com espaco, escondem todo Node global doPATHe rodam o sidecar instaladogarraia whatsapp self-test --json, exigindonode_origin == privado,self_test == oke o carregamento de baileys + pino + sharp, alem de provar que onode/e obridge-whatsapp/node_modulessao 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 respondeGET /api/healthe morre junto com o app em <=15s), a atualizacao e a desinstalacao (update-{linux,macos,windows}: v0.4.9 -> novo, a sessao em~/.garraia/whatsappsobrevive, e a desinstalacao tira o pacote e preserva o dado do usuario). Semcontinue-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 emdocs/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 emnode-version.txt, hoje v24.22.1 LTS Krypton), verifica a PROCEDENCIA por GPG contra as chaves do release team vendorizadas no repo (keys/*.asc+ allowlistkeys/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 comnpm ci --omit=dev --no-audit --no-fundusando o Node e o npm do PROPRIO tarball (sem cross-install), geraTHIRD_PARTY_NOTICES.txt(Node + todos os pacotes denode_modules, falhando se algum nao tiver licenca identificavel) e gravamanifest.json(versao, plataforma, sha256, fingerprint do assinante, estado do lock da ponte). Layout combinado com o resolver Rust:node/node.exeno Windows,node/bin/nodeno Unix. Novo workflow.github/workflows/node-runtime.yml(matriz das 3 plataformas empull_request,workflow_dispatcheworkflow_callpara 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, semNODE_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 embridge.mjsnem no codigo Rust da ponte. Refs #1636. - Node privado por caminho explicito para a ponte do WhatsApp (#1636).
O modo vinculado agora resolve o
nodepor 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 aocurrent_exe()), e so entao a PATH. O resolver nunca muda oPATHdo 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). npmso e exigido quando ha dependencias a instalar (#1636). Com a arvore pronta (node_modulesja no lugar), o pareamento e o atendimento sobem semnpmnenhum;npmso e cobrado quando o plano e instalar, e sua ausencia vira um erro claro em vez de uma falha despawncrua.- 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.jsonembutido no binario, onode_modulese copiado (atomicamente, sem shell) para o diretorio da ponte em vez de rodarnpm 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 rodabridge.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
onboardingemgarraia-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 dostate.rs. A tela do QR mapeia os eventos NDJSON dogarra whatsapp link --machine(fatia 1) via o tipoEventoNdjson(serde, tolerante a eventos desconhecidos).Avancarnao pula o vinculo,Falhoupermite tentar de novo e nenhum estado terminal avanca sozinho. Inclui o tipoOnboardingPersistido(round-trip paraonboarding.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 dolink: nao exige terminal, nao le stdin e emite NDJSON no stdout (uma linha por evento, comschemaeevent—status,qr,error,log). O QR sai como matriz de0/1pelo mesmo motor de QR do terminal, via a novaqr::modulesemgarraia-channels(um motor so, sem risco de polaridade invertida); o payload cru nunca entra. A ordem que e contrato se mantem:session_savedantes dechannel_enabled. Erros tambem saem como NDJSON e o exit code segue os mesmos sysexits dolink. Olinkinterativo fica inalterado. Para o app desktop consumir como sidecar; verdocs/whatsapp.mdsecao "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 workflowdocs-site.yml, que compila cada PR e publica no Pages a cada push namain. Publicado em https://michelbr84.github.io/GarraRUST/ (url/baseUrlconfiguraveis por env). Nao altera oci.ymlnem o sitegarraia.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 emdoctorficam como follow-up). - Fundacao de schema de billing do Garra Cloud: plan/subscription/invoice (#1652).
Migration 035 do
garraia-workspaceadiciona tres entidades.planse um catalogo global (sem RLS, mesma classe de roles/permissions), semeado apenas com o planofreesem preco (price/currency NULL = preco nao fixado neste repositorio MIT).subscriptionseinvoicessao por tenant sob FORCE RLS (isolamento porgroup_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 funcaogarraia_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_diagnosticsnogarraia-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 viainclude_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 comodisponivel. Consulta sob demanda pela nova toolgarra_capabilities(filtrosname/category, limite de itens; sem filtro devolve só contagens — consumo de contexto baixo), integrada aogarra_status, que ganha a seçãocatalogcom 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 (itensoperator_onlyretidos e contados).
Changed
- A automacao de release marca
make_latest: falseexplicito para prereleases. Orelease.ymlja punhaprerelease: trueem 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 ogarra update(fixo em/releases/latest) e o updater Tauri (que consultareleases/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,mincom o teto do operador) eturnos_ultima_horano diagnostico do gateway (toolgarra_diagnostics, operator-only). Ogarraia doctoroffline 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 chamaWhisperClient::health_check()com orcamento detimeouts.health(nao trava o boot num host que engole pacotes) e logaSTT readyso quando o Whisper responde, ou umwarn"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 itensgarra_diagnostics(#1669) edesktop(app com o Assistente de Primeiro Uso, #1636), e corrige owhatsapp_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 dowhatsapp self-test. O caminho da allowlist no catalogo passa a ser o que se escreve noconfig.yml(channels.whatsapp_linked.allow, sem umsettings.que o YAML nao tem, porque o campo e achatado); o mesmo vale para omax_turnos_per_minuteda #1650. Omcp-clientregistra o jail fail-closed com o servidor filesystem fixado em 2.0.0 (#1684), einstalacao-atualizacaopassa a citar oinstall.ps1e o canal de pre-lancamento. O runbook de release (docs/releasing.md) ganha o passo de subir ocatalog_versionjunto 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 comevent: error, a rota/api/ttscaia no fallback de texto sem avisar ninguem, e odocs/voice.mddizia que funcionava. Agora o cliente leGET /gradio_api/info, monta odatacom exatamente o numero de valores que o servidor declara (usando osparameter_defaultdele, ou os defaults do app oficial), cacheia a assinatura e cai no layout oficial de 7 valores quando/gradio_api/infonao responde. Servidores customizados de 4 parametros continuam funcionando. Umevent: errorno SSE agora vira erro com a mensagem do servidor, e/api/ttse o canal de voz do Telegram registramwarncom 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 ohost:portreal derivado dogateway_urlque/api/healthja 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/healthe auth-free, entao expor o diretorio ali vazaria o home/usuario do SO a um peer anonimo num bind nao-loopback. docs/voice.mdcorrige o exemplo de/api/stte documenta o pin do torch. O exemplo usava--data-binary, mas o handler exige multipart com o campofile(um corpo cru volta400 No audio file provided); agora usa-F "file=@audio.wav". A pagina tambem explica que o cliente se adapta a assinatura dogenerate_tts_audioe avisa que ochatterbox-ttsfixatorch==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/latestnao resolvia, o ultimo fallback deinstall.sheinstall.ps1pegava o primeiro release nao-draft — e prereleases contam, entao umcurl | sh/irm | iexpodia instalar um-rc/-beta/-alpha. Os dois instaladores agora pulamprerelease: truetambem (alem dedraft: true) e escolhem a estavel mais recente; oinstall.shclassifica cada release portag_name/draft/prereleaseem qualquer ordem (scanner com profundidade de chaves, imune a chaves{}dentro debody). Para instalar um RC de proposito, pineGARRAIA_VERSION=vX.Y.Z-rcN. - Fixa o
@modelcontextprotocol/server-filesystemautoprovisionado em@2.0.0(o upstream deprecou a serie por data2026.8.31e voltou ao semver;latestagora e2.0.0). O handshakeinitialize+tools/liste identico entre as duas versoes (mesmas 14 tools, mesmos argumentos de caminho). Ummcp.jsonexistente fixado em@2026.8.31continua 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>), entaollm.ollama-qwen3.provider: openairegistra sob o idollama-qwen3. OGET /api/providersainda comparava os ids contra um catalogo fixo de TIPOS, entao o console marcava "OpenAI (Not Configured)" / "Ollama (Not Configured)" e nunca mostravaollama-qwen3. A resposta ganha um campo aditivoinstances(id,kind, modelo, se e o padrao, se esta em fallback) e oactivedo catalogo passa a considerar o tipo configurado quando ha instancia daquele kind.PATCH /api/providers/defaultePOST /api/providers/testja 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.mdainda anunciava a v0.3.9 (set 2026) como novidade; agora reflete a v0.4.9 (out 2026) com base na prosa de abertura doCHANGELOG.mde no link da wiki correspondente. - Atualiza
spin0.10.0 -> 0.10.1 e 0.9.8 -> 0.9.9 noCargo.lock(0.10.0 e 0.9.8 foram yanked; derrubavaSecurity Auditecargo-denynos PRs). - Ponte do WhatsApp agora sobe o Node no Windows (#1636). O
env_clear()da contencao do processo filho tiravaSystemRoote os demais essenciais de sistema, e o Node abortava logo no boot comAssertion 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 enpm ci, nao so o novoself-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 viagetppid()— 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_drope oDropso 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 ObjectKILL_ON_JOB_CLOSE(modulosjobobjectemgarraia-channelsegarraia-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 comkill(-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 comTerminateProcesse afirmar que nenhum descendente sobrevive. find_executableda ponte do WhatsApp so aprova arquivo com bit de execucao no Unix; umnodeem0644deixa de passar na deteccao e de virarnpm cicom 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 doctoravisa quando ha outra copia degarraia/garranoPATHcom versao diferente da que esta rodando (ex.: build antigo em~/.local/binmascarando o.debnovo), listando os dois caminhos e o fix; campobinaryno--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 linhaWhatsApp vinculado caiu; reconectandonao 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 apontatauri-plugin-updatereutoipa-swagger-uipara forks vendorizados (#1668) entrou depois da v0.4.9, e o Dockerfile nunca copiavathird_party/: ocargo chef cookabortava 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 copiathird_party/no planner e nos dois lados docook; o testedockerfile_copies_every_patch_path_outside_cratesprende a regra em todo PR, e o novo workflowdocker-build.ymlconstroi a imagem (sem push) e responde/pingquando muda o Dockerfile, oCargo.toml/Cargo.lockda raiz outhird_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/diagnosticsdeixa de acender amarelo numa instalacao nova (#1695). A linhatools.capabilitiessaiawarningem toda instalacao limpa, porquedevice_read/device_execute(sem adapter de hardware) etelegram_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 viranot_configured(neutro, como obashsem sandbox desde a #1471): as tools de hardware devolvemnot_configuredquando nenhum adapter (hardware.mqtt/hardware.home_assistant) foi declarado, e o registro de capacidades tratatts_unconfiguredcomo nao configurado. Configurado e quebrado continua amarelo: adapter declarado sem nenhum dispositivo segueno_devices, e canal configurado fora do ar seguechannel_offline. O motivo de origem continua noreason_code.- Sem Node.js na maquina, o boot nao provisiona mais um MCP
filesystemque nunca sobe (#1696). A entrada autoprovisionada rodanpx, 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 traznpx) o spawn falhava comNo such file or directory, o supervisor gastava os restarts e o/api/diagnosticsficava amarelo comcausa: other. SemnpxnoPATHo boot nao grava omcp.jsonnem cria o workspace, e a linhamcp.serversficaskippeddizendo o que falta e como ligar ofilesystem; as file tools nativas seguem disponiveis, e o primeiro boot depois de instalar o Node.js 20+ provisiona sozinho. Ummcp.jsonexistente continua intocado, e servidor configurado que nao sobe segue acusando. - No Windows, o
/api/diagnosticsdeixa de dizer que o workspace esta "fora do data_dir" (#1698). Odata_dire canonicalizado, e no Windows o canonico e\\?\C:\..., enquanto as raizes domcp.jsonsao gravadas comoC:\...; a comparacao crua falhava e toda instalacao Windows viamcp.filesystem_roote 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 dodata_dirque 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 faltagarraia 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
jsonwebtokentroca derust_cryptoparaaws_lc_rs: comorust_cryptoe 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 ocrypto-bigint 0.5.5vulneravel, que so tem fix em 0.7.6 — sai do lockfile sem nenhuma excecao nodeny.toml/audit.toml. Trade-off documentado no comentario doCargo.toml: oaws-lc-syscompila C nativo (cmake + perl, presentes nos runners).
Security
- Revoga de
garraia_appo 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 emtask_attachments/message_attachments/message_reactions/message_mentions/doc_page_mentions, DELETE emdoc_pages/user_email_change_requests, e UPDATE+DELETE emdoc_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 comogarraia_purge, nunca pelo pool do app. O caso com peso de seguranca eaccount_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
ctutils0.4.2 -> 0.4.5 noCargo.lock(RUSTSEC-2026-0385: geracao de mascara e comparacoes deu128sem tempo constante). Entra pordigest0.11, usado porargon2/blake2(hash de senha) ehmac. Sem mudanca de codigo. - Atualiza a familia
wasmtime/cranelift/wiggle49.0.3 -> 49.0.4 noCargo.lock(RUSTSEC-2026-0379, critica, e RUSTSEC-2026-0378 no sandbox de plugins). - Atualiza
rustls-webpki0.103.15 -> 0.103.16 (RUSTSEC-2026-0382) ewebpki-roots/webpki-root-certs1.0.7 -> 1.0.10 (RUSTSEC-2026-0381/0383) noCargo.lock. - Ignora com prazo (2026-11-20) RUSTSEC-2026-0381 para o shim
webpki-roots0.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-filesystemque 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 emOPERACOES_COM_CAMINHO(revisao humana). Tools de outros servidores MCP seguem passando normalmente. - Jail e workdir por orquestrador no
garra_agentdo 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 dobash. 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_rootsdeixa 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 obashrecusado. As mensagens das file tools e dogit_diff/code_reviewque 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: emisolated-podtodos os orquestradores compartilham o filesystem do pod, entao o workdir isola obashso no sandbox docker/podman. - Corrigidos os advisories RUSTSEC-2026-0367, -0368 e -0369 (zip 4.6.1 e 8.6.0). O
tauri-plugin-updatere outoipa-swagger-uipassam a usar zip 9.0.5 por forks vendorizados emthird_party/(verthird_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
unmaintaineddo 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 executambash, nao escrevem arquivo, nao enviam mensagem proativa, nao agendam, nao acessam dispositivo e nao gravam memoria, mesmo com/mode codeou com oauto. 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/dangerousAcceptInvalidHostnamescontinuam aceitos na config (sem quebrar instalações) mas são ignorados, com aviso. Resolve os 4 alertasrust/disabled-certificate-checkdo 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/:overridesnopackage-lock.jsonresolvem 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.3permanece 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/exportdevolvia 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/meaceitaemailsem nunca trocar o e-mail as cegas.users.emaile 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 emuser_email_change_requestscom estadopending+ 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/meenfileira o apagamento que antes era "deferido a um worker futuro". A mesma transacao que marca a lapide e revoga as sessoes grava o pedido emaccount_deletion_requestscom carencia de 30 dias; o novoaccount_purge_workerdestroi conteudo e identificadores do titular (mensagem, comentario, memoria, arquivo + versoes + blob no object store) e deixauserscomo lapidestatus = 'purged'. A linha deusersnao e removida porque cinco FKsNO ACTIONapontam para ela e remove-la apagaria contexto de terceiros — mensagem de outro membro nunca e apagada por pedido de um.audit_eventssobrevive desidentificado (actor_label,ipeuser_agentviram NULL): e a unica prova de que o apagamento aconteceu. Sem object store ligado o pedido NAO e fechado — declararcompletedcom blob vivo seria mentir no audit.- O worker de apagamento roda com credenciais dedicadas e e retomavel. O
role
garraia_purge(migration 034) temEXECUTEnas quatro funcoesSECURITY DEFINERde apagamento e mais nada — nenhuma privilegio de tabela, nao eBYPASSRLS; o pool validaSELECT current_userigual aos demais dedicados. O worker so nasce seGARRAIA_PURGE_DATABASE_URLestiver definida; sem ela o warn no log e explicito e o procedimento manual emdocs/legal/data-subject-requests.mdsegue 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 doCONTRIBUTING.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 doSUPPORT.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 mcpvalida 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 uninstallremove a CLI da maquina (binario, aliasgarra, wrappers do Termux e backups.old/.new);--purgeinclui 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--yesgarra_askno 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 envelopegarra.ask.v1do servidor stdio. A tool é desligada por padrão: só aparece comgateway.mcp_http.allow_ask: true, aceita só o modelo default do projeto enquantoask_allowed_modelsestiver vazio, e tem orçamento próprio deask_budget_per_minute(padrão 10) que nunca divide o de envio. O núcleo dogarra askfoi extraído para o crategarraia-ask, compartilhado pela CLI e pelo gateway;garra aske ogarra_askdo stdio seguem iguais. Refs #1612- Expõe
garra_send_message/garra_aska 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 emcp_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_agentno 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 envelopegarra.agent.v1com 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 comgateway.mcp_http.allow_agent: truee comagentna listatoolsdo orquestrador. Tem orçamento próprio deagent_budget_per_minute(padrão 2) e teto de tempoagent_max_seconds(padrão 300, máximo 600), e não divide cota com envio nem comgarra_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 paragarraia-gateway/src/agente_mcp.rs, usado pelo stdio e pela ponte, sem mudança nogarra 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 crateregex, tempo linear) que se somam aos padroes embutidos e viram[REDACTED]nas mesmas superficies da redacao embutida:garra_send_message, o erro degarra_aske a saida degarra_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 peloconfig check, sem derrubar os demais. Default vazio: comportamento inalterado. - Teste de contrato do
bashpela ponte MCP HTTP (#1626, item 3). Dois testes emmcp_http_contract.rsexercitamgarra_agentde ponta a ponta com um provider falso que pede a toolbash: o comando roda e a saida volta ao modelo, e um comando destrutivo e barrado pela denylist doBashTool. 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.mdganha 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.mdganha 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.mdpassa a classificar controlador/operador por fluxo com inventario de suboperadores;docs/compliance/lia.mdnasce (LIA das sessions condicionado a revisao legal externa) e o DPIA o referencia;docs/compliance/incident-response.mdalinha a notificacao ANPD a orientacao de 3 dias uteis e ganha anexo de tabletop de 1h (execucao pendente);docs/src/pricing.mdrecebe a grades Community/Beta/Pro R$39/Studio R$149 com quotas e regras de franquia LLM;SUPPORT.mdedocs/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)], eclippy.tomllibera os dois em#[cfg(test)]. As 4 ocorrencias de producao viraram erro tipado (400em 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)noci.yml: service container depostgres:16.8-alpine+ as env vars que o bootstrap doGatewayServerexige (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]deauth_test.rs,gateway_integration.rsevoice_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 domarkdown.rspermanece — é sonda, não dívida. Também neste PR: baseline.quality/baseline.jsonre-medido comfreeze-baseline.py --adopt-current-file-metrics --reason '#1570'(campos de tamanho passam a descrever o repositório atual;coverage72,0% eaudit0 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çoollamado compose (ollama list) — ogarraiae 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) edocs/legal/tos-draft.md(termos de serviço);docs/operations/comsupport-policy.md,version-support-policy.md,deprecation-policy.mdesla-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).
tokio1.53.3,serde_json1.0.154,uuid1.28.0,libc0.2.190,zeroize1.9.1,aws-sdk-s31.152.0, a familiaopentelemetry0.33.1, o Tauri 2.12.2 com os pluginsglobal-shortcut2.4.1,shell2.5.0,notification2.7.0,dialog2.8.3 eupdater2.14.0 (o desktop deixa de arrastarsigchld, osignal-hook0.3 e a arvorewindows0.61),@playwright/test1.64 nos testes E2E e a imagem base doDockerfileemrust:1.99-slim. Substitui os quatro PRs do Dependabot abertos com o mesmo conteudo, que ficavam vermelhos nocargo-denypor causa do #1630.
Removed
- Rascunhos legais e precificacao saem do repositorio publico (#1616).
docs/legal/(ToS, Privacy, DPA, direitos do titular),docs/src/pricing.mdedocs/business/cost-model-inputs.mddeixam 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.jsonpublicado. O desktop nao e vendavel como distribuicao empacotada: nao havia DMG no macOS, otauri-plugin-updaterestava ligado compubkeyvazio e nenhum workflow publicavalatest.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: trueebundle.macOS.targets: ["app", "dmg"]explicitos — sem eles o bundler nao emite o.app.tar.gznem o DMG que o updater e o macOS precisam. -
release.yml: jobbuild-macos-desktop(macos-latest) compila o DMG pelo mesmocargo tauri build, e o jobreleasepassa a gerar e publicarlatest.jsonassinado ao lado dos binarios — o endpoint que oupdaterdo 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 obuild-installer.ps1estagia o.sigvizinho do.msi/-setup.exeque o Tauri 2.x emite (#1568) -
Domínio dos caminhos de contato e compra corrigido para
garraia.org(#1574). Odocs/src/pricing.mde oCONTRIBUTING.mdapontavam paragarraia.cloud, que não resolve (NXDOMAIN): link de compra, demo, política de privacidade e os e-mailsenterprise@esecurity@estavam num domínio morto. O domínio real do projeto égarraia.org— resolve,/pricing,/demoe/privacyrespondem 200, e o MX existe (mailserver.purelymail.com). OSECURITY.mdjá diziasecurity@garraia.org; oCONTRIBUTING.mdcontradizia. 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.mddeixa 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
execveem /data por filhos de processos lançados vialinker64; o bash tool e onpm cido 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
bashnegava QUALQUER comando com|,&&,||ou;— era a opcao fail-closed de #1105, e por isso nunca houve bypass, mas derrubava o pipeline legitimo: comgit *egrep *declarados, umgit status | grep modificadomorria 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 | shsegue negado enquantoshnao estiver declarado, e um pipeline nao concede autoridade que o operador ja nao tenha dado —a | bcom 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 dosafety_gatesegue na frente de tudo. Mensagem de negacao e prompt doHostComAllowlistatualizados 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_secse 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 únicoget_me()e tratava QUALQUER erro como "Telegram token inválido" — sob Termux/Android uma falha de SSL/DNS (Bionic semSSL_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 updateegarra rollbackdeixam 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 (sysexitsEX_SOFTWARE), no padrao ja usado porconfig check,doctoredesktop. -
fix(cli/gateway):
.enve lido apenas do diretorio atual, sem walk-up pelos pais —dotenvy::dotenv()edotenvy::from_filename(".env")sobem os diretorios pais ate achar um.env, e um arquivo na raiz do repo injetavaGARRAIA_CONFIG_DIR,GARRAIA_EXECUTION_PROFILEe credenciais em todo comandogarrae em todo boot do gateway rodado de dentro de uma subpasta; CLI e gateway agora usamdotenvy::from_path(".env"), que eFile::openpuro: o.envdo proprio diretorio continua valendo, o do pai deixa de valer (#1604) -
docs(channels):
channels.mdpara de dizer quewhatsapp_linkednao existe noconfig.yml— o canal de dispositivo vinculado tem canal proprio nagarraia-channels, validacao noconfig checke entrada documentada (type: whatsapp_linked) nowhatsapp.mdcom 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_locknuma conexao do pool e tentavam liberta-lo noutra, viatokio::spawndentro doDropde um guard. Advisory lock no Postgres e por sessao:pg_advisory_unlockchamado noutra conexao devolvefalsee nao faz nada. Como o sqlx devolve a conexao ao pool sem resetar a sessao (semDISCARD ALL, e oping()dotest_before_acquiree so umwrite_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 UMAPoolConnection, fixa ela do acquire ao unlock, e se o unlock devolverfalsefecha 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 paralocked_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.rsetasks_recurrence_worker.rssao duas copias do mesmo defeito; corrigir so uma seria tratar sintoma. LiteraisLOCK_SQL/UNLOCK_SQLcomoconst(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_workervinculavabatch_size: i64(bigint) contradue_task_recurrences(timestamptz, int)(migracao 033). O Postgres nao faz cast implicito debigintna 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 queuploads_workerja fazia. -
tests/rest_v1_task_recurrence_worker.rs(novo) etests/rest_v1_uploads_delete_worker.rs(estendido) contam os holders do lock viapg_lockse 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 comleft: 1, right: 0.
Refs #1611
- Ledger: o
linedas 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 pelocheck-ledger-anchors.pya partir do código atual (739/750/871/2205), mas ocodeql-reapply-dismissals.sh --applyvalida olinecontra o span do alerta registrado no GitHub — e enquanto o CodeQL não reanalisar a main, esses números divergem e o--applyaborta 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 viacodeql-rekey-ledger.py, que é feito pra isso).
Refs #1618
- O loader valida
GARRAIA_CONFIG_DIRna ingestão: valor placeholder de.env.example(ex.:/custom/config/pathvindo do.envdo CWD) ou vazio agora cai no config_dir padrão com aviso, em vez de derrubar qualquer comando comEACCES (os error 13)opaco — inclusive ogarra config check(#1620). - Schema de
garra_agentanuncia o teto do operador (#1626).timeout_secspassa a sair commaximumedefaultderivados deagent_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_smokecopiava o binario para um tempdir e ospawnrecebiaETXTBSYquando outro teste do mesmo processo ainda tinha o descritor de escrita da propria copia aberto entreforkeexec— agora ospawntenta de novo nesse erro e so nele.hub_inacessivel_registry_fica_vazioobtinha 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) deuploads.rsesession_workspace.rs(#181–#188). Auditoria por entrada concluiu falso-positivo nas duas causas-raiz: o caminho do staging nasce dePath<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 dosession_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.jsone no.mdcom justificativa, disambiguators nos snippets ambíguos (#186/#187) e regressão novao_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 olinede uma entrada espelha o span do alerta vivo (é o que o--applyvalida) — enquanto o CodeQL não reanalisar, código e alerta divergem e o valor derivado derruba o--applyfail-closed; as 4 linhas seguem o alerta, e o reancoragem automático só entra após re-análise ou viacodeql-rekey-ledger.py(corrigido no PR seguinte). Dismissals aplicados viacodeql-reapply-dismissals.sh --applyapós o merge.
Refs #1618
- Vinte e quatro advisories RUSTSEC fechados de uma vez (seis do
quinn-proto, quatro dorustls, treze da arvorewasmtime/wasmtime-wasi, um dounsafe-libyaml), e ocargo-denyvolta a passar sem nenhuma excecao nova (#1630).rustls0.23.45 -> 0.23.46 (RUSTSEC-2026-0346 a -0349:Debugde 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 porServerHellonao autenticado);quinn-proto0.11.16 -> 0.11.18 (RUSTSEC-2026-0338 a -0343: panics e exaustao de memoria acionaveis pelo peer remoto);wasmtime/wasmtime-wasi49.0.2 -> 49.0.3 (RUSTSEC-2026-0352 nowasmtime-wasi, escape do sandbox de filesystem ao renomear symlink com barra final, e RUSTSEC-2026-0353 a -0364 nowasmtime: panics do host no component model, limites de handle table e tags desalinhadas — sem subir o MSRV, que segue 1.96);chacha200.10.0 (yanked) -> 0.10.2. Oserde_yaml0.9.34, que arrasta ounsafe-libyamlarquivado (RUSTSEC-2024-0449, sem versao corrigida), e trocado pelo fork mantidoserde_norway0.9.42 — mesma API, mesmo comportamento,unsafe-libyaml-norwaypor baixo — emgarraia,garraia-config,garraia-gateway,garraia-learningegarraia-skills. Nenhuma entrada foi adicionada aoignoredodeny.tomlnem doaudit.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 exposicaoHostComAllowlist: quando a config declara padroes de comando (prefixo* no fim ou comando exato), oBashToolentra 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 dosafety_gatesempre na frente. docs/voice.mdganha a secao "Known pitfalls with the stock app" (#1539). Duas armadilhas de quem sobe omultilingual_app.pydo 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 URLshttps://passadas cruas aolibrosa.loade 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 dotelegram_send(chat da sessao, ouchat_idexplicito so se estiver emproactive_chat_ids), compartilhada emresolver_alvo_telegram. Mapeada paraMessageSendna 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 aceitaMessageContent::Audioapenas 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(guardaChannels/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 owhatsapp_linkedtem motor por principal, os demais sao listados com honestidade (policy: not supported yet / defaults). Para owhatsapp_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/accessrecusa com 409elevation_confirmation_requiredsemconfirm_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]eGET /admin/api/whatsapp/access/exportproduzem um documento versionadogarraia.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-piimascara 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) exigeChannels/Updatee e auditado.garraia whatsapp access import <arquivo> [--confirm-widening] [--dry-run]ePOST /admin/api/whatsapp/access/importvalidam formato e versao (formato desconhecido e versao de major futuro sao recusados), recusam export redigido, substituem a politica inteira (limpando o legado e preservandoenabled/default_mode/sessao), mostram o impacto por principal pelo mesmo motor do--dry-rune exigem confirmacao explicita para qualquer alargamento. A escrita e atomica com backupconfig.yml.import-bak(0600, sem seguir symlink) para rollback, e o import vai para o audit como a acaoimport.
Alterado
garraia doctormostra o shell do agente no Termux (#1560). O bloco Termux ganhou o itemTool bash, decidido pela mesma funcao do gateway e domcp-server: sem docker/podman (sempre o caso no Android) e semagent.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 modoHostComAllowlistemdoctor,/api/diagnosticsegarra_statusagora diz explicitamente "SEM sandbox".docs/installation.mddocumenta o shell no Termux.
Corrigido
-
.quality/baseline.jsondeixa de dizercoverage_pct: null(#1566). O #1573 consertou a corrida que impedia o ratchet de achar olcov.infoe fixou os numeros num doc versionado, mas o baseline estruturado — o arquivo que ocompare.pyde 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 jobCoverage (cargo-llvm-cov)do run 37538157886 emmainno commit13e6f1dc, e gravados pelofreeze-baseline.pya partir dolcov.infodaquele run — nao a mao, o invariante anti-fraude continua valendo. No mesmo passo o ratchet estrito aperta omax_file_linesde 10.513 para 6.933, o valor real, e corrige omax_file_pathparacrates/garraia-config/src/check.rs: o antigo apontava paragarraia-agents/src/runtime.rs, que ja nao e o maior arquivo do repo. Os contadoresfiles_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 seguereport-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 oscripts/ci/coverage_gate.pyreprova o job abaixo deCOVERAGE_FLOOR_PCT, hoje70.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 olcov.infopelo mesmoparse-llvm-cov.pyque escreve o baseline, para que os dois numeros nao possam divergir por existirem dois parsers; compara com>=, inclusivo; e e fail-closed —lcov.infoausente 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.jsonda 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 decargo build --releasee so resolve se o cliente MCP abrir exatamente na raiz do repo — em worktree limpo o servidor nunca subia. Agora usagarradoPATH, como omcp.json.examplesempre usou. No mesmo arquivo faltavaGARRAIA_MCP_ENABLE_TOOLS=1, sem o qualtools/listanuncia apenasgarra_askegarra_agentnunca 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-chatterboxe mantinha o modo voz desligado contra ummultilingual_app.pyde 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_checke o checktts-chatterboxdo/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.mdtroca ocurl /healthdo 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 armsopenaieollamado bootstrap registravam todo perfil com o id do tipo, entao duas entradasprovider: openai(um llama-server local e um Ollama OpenAI-compat, p. ex.) colidiam em"openai":get_providerso achava a primeira e a ordem de registro vinha da iteracao doHashMapde config — o provider efetivo, e o default, eram sorteados a cada boot. Em 0.4.6 a mesma config alternava o/api/health.modelentre 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, eagent.default_provider/fallback_providers/model=<chave>/...passam a enderecar a instancia certa. Perfil cuja chave e o proprio tipo (openrouter) nao muda; umagent.default_providerque nomeia o tipo sem existir como chave no mapallm: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
LlmRequestdo primario intacto,modelincluso — emodele o nome que aquele backend entende. Um llama-server local servindoglm53-flashcaia, o openrouter recebiaglm53-flashe respondia 400is not a valid model ID; o usuario viaall providers failed, como se o fallback nem existisse. Agora omodelso sobrevive quando nomeia o proprio fallback (openrouter/autoindo 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 ollm.<perfil>.modelque a config ja dizia. Vale nos dois lacos, batch e streaming. (4) Emstream: trueoErrdo turno era descartado por umif let Ok(..): o canal de deltas fechava sem nenhum delta e o SSE terminava emfinish_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 mentirstop. -
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. Mashf.co/unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XLtem duas barras, e id do openrouter e semprevendor/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/completionssemmodeldeixa de mandar"gpt-4"ao backend (#1541). O handler faziabody.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 404model 'gpt-4' not found— a API OpenAI-compat era inutilizavel justamente para o cliente que omitemodelporque quer o default do servidor. Agora o corpo semmodelchega ao provider como o sentinel vazio que ele ja sabe resolver com o proprioconfigured_model(mesma decisao que o A2A tomou), e o campomodeldo 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. -
restartpara de criar daemon orfao quando o gateway roda sob systemd (#1542).garraia restartmatava 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 contraAddress 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 osystemctl [--user] restartcerto. Recusa em vez de delegar porque a unit sobe peloExecStartdela 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.capabilitiesdo/api/diagnosticsdiziamgarraia mcp restart <nome>, masgarra mcpso temlist,inspect,resourceseprompts— 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 distingueretrying(o supervisor reconecta sozinho) defailed(osmax_restartsacabaram e so o restart manual traz de volta), para ninguem reiniciar a mao um servidor que ja ia voltar. -
bind_fail_closed_clideixa 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 usesem nada errado no binario. As portas entregues agora ficam num registro process-wide. So alvo de teste. -
Perfis
llamacppeanthropicvoltam a resolver por chave, e o fallback por tipo volta a funcionar (#1554). O #1540 deu id por perfil aos armsopenaieollamado bootstrap e deixouanthropicellamacpppara tras. Ollamacpptinha um agravante proprio: o id que ele registra (llama-cpp, com hifen) nao e igual aoprovider: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, eagent.default_providerapontando 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. AgoraAnthropicProvidereLlamaCppProvidertemwith_namee os dois arms registram pela chave do perfil, eresolve_registered_provider_idconhece a traducao tipo -> id canonico a partir de uma constante publica, em vez de um literal repetido. -
agent.default_providerque 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 tinhadefault_provider: ollamacomllm.local.provider: ollama, porque o registro passou a serlocal. A correcao do #1554 teria repetido o estrago comanthropicellama-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 varreduraMETA_SHELLolhava a string crua e tratava parenteses,;,|,$etc. dentro de aspas como "comando composto" — umforja-askcom "(transcreva o conteudo completo)" na mensagem era negado mesmo com o prefixoforja-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.mdtraziacoverage_pct: Noneem todo run, e a causa nao era falta de instrumentacao: o jobratchet(timeout 10 min, ~3 min de trabalho) sempre olha antes do jobcoveragedoci.yml(timeout 45 min, 21-30 min de execucao) terminar, e o filtroconclusion == "success"descartava o run ainda em andamento. A busca passa a varrer os ultimos 20 runs por umcompleted+successe, quando o SHA da PR ainda nao fechou, cai para o ultimo run verde demain, gravando a procedencia dolcov.infono step summary (comNOT this PRexplicito no fallback). Continua best-effort: nenhuma falha ali derruba o job. Novo baseline versionado emdocs/coverage-baseline-2026-10.mdfixa 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-gatewayliga as features de canal incondicionalmente e oserver.rsinstancia os seis no boot, semcfg— 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-q65xeGHSA-jqpg-j7w6-42pr(combustivel gasto por callees decall_refe 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) eGHSA-j2g9-4prp-pf6h(panico com datetime fora de faixa noset-times/set-times-atdo filesystem WASI). Sem migracao de API nogarraia-plugins/src/runtime.rsdesta vez. A arvore do cranelift 0.136.1 declararustc 1.96.0, entao orust-versiondo workspace e o jobMSRV checkacompanham — 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-0325eRUSTSEC-2026-0326(corrupcao da heap do GC: import de tag WebAssembly com tipo errado, e rooting ausente para valores do GC vivos atraves detry_call) eRUSTSEC-2026-0327(contagem de resultados do callback async-lifted de componente sem validacao, com estouro de buffer na pilha nativa) — e quatro nowasmtime-wasi:RUSTSEC-2026-0321(opoll_oneoffdo 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_readdircopiava padding de struct nao inicializado para a memoria do guest) eRUSTSEC-2026-0324(guest derrubava o host com timestamp de filesystem anterior a epoch no wasip3). OSecurity - cargo auditagendado ficou vermelho em 2026-10-03 com os sete. Patch de lockfile apenas: oCargo.tomlja pediawasmtime = "49", entao nao houve mudanca de manifesto nem migracao de API nogarraia-plugins/src/runtime.rs. E o primeiro bump dewasmtimeem seis que nao eleva orust-version— a arvore do 49.0.2 (cranelift 0.136.2, pulley/wiggle 49.0.2) segue declarandorustc 1.96.0, igual a do 49.0.1, entao o jobMSRV 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_pode a acaopresetda API admin gravamlevelewritede 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 noconfig.yml— nao ha rotulo guardado que possa divergir do que o portao aplica.chat_onlynao libera ferramenta nenhuma ereadso leitura, os dois seguros para contexto remoto;developerefull_podcompilam para o mesmofull, que por construcao nao tem teto proprio — quem decide e o modo mais oexecution.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 campopresetnas acoesdefault,group-defaultegroupda API —, com as guardas de sempre:developerefull_podsaofulle continuam recusados emaccess.default. O documento de politica efetiva em JSON (garra whatsapp access --jsoneGET /admin/api/whatsapp/access) passa a trazer um rotulopresetcalculado ao vivo dolevel/write, comcustompara o que nenhum preset cobre.
Changed
utoipa6.0 eutoipa-swagger-ui10.0 sobem juntos (#1526, #1529). As duas crates trocam o tipoutoipa::openapi::OpenApientre si, entao bumpar so uma quebra a compilacao: autoipa-swagger-ui9.0.2 declaraSwaggerUi::url(self, url, openapi: OpenApi)contra a serie 5.x, e com autoipaem 6.0.0 o gateway parava numE0308: mismatched types— dois facades incompativeis no mesmo grafo. Autoipa-swagger-ui10.0.0 foi re-lancada justamente para casar com autoipa6.0, entao o par so anda em conjunto. Nenhuma mudanca de codigo foi necessaria: as breaking changes dautoipa6 (fim do ignore por expressao emToSchema/IntoParams,utoipa-gensobresyn3) nao tocam nada que o gateway use. O build offline do Swagger UI segue pela featurevendored, agora viautoipa-swagger-ui-vendored0.2. Para nao repetir o bump partido, o.github/dependabot.ymlganhou um grupoutoipa, no mesmo molde dos gruposwasmtimeeopentelemetry.- Familia opentelemetry sobe para 0.33 numa unica mudanca (#1527, #1528, #1530, #1531).
opentelemetry,opentelemetry_sdkeopentelemetry-otlpvao de 0.32 para 0.33 etracing-opentelemetryde 0.33 para 0.34 nogarraia-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 grupoopentelemetrynodependabot.ymlpassa a casa-las por pattern em qualquer update-type, como ja era feito para o parwasmtime/wasmtime-wasidesde 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 emexecution budget exceededdepois 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 oworking_dirdas file tools e dorepo_search, e o vinculo fica nosessions.db(sessions.project_id): umgarraia restartrestaura 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/projectseGET /api/projectspassam a gravar e ler o banco quando hasessions.db(antes viviam so em memoria). No WhatsApp pessoal, uma mensagem que comeca com/e decidida por principal ANTES de ir ao modelo:/helppara todo admitido,/projectpara dono e usuario,/modee/goalso 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 dobashnuma visao por capacidade com estado —visible,denied,unavailable,unhealthy,not_configured— e motivo legivel por maquina e por humano (mais remediacao).garra_statuspassa a devolvercapabilities(e a nota do prompt manda o modelo responder a partir dela:deniede "existe e nao esta liberada", nunca "nao existe";unavailablerepete a remediacao); o/api/diagnosticsganhatools.capabilities(contagens por estado e o que esta indisponivel/fora do ar/nao configurado);GET /admin/api/capabilitiesexpoe o mesmo registro ao console.device_read/device_executeficam indisponiveis (no_devices) sem dispositivo registrado;device_listcontinua, 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.ToolPolicyganhaallowed_capabilities,denied_capabilitieseno_tools; oExecContextganha um teto (TetoDeCapacidades) composto por E com o modo em todo ponto do runtime — a lista que o modelo ve, o despacho e ogarra_status. Os niveischat|read|fulle owrite on|offdo WhatsApp compilam para esse teto (politica_do_nivel):writemexe 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 emopen, nuncafull),users(por numero ou JID:level: chat|read|full,write: on|off,role: owner,blocked: true) egroups(enabled,defaulte politica por JID de grupo). O nivel e owritecompilam para um teto de capacidades composto por E com o modo da sessao: o teto so tira, nunca poe, ewritemexe so em escrita de arquivo (nativa e MCP) — shell, dispositivo, mensagem e agenda sao controles independentes. Grupo nunca herda o papel do dono; bloqueado venceopen,allowe pareamento; nivel desconhecido echat; a secao inteira e relida por turno (um bloqueio vale na mensagem seguinte) e cada normalizacao deixa um aviso sem numero. Compatibilidade:allow,ownersereply_in_groupscontinuam valendo e, semaccess:, nada muda — nivel ewriteso existem onde foram declarados; a excecao e quem entrou por codigo do/pair, que passa a ter tetoread(credencial fraca). O log do turno ganhaprincipalealcance(etiquetas fixas, nunca identidade). garra whatsapp userslista quem pode falar com o GarraIA (#1393). Ostatusdizia so QUANTOS estavam autorizados, e quem tinha liberado tres numeros meses atras precisava abrir oconfig.ymla mao para saber quais eram. O comando novo mostra o papel (allowouowners) 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 oconfig.yml. Oremovee o espelho doallow: tira a identidade deallowe deownerspela 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/-yfora do terminal, pergunta com default NAO dentro dele —, e quem nao estava na lista sai 0, porque revogar e idempotente.garra whatsapp owner/unowneradministram o papel de dono (#1395). Ate aqui virar dono so acontecia junto com oallow --owner(ou dentro dolink), e deixar de ser dono so existia editando oconfig.ymla mao — ou passando oremove, que tira o acesso junto. Oowner <numero>grava emownerspela mesma escrita doallow --owner, com as mesmas duas portas:execution.profile = isolated-podobrigatorio (64 fora dele) e confirmacao explicita,--yes/-yfora do terminal. Ounowner <numero>tira o papel sem nunca tirar o acesso: quando a identidade so existia emowners, a entrada passa paraallowna 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 emstandardque 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 nowhatsapp userse nostatusna 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).accessimprime a politica efetiva inteira (admissao, default do desconhecido, grupos, e cada principal com piso, nivel e o que pode de fato — calculado pelo MESMOToolGatedo turno, nunca por um parser paralelo), com--jsone, so localmente,--reveal.access open|restrictedtroca a admissao (openavisa e pede confirmacao ou--yes);access default chat|read [--write]define o que um desconhecido recebe emopen(fulle recusado);level <numero> chat|read|fullewrite <numero> on|offgravam nivel e escrita por identidade (writemexe so em escrita de arquivo, nativa e MCP);block/unblock;access groups on|off|default <nivel>eaccess group <jid> <nivel>;access resetvolta 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_bytesna secao): quando, quem, por onde, acao, alvo...1234e 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/accessdevolve a politica efetiva — o MESMO documento degarraia whatsapp access --json, produzido porwhatsapp_linked_politica::visao— maishot_reload(se o gateway rele a config a quente);POST /admin/api/whatsapp/accessaplica uma mutacao (action: open | restricted | default | level | write | block | unblock | groups | group-default | group | reset, comidentity/jid/level/write/enablededry_run) pelo mesmomutacao::aplicarda CLI — validacao antes de gravar (400 com a mensagem acionavel), escrita atomica0600, impacto por principal (o que ganha e perde) e audit comorigem: admin_apie o username do admin;GET /admin/api/whatsapp/access/auditle a trilha. Leitura comChannels/Read(viewer le), mutacao comChannels/Update(viewer recebe 403); cookie e CSRF dos layers do/admin. A API nunca revela identidade (so...1234). O checkwhatsapp.accessdo/api/diagnosticspassa a avisaraccess.admission: open(com o passo para fechar) e secaoaccesscom 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 porPOST /admin/api/whatsapp/access. Resumo (canal, perfil de execucao, piso do dono, hot reload, contagens, avisos), admissaorestricted/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 seletorchat|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 mandandoidentity_last4, que o gateway resolve entre as identidades declaradas (ambiguo = 409); a identidade inteira nunca desce ao navegador.data-testidestaveis + spec Playwrighttests/playwright/whatsapp-access.spec.ts. Motor:Mutacao::Papel(owner/unowner, preservando o acesso ao rebaixar o dono legado) eMutacao::Remover(tira deallow,ownerseaccess.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/sessionstrazprincipal,level,write,chosen_mode,effective_mode,project_nameehas_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, eisolated-pode 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, emisolated-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_searchvarias 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_searchsem 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_dirdiferente num turno novo limpa a sessao.garra_statusdevolvebreaker(tool,reason_code,reason) ebreaker_means; a nota do prompt manda o modelo respeitar a lista. O agregado no/api/diagnosticsfica para a #1438. garraia doctor whatsapp(#1419). O caminho do WhatsApp pessoal de ponta a ponta numa passada: vinculo (LinkHealth, a mesma tabela dostatuse do console), chave da sessao (origem e prova de que abre), gateway e ponte (com o gateway de pe, o que o/api/diagnosticsdiz; sem ele, a linha diz que nao sabe), acesso (contagens de autorizados e donos), perfil de execucao (isolated-pode sempre aviso, dizendo o que o dono ganha), raizes das file tools (o mesmo resolvedor do boot), MCP visivel no pisosearch(servidor inteiro, so operacoes de leitura, ou escondido — com a sintaxe para liberar) e provider padrao. Cada linha traz o proximo passo;--jsonfala 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 emgarraia_gateway::bootstrap::whatsapp_linked_doctor; a CLI virou consumidora e a saida dela (--json, tabela humana, exit 0/2/69) nao mudou. O gateway ganhouGET /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/diagnosticspor chamada de funcao, sem HTTP — e devolve{ status, version, lang, checks }comchecksidentico aoreport.checksdo--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/accesstrazrejectionsePOST /admin/api/whatsapp/access/rejections/resetzera; 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.shautomatiza 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.debx86_64 da CLI (artefatogarraia-linux-packagesde um run dorelease.ymlviagh run download, ou build local empacotado com o mesmonfpmpinado e o mesmopackaging/nfpm.yamlda release), sobe umubuntu:24.04cru com--network host, instala comapt-get install ./garraia.debe prova o fluxo inteiro:garraia --version(e o aliasgarra),doctor --json(exit 0, JSON valido),doctor whatsapp --json(exit 69 sem WhatsApp, linhawhatsapp.linkedpresente),config set-modelapontando para o Ollama do host sem chave paga,start -dna 3899 ate/api/healthhealthy, sessao REST com resposta real do modelo,restart -dcom sessao e config sobrevivendo (historico readotado dosessions.db, provider/modelo no health do processo novo) estopcom 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,.debtestado,resumo.txteresumo.jsonPASSOU/FALHOU por passo) vai paradogfood/linux/<data>/, gitignored.docs/releasing.md§1.5 registra o comando na matriz.- O wizard
garra whatsapp linkoferece a politica de acesso depois do QR (#1429; ADR 0025). Depois de perguntar o primeiro numero (e o dono, emisolated-pod), o wizard passa a perguntar, com defaults seguros: o nivel de quem acabou de entrar (chat|read|full, defaultread) e, fora dechat, a escrita de arquivo (default nao); e a admissao — so quem voce autorizar (restricted, default) ou qualquer numero (open). Escolheropenmostra o default que um desconhecido recebe NESTA config e pede a mesma confirmacao doaccess open, com default nao. Cada resposta e gravada pelo mesmo motor doaccess(whatsapp_linked_politica::mutacao: validacao, escrita atomica0600, audit com origemcli) — o wizard nao tem uma segunda forma de escrever politica, e Enter em tudo deixa o numero comreadsem 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 oallowgravaria. O resumo final passa a ser o da politica efetiva (as linhas doaccess), no lugar do resumo dousers. - O
garra initoferece 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 umgarra whatsapp linkseparado. 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 oconfig.ymlser escrito, porque o vinculo grava na mesma secaochannels.whatsapp_linked; um vinculo que nao complete (sem Node, QR nao lido, desistencia) nao derruba oinit, 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::plandiz o que o resolve faria no diretorio da conta (passphrase do cofre,session.keyexistente 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_modesaperta o diretorio da conta para 0700 e todo arquivo regular dentro dele para 0600 (incluido orecusas-lid.jsonque 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 asession.keyja 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::retentionpassa a ser o dono das faixas dememory.retentioneruns.retention_days: oconfig checkdelega para ele e o futuroPATCH /admin/api/retentionvai 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::retentionconta o que uma limpeza apagaria sem apagar (mesma clausula doDELETE, cobrada por teste), mede o tamanho dos dois bancos e grava a ultima limpeza no proprio banco:compacteprune_agent_runsanotam 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,Instantinjetado e lock sem panico. Nada sai da maquina: so nome de servidor e booleanos — nuncalast_error, comando ou caminho. O endpoint autenticado que expoe o snapshot vem em fatia propria. scripts/setup-toolchain.shfixa 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 eracargo check/cargo testfalhando com "is not supported" ate rodarrustup updatemanualmente. Umrust-toolchain.tomlcommitado 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 quedtolnay/rust-toolchainacabou de instalar com esses targets. O script evita isso: usarustup 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 (secaomcp:) 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 ormcpque ja estava no workspace, e expoe cinco tools:garra_status,garra_list_chats,garra_read_history(com segredos redigidos pela mesmaredact_secretsdo log),garra_pair_statusegarra_send_message. Transporte stateless com resposta em JSON, entao umcurlconfere o endpoint sem parsear SSE. Tudo desligado por default:gateway.mcp_http.enabledliga a rota, e ela recusa subir semgateway.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_sendmais o destino emproactive_chat_ids), e uma tool que nao pode enviar nem e anunciada. Ogarra config checkavisa nos dois erros silenciosos: ponte ligada sem credencial e envio ligado sem allowlist.docs/gateway-mcp-http.md.
Changed
- O modo
searchenxerga a leitura do MCPfilesystem(#1384). O piso de todo remetente do WhatsApp pessoal escondia TODA ferramenta do servidorfilesystem— inclusiveread_text_fileelist_directory— porque a whitelist so listava as nativas, e o modelo dizia nao ter como ler arquivo com o servidor conectado. Aallowedganha uma terceira forma de entrada,*/<operacao>(a operacao exata, em qualquer servidor), esearchpassa 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, edeniedsegue vencendo tudo. Os outros modos somente-leitura nao mudam nesta fatia. docs/security/threat-model.mdganha 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(eusers) passam a dizer a admissao (restritaouABERTA, com o default que um desconhecido recebe) e o--jsonganhaadmissionedefault_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) validaidentitycom 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 comerroreerror_codeestavel. 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 colunasweb(web_fetch),memory_readememory_write(pela classememory.*; hoje nenhuma ferramenta nativa a carrega, e por isso a coluna e falsa no pisosearch, que nao a nomeia), ao lado de arquivos, MCP, shell, dispositivos e mensagem — o minimo que a #1411 pede (#1411). garraia whatsapp allow,remove,ownereunownerpassam a auditar (#1414; ADR 0025). Os comandos legados gravavamallow/ownersnoconfig.ymlsem deixar rastro, e a trilha deaccess auditso 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 --ownereallow, e e o resumodepoisque diz que o alvo entrou como dono), origemcli, 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 dolink, que e a mesma escrita doallow, audita do mesmo jeito. Mudanca gravada com o audit indisponivel sai 73, como nos comandos novos; nolinko exit continua 0 (o vinculo valeu) e o aviso sai em stderr.garra_statusdiz se as file tools tem raiz nesta sessao (#1416, #1418). O relatorio ganha o blocofile_tools—ready(bool),source(session_workspace|session_working_dir|declared|none),roots(contagem, so quando declaradas) emeans(so quandoready = 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, ondesession.working_dire 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_writeelist_dirnuma sessao sem raiz nenhuma saem comounavailablecomreason_code: no_roots, erepo_searchsem repositorio comono_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_sende 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.NoRootsdas file tools ganha mensagem propria e acionavel — "nenhuma raiz esta configurada para esta sessao ... selecione um projeto com/project <nome>ou configureagent.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 linktermina 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 comallowherdado) saia sem ver o portao. Agora, antes da linha final, ele imprime o MESMO resumo dogarra 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,updateerollback, 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 foimessage.cache_roomnamesdochat.db(chat<digitos>, o identificador que o servidor gera e que igualachat.chat_identifier), nunca ochat.display_nameque os participantes renomeiam — mas chamava-segroup_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.bashcomagent.sandbox.mode = off— o default documentado do perfilstandard— passa dewarninganot_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) continuawarning.runtime.channelssem canal de mensageria passa dewarninganot_configured: chat web, CLI e API nao entram no registry de canais e o WhatsApp vinculado tem a linhawhatsapp.linked; o passo antigo mandava procurar erro de registro de um canalwebque nunca existiu ali, e o novo diz como declarar um canal. - Quality Ratchet:
runtime.rsvolta para baixo da baseline (#1254, plan 0064). Omod testsdecrates/garraia-agents/src/runtime.rs(6 763 das 10 881 linhas, o maior.rsdo repositorio) viraruntime/tests/, um arquivo por tema com ate ~530 linhas — nao umruntime_tests.rsunico, porque o ratchet conta todo.rsrastreado e tambem quantos passam de 700/1500/2500 linhas. Os submodulos aninhados viram arquivos irmaos sem mudar de caminho; helpers ficampub(super)no tema que os criou. As tres guardas que varrem o fonte leem agora umruntime.rsque e so producao. O mesmo paratools/repo_search_tool.rs(840 → 433, testes emrepo_search_tool/tests.rs) e para o smoketests/whatsapp_smoke.rsda CLI (822 → 418 +whatsapp_smoke_acesso.rs), os dois que tinham cruzado as 700 linhas desde a baseline.max_file_lines10 881 → 6 901 efiles_over_700114 → 112;files_over_1500/_2500seguem um acima da baseline porgarraia-cli/src/whatsapp/acesso.rs(1 650) egarraia-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. Comagent.file_rootsvazio — 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_writeelist_dirapareciam 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_rootmuda so a raiz do servidor MCPfilesysteme 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 emagent.file_rootsou emGARRAIA_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/diagnosticspassou a trazer a linhafiles.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 nasce0700(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/diagnosticsrelativiza o caminho tambem quando odata_dirpassa 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 aRunTestsTool: a combinacaofile_write+run_testsque 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>/workspacecomo 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 comoworking_dirda 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 em0700, 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 emagent.file_roots/GARRAIA_FILE_ROOTScontinua vencendo sozinha, sem escopo por sessao: um diretorio compartilhado ali e escolha explicita do operador. A linhafiles.workspacedo/api/diagnosticspassa a mostrar<data_dir>/workspace/<sessao>, sem nunca publicar o identificador da sessao.O
working_dirsintetizado alcanca toda invocacao de ferramenta, nao so as quatro file tools:bash(sandboxed),git_diff,code_reviewerepo_searchtambem passam a operar dentro do diretorio da sessao. Nobashsandboxed isso troca a recusa fail-closed de antes (mount vazio semworking_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_searchrecusa na hora quando nao ha repositorio ativo (#1380). Semworking_dira 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$HOMEou o diretorio de dados. Agora a tool usa oRepoDir::decidirpara separar os dois sentidos de "sem working_dir": o diretorio herdado que E (ou esta dentro de) um repositorio segue sendo buscado, como nogarra chatlocal; sem.git/.hg/.svn/.jjnele ou acima dele, a resposta sai em milissegundos, dizendo onde a tool olhou e que basta selecionar um projeto. Sessao que escolheu umworking_dirnao muda de comportamento. Num turno restrito (o pisosearchdos canais remotos, o mesmo em que ogarra_statusretemsession.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 ogarra_status. Mudanca de comportamento: uma arvore de codigo SEM metadado de VCS no diretorio do processo (por exemplo umCOPYde container ou um tarball exportado) era buscavel e agora recebe a recusa — selecione o projeto (defina oworking_dirda sessao) para buscar nela. -
garra_statusexplica o quewithheldsignifica (#1382, #1387). O relatorio ja retinha o que so interessa ao operador num turno restrito e listava os campos retidos emwithheld, mas nada dizia ao modelo como ler isso: um campo retido sainull, e commcp_servers: nullo modelo respondia que este Garra nao tem MCP. Adescription()da tool e o proprio relatorio (campowithheld_means, presente so quando ha algo retido) agora dizem que campo citado emwithheldesta OCULTO POR POLITICA desta conversa, e nunca deve ser lido como capacidade ausente, desligada ou nao suportada. O texto tambem nao afirma o contrario:withhelde lista fixa, entao onullsignifica 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, noruntime.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 stopnao 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 esleep, umdocker execsem--init), o gateway encerrava limpo no SIGTERM — ogarraia.logtermina em "gateway shut down gracefully" — mas virava zumbi, ekill(pid, 0)continuava devolvendo 0. Ostopentao esperava os 5 s de graca, mandava SIGKILL e anunciava "survived SIGTERM and SIGKILL" de um processo ja morto. No Linux,is_process_runningagora le o estado em/proc/<pid>/state umZconta como parado; fora do Linux o comportamento e o de sempre. Teste de unidade forka um filho, espera ele virar zumbi de fato e exigefalse. -
Diagnostics separa "nunca configurado" de "quebrado" (#1437). O
GET /api/diagnosticstinha 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.envinexistente e por umGARRAIA_JWT_SECRETque o proprio texto da linha chamava de opcional. Entram duas variantes aditivas —disabled(ha um interruptor e ele esta desligado: modo voz) enot_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 deok. 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 warningslimpo no stable 1.95 (#1447). Duas expressoes booleanas pre-existentes disparavamclippy::nonminimal_boola partir do rustc 1.95: o predicado deretainemwhatsapp_linked.rs(De Morgan) e uma asserção de teste emsafety_gate.rs(!x.is_ok()→x.is_err()). Sem mudanca de comportamento; nenhum#[allow(...)]novo. -
Hook
pre-tool-usedeixa de bloquear toda remocao por caminho (#1453). Os padroesrm -rf /,rm -rf ~erm -rf ./eram casados como substring literal, entaorm -rf /tmp/scratchpad/xerm -rf ./pintest— limpezas legitimas de uma rodada autonoma — caiam como "comando perigoso". Ormpassa 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 desudo/xargs/&&. Um caminho DENTRO desses diretorios nao casa. Os demais padroes literais (fork bomb,DROP TABLE, force push emmain,dd if=,mkfs.) continuam iguais, escripts/test-hooks.shganha 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 odl.min.ioresponde 410 — o check obrigatorioClippy Lintingfalhava nodocker pullem toda PR, e o retry do #1457 nao tinha como ajudar. Agorascripts/ci/build-minio-image.shcompila ominioa partir da tag upstream fixada (RELEASE.2025-02-28T09-55-16Z, commit conferido antes do build, tag movida e recusada), embala emdebian:bookworm-slime etiqueta a imagem com onome:tagque 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. Odocker-compose.minio.ymlde dev usa o mesmo script e deixa de depender da imagem domc, tambem fechada: o bucket passa a ser criado com oaws-cli. -
As raizes das file tools sao resolvidas uma vez, no boot (#1459). O
/api/diagnostics(rota auth-free) e ogarra_statuschamavamraizes_das_file_toolsa cada request/chamada: umcanonicalizepor raiz declarada e umwarn!por raiz que nao resolve, amplificaveis por quem quisesse. Agora oAppStateguarda 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
0700no propriomkdir(#1463). Antes o diretorio era criado com a umask do processo (tipicamente0755) e so depois fechado porset_permissions, com uma janela em que qualquer usuario local o lia; e ocreate_dir_allseguia 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 omkdirfalhar em vez de ser seguido. A sequencia symlink → nao-diretorio → mkdir passa a existir uma unica vez, emSessionWorkspace::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 odata_dirantes, porque numa instalacao limpa ele ainda nao existe nesse ponto. A guarda de fonte que exige um unico ponto de montagem deToolContextemruntime.rspassa a casar o construtor em qualquer forma (numa linha so, com caminho de modulo, com..base), e nao so a linha exataToolContext {(#1464). -
WhatsApp pessoal:
access.groups.enabled: falseescrito a mao na config passa a VENCER oreply_in_groups: truelegado (antes o legado ganhava em silencio e o canal seguia respondendo em grupo, sem teto); a precedencia gera aviso no boot/config checke esta na ADR 0025 (#1501). -
garra mcp-servervolta a entregar as tools ao Claude Code (#1518). Quem negocia a spec MCP2026-07-28(o Claude Code, viaserver/discover) valida otools/listcontra o schema novo, que tornattlMsecacheScopeobrigatorios (SEP-2549). O rmcp 3.x (#1162) modela os dois comoOptione oListToolsResult::with_all_itemsos deixa fora do fio, entao o cliente recusava a lista inteira ("Invalid result for tools/list") e o/mcpmostrava o servidor conectado, mas sem nenhuma tool. O handler agora mandattlMs: 0ecacheScope: "private"quando a versao negociada e2026-07-28ou mais nova; peer legado segue recebendo otools/listde antes, byte a byte. -
codeql-rekey-ledger.pycasa 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 numprintln!multi-linha e a ultima do span; o rekey comparavastart_lineexato 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 docodeql-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 chamavaMcpManager::call_tooldireto — semToolGate, sem o modo da sessao, semToolApprovale semHardwareGate. O unico controle era oRole::Userdo comando, que todo mundo tem: uma sessao criada em modosearch(read-only) executava ferramenta de mutacao porPOST /api/sessions/{id}/messages, eGET /api/slash-commandslistava os nomes de graca. O caminho legitimo nao muda — as ferramentas MCP continuam chegando ao modelo peloAgentRuntime, despachadas atras doToolGate. - Leitura de sessao por id separada em cliente e operador (#1462, opcao 3).
GET /api/sessions/{id}/historydevolvia 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 oresumesem 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 mesmo404 session not foundde id inexistente, byte a byte, e nao e hidratada, desconectada nem listada — em memoria e nosessions.db. A leitura do operador ganhaGET /admin/api/sessions/{id}/history(cookie do/admin+manage_sessions;viewerrecebe 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 dosubdo JWT — cada usuario le so a propria sessao. - Um
session_idescolhido pelo cliente deixa de alcancar a sessao de outra superficie (#1462).POST /v1/chat/completionsaceitavaX-Session-Idverbatim ePOST /api/sessions/{id}/messagesaceitava 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 responde404 session not foundsem 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/diagnosticsnao publica mais caminho absoluto do host para raiz do MCPfilesystemfora dodata_dir(#1465). As linhasexecution.profile(emisolated-pod) emcp.filesystem_rootcaiam no fallback que imprimia o caminho como esta — no caso legado mais comum, o$HOMEdo host com o nome de usuario — numa rota auth-free. Agora uma raiz fora dodata_dirsai 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 quefiles.workspaceja aplicava a raiz declarada. O caminho continua nomcp.json/config.ymle no log de boot.- Chamadas MCP de filesystem confinadas ao jail da sessao (#1482, nucleo da
#1383). Em
standarda raiz do servidorfilesystemautoprovisionado e<data_dir>/workspace, o pai de todo diretorio de sessao (#1449); com a leitura MCP liberada no pisosearch(#1384), um contato admitido listava o pai e lia o que o agente escreveu para outra pessoa. AgoraMcpToolpassapath/paths/source/destinationdas operacoes de filesystem pelo MESMOFileJaildas file tools nativas, com oworking_dirda sessao; caminho relativo resolve contra o diretorio da sessao; fora do jail a recusa e a frase unica das tools nativas;list_allowed_directoriesresponde as raizes efetivas da sessao sem chamar o servidor. O gateway entrega o jail aoMcpManagerno 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 dodata_dirda 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_longosagora cobre telefone com separador comum (#1514). A funcao que oRedactingWriterusa 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 umtest-engineerdurante 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 oCLAUDE.mdmanda usar em prosa narrativa, ouDD-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 porcode-revieweresecurity-auditorindependentes: 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 duasMessagePart::Textdiferentes — 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.