Novidades da v0.4.3
🇬🇧 English version · 📋 CHANGELOG completo · 📦 Baixar
A release em que o Garra entrou no WhatsApp pessoal. Até aqui o canal
WhatsApp exigia conta Business, número cadastrado na Meta e URL pública —
barreira que o usuário doméstico não passa. Agora garra whatsapp mostra um
QR code no terminal, você lê com o celular, e o número que já está no seu bolso
passa a falar com o agente, com a sessão cifrada em disco e o gateway
supervisionando a ponte.
Junto veio a maior faixa de segurança de uma release até hoje: cinco fail-opens
do MCP fechados, um jail de diretório nas file tools, o guard de injeção
indireta estendido a MCP, arquivos e dispositivos, e o sandbox por tool
(agent.sandbox) que existia inteiro e nenhuma instalação conseguia ligar.
E, no runtime, um LLM padrão único (z-ai/glm-5.3-flash via OpenRouter), o
auto-router de modo em todas as portas de entrada, fallback local quando a rede
cai, e um garra chat que sobrevive a crash, timeout e Ctrl+C.
WhatsApp pessoal por dispositivo vinculado​
Antes: só a Meta Cloud API. Agora há dois caminhos, e eles não se misturam.
garra whatsapp
1) Conectar meu WhatsApp pessoal (ler um QR code) → garra whatsapp link
2) WhatsApp Business pela Cloud API oficial da Meta → garra whatsapp cloud
| Comando | O que faz | Exit code |
|---|---|---|
garra whatsapp | menu de duas opções | 0, ou 1 se cancelado |
garra whatsapp link | vincula por QR | 0 · 1 cancelado · 69 sem Node, sem terminal ou QR não lido · 70 erro interno |
garra whatsapp cloud | wizard da Cloud API | 0 · 1 cancelado · 69 sem terminal · 70 erro interno |
garra whatsapp status | diz se há vÃnculo e se a sessão abre | 0 vinculado · 69 não vinculado ou ilegÃvel |
garra whatsapp logout | apaga a sessão e desliga o canal | 0 · 1 cancelado |
garra whatsapp restore | devolve o session.enc.prev ao lugar | 0 · 69 nada arquivado ou sessão em uso · 70 erro interno |
Sem terminal (ssh servidor 'garra whatsapp'), o menu imprime as duas opções
com o comando de cada uma e sai 0, como o garra init; quem já escolheu
link recebe o motivo, a saÃda (ssh -t …) e exit 69 em vez de um falso
sucesso (#1238).
Como funciona por dentro​
garra whatsapp link / gateway (serve)
| NDJSON v1 por stdio · ping/pong · env_clear + allowlist · PDEATHSIG · Drop mata o filho
bridge/whatsapp/bridge.mjs Node + Baileys 7.0.0-rc14, STATELESS — nunca escreve no disco
|
WhatsApp Web (dispositivo vinculado)
- A ponte é burra de propósito. O Node cuida só do protocolo do WhatsApp;
o Rust desenha o QR, cifra e grava a sessão, aplica allowlist e roteia para o
agente. O estado de autenticação vive em memória na ponte e volta como
snapshot completo (
session_update) para o Rust gravar (#1238, ADR 0023). - Sessão cifrada em repouso. AES-256-GCM (a mesma pilha do
CredentialVault) em<data_dir>/whatsapp/default/session.enc, arquivos0600num diretório0700, escrita atômica. A chave deriva deGARRAIA_VAULT_PASSPHRASEquando ela existe; sem ela fica numsession.keylocal, e ostatusavisa disso em letras claras. - Nada destrutivo antes da última confirmação. Há uma tela de
consentimento antes do primeiro QR (cliente não oficial, a conta pode ser
bloqueada, use um número secundário). Um re-vÃnculo que não chega a gravar
sessão nova — QR expirado, Ctrl+C, ponte que morre, WhatsApp recusando —
restaura a sessão anterior;
enabled = truesó vai para a config depois de a sessão existir em disco, então um pareamento abortado não deixa o gateway pagando timeout a cada boot. - Todo relógio tem teto. Handshake, QR, "conectando", autenticado-mas-
nunca-conecta,
shutdownde cortesia (500 ms),npm ci(600 s, sem órfão): cada fase do pareamento diz quanto tempo falta e desiste com a mensagem certa. Um teste enumera todas as fases e não compila se alguém acrescentar uma sem dizer qual é o teto dela.
O canal whatsapp_linked no gateway​
A mensagem lida pelo QR chega ao agente por um canal pull próprio, e o modelo de ameaça dele é outro: a mensagem vem de qualquer pessoa que conheça o número pessoal do operador. Três decisões saem disso, nenhuma configurável para menos (#1238):
- Allowlist própria, fail-closed. Entra-se com código do
/pairou pela listaallowda config; portão vazio significa ninguém. A allowlist global não serve aqui porque todo canal irmão a preenche sozinho por auto-claim do primeiro remetente. - Ferramentas somente-leitura por padrão. Sessão sem modo escolhido
resolve para o perfil
searchaté o operador subir com/mode. - Guard de injeção indireta no texto recebido.
Grupo só responde com opt-in explÃcito, e mensagem própria (from_me) nunca
gera turno. O gateway supervisiona a ponte em mode: "serve", com prova de
vida no protocolo: depois de conectar, ping a cada 15 s e resposta exigida em
45 s, senão cai no backoff de reconexão (#1283). Ctrl+C no gateway cancela
o supervisor — a ponte Node não fica viva depois do "shut down gracefully".
garra whatsapp status, GET /api/channels e o check whatsapp.linked do
GET /api/diagnostics classificam pela mesma função, com o próximo passo
acionável ("rode garra whatsapp link", "rode npm ci em
O que sai no seu terminal​
A cauda do stderr do Node é a única saÃda crua de ferramenta externa no fluxo,
e ela passa por redação antes de aparecer: sequências longas que parecem
material cifrado (base64 padrão e url-safe, inclusive coladas a nomes de campo
como creds.noiseKey= ou npm ERR! _auth=) viram <redigido: N caracteres>,
medido sobre 2000 chaves com vazamento residual de ~1,3 caractere (#1238,
#1276). O nome da conta recusa con, prn, aux, nul, com1–com9,
lpt1–lpt9, que não podem virar diretório no Windows.
- A redação do stderr também cobre base64 percent-encoded e com a barra
escapada como
\/, formas que umthrowde biblioteca ou um JSON serializado produzem (#1276).
O que precisa de Node: só o caminho do QR. node e npm são procurados
na PATH e a exigência é Node.js 20 ou mais novo (engines da ponte:
>=20). A Cloud API não precisa de Node. A ponte vive em bridge/whatsapp/
— diretório, não crate — e os testes Rust rodam contra uma ponte falsa em
Python, sem Node e sem telefone; o pareamento com aparelho real é validação
manual, descrita passo a passo na doc.
📖 docs/whatsapp.md · ADR 0023
Segurança​
Sandbox por tool: agent.sandbox​
A contenção da #1222 existia inteira — SandboxPolicy, backends, fail-closed
quando o backend falta, onze testes — e nenhuma instalação conseguia
ligá-la: os três construtores do BashTool fixavam off, e a chave de
config que as mensagens de erro citavam não existia no schema. Agora há a
seção, lida pela mesma função nos três pontos de produção (gateway, garra chat, garra mcp-agent), e o garra config check recusa sandbox que parece
ligado e não está (#1225).
| Chave | Valores | Nota |
|---|---|---|
mode | off (default) · all · allowlist | seção ausente = off, byte a byte o comportamento anterior |
backend | docker · podman · ssh | mode != off sem backend é erro |
image | imagem do container | default debian:bookworm-slim |
ssh_host | host remoto | obrigatório em ssh |
sandboxed_tools / elevated | listas de tools | elevated escapa e roda no host |
mount_workdir / network_disabled | bool, default true | ignorados por ssh |
O que cada backend garante — e não garante — está na tabela do threat model
§5.13. Três limites valem para todos: hoje só a tool bash é envolvida
(run_tests, git_diff, code_review e repo_search seguem no host mesmo
com mode: all); o backend ssh não é sandbox, é execução remota que
isola o host local e nada mais; e na prática isto é Unix — no Windows o
wrap_command falha fechado e o config check reporta erro.
Fecharam junto dois furos da primeira versão: metacaractere de shell escapava
para o host porque o comando era citado com {:?} em vez de quoting POSIX
(#1231), e um valor de config começando com - (ssh_host: "-oProxyCommand=…")
virava opção do ssh/docker em vez de argumento — recusado em três
camadas, sem nunca logar o valor (#1225). O comando sandboxado passa por
redact_secrets e truncagem antes de ir ao log, em debug!.
backend: sshcomnetwork_disabledoumount_workdirligados passa a ser recusado fail-closed: como o ssh ignora os dois, a config tem de dizerfalseexplicitamente para o operador reconhecer que não há contenção (#1225 S3).mode: allavisa na subida — uma vez por ponto de entrada: gateway,garra chategarra mcp-server— que cobre sóbash;run_tests,git_diff,code_reviewerepo_searchsão host-only, e oconfig checkaponta quando a config lista uma delas emsandboxed_tools/elevated(#1225 S2).- Teste de integração com Docker real prova o
--network noneno CI Linux (#1225 S4).
📖 docs/security/threat-model.md §5.13 · config.hardened.example.yml
MCP: cinco fail-opens fechados​
- Reiniciar um servidor pela admin API não apaga mais a allowlist de tools
(#1242). O restart reconectava com lista vazia — que significa "permite
tudo" — depois de o
disconnectter descartado a única cópia. Agora a allowlist é resolvida antes do teardown, contra o mesmo merge (mcp.json+config.yml) que o boot lê, e o restart nunca alarga o que está em vigor. DELETE /admin/api/mcp/{id}revoga de verdade (#1262): derruba a conexão viva, limpa opending(de onde o monitor de saúde ressuscitava o servidor deletado com os segredos recém-revogados) e solta as tools do runtime.- A whitelist do modo do agente passa a valer para tool MCP (#1264).
ToolGateisentava qualquer nome com__; modo somente-leitura deixava passar ferramenta de escrita de servidor MCP. A permissão agora é declarada:meu-servidor/*libera o servidor, nome completo libera só a tool,deniedvence tudo. - Entrada HTTP do
mcp.jsonnão é mais descartada pelo loader (#1274) — e com ela não se perde mais aallowed_toolsque o operador escreveu. - Uma escrita de admin não apaga mais
allowed_tools,inherit_enveenableddos outros servidores domcp.json(#1273): os dois tipos com o mesmo nome passaram a concordar num schema só.
E na fronteira do processo filho:
- Servidor MCP stdio deixa de herdar o ambiente inteiro do gateway
(#1075). Todo
npx -y algum-serverrecebiaGARRAIA_JWT_SECRET, as chaves de provider e o.envinteiro sem precisar de tool call nenhuma. Agora o ambiente nasce de uma allowlist mÃnima (PATH,HOME, locale,TMPDIR, CA bundles) mais o mapaenvdaquele servidor;inherit_env: trueé a válvula de escape por servidor, comwarn!e indisponÃvel pela admin API. vault:noenvde servidor MCP passa a ser resolvido no boot, e fail-closed (#1237): referência que não resolve impede o servidor de subir, nomeando servidor e chave, nunca o valor. Antes a string literalvault:…chegava ao filho como se fosse o segredo.- O resultado de tool MCP entra no contexto pelo guard de injeção indireta, com teto de 256 KiB e truncamento visÃvel (#1243).
POST /api/mcp/marketplace/installexige admin autenticado comPermission::ManagePluginse recusaenvque decide qual código o filho executa:PATH,LD_PRELOAD,NODE_OPTIONS,npm_config_*(#1245).
O gate de api_key e o bind exposto​
gateway.api_keypassa a cobrir o plano de conversa e o A2A (#1240):POST /v1/chat/completions,/v1/messages,/v1/messages/count_tokense/a2a/*estavam fora do gate, que testava só o prefixo/api/. O socket do papagaio (/ws/parrot) entrou junto, e o Garra Desktop passou a de fato mandar a credencial. Semgateway.api_keyconfigurada nada muda.garra initnuma VM como root não deixa mais o gateway na internet sem credencial (#1241): bind fora de loopback gera 32 bytes do CSPRNG emgateway.api_key, oconfig.ymlnasce0600, e a chave não é impressa no terminal. O boot avisa uma vez quando o endereço ligado alcança a rede e não há credencial — inclusive emgarra start -d, antes do fork.- Página web qualquer não dispara mais POST/PATCH/DELETE contra o gateway
local (#1182): guarda anti-CSRF genérica em toda a superfÃcie mutante,
Originchecado no handshake de/wse/ws/parrot, e CORS default fechado —gateway.allowed_originsvazio agora significa nenhuma origem cross-origin (ver "Atualizando"). /pairvolta a funcionar em todos os canais (#1189) — cada canal montava umPairingManagerpróprio, sempre vazio — e o código de pareamento ganha limite de tentativas (5 por usuário / 15 min, 20 globais queimam o código), comparação constant-time e aviso ao dono quando algo foi queimado (#1191).
As tools do agente​
- Jail de diretório nas file tools (#1244).
file_read,file_writeelist_diraceitavam~e caminho absoluto sem confinamento: um prompt por Telegram mandava ler~/.ssh/id_rsa. AgoraFileJailconfina à união deagent.file_roots(+GARRAIA_FILE_ROOTS) com oworking_dirda sessão; conjunto vazio nega tudo. Canonicaliza antes de comparar (.., symlink,~, absoluto) e recusa symlink pendurado; a recusa é uma frase só, sem caminho, para não virar oráculo de existência. Residuais declarados: TOCTOU e hardlink. - Guard de injeção indireta em
file_reade nas tools de hardware (#1243): conteúdo de arquivo e o que vem do barramento (id de dispositivo,statedo Home Assistant, payload MQTT/serial) chegam ao modelo emoldurados como dado não-confiável quando suspeitos; texto limpo segue byte a byte. - A query do modelo não cai mais em posição de flag:
repo_searchmontarg/grep/findstrcom a query depois do--—--pre=/bin/shfazia o próprio ripgrep executar um.txt(#1266);git_diffpõe ofile_pathdepois do--e recusa revisão começando com-—--ext-diffreabria execução externa via.git/configplantado (#1269). Os filhos derun_testsebashdeixam de herdar o stdin do gateway (#1270); inventário completo das 14 chamadas deCommandno threat model. POST /api/providersconecta com cliente HTTP pinado aos IPs validados e redirects desligados (#1248), fechando redirect laundering para169.254.169.254e DNS rebinding; os construtores default dos providers também deixam de seguir redirect.
Também: rustls 0.23.45 fecha RUSTSEC-2026-0285 (#1206); deny.toml perde um
ignore morto pós-poise 0.7 (#1202); ConfigLoader::save virou atômica
(tmp 0600 + rename + fsync) e um config.yml vazio deixa de virar
defaults em silêncio (#1238).
Agentes e runtime​
Um LLM padrão só​
z-ai/glm-5.3-flash via OpenRouter passa a ser o padrão oficial em todas as
superfÃcies (#1180, ADR 0022). A pergunta "qual LLM o Garra usa quando o
usuário não escolhe?" tinha cinco respostas conforme a porta: openrouter/auto
no chat e no wizard, openrouter/free no mcp-server, openai/gpt-4o no
gateway, lmstudio no desktop. Agora há uma constante única, travada por
teste. O local (Ollama) continua inteiro, como segunda opção em
agent.fallback_providers; o autodetect do garra chat tenta nuvem com
credencial antes do Ollama; e o wizard passa a destacar "Cloud-first" sem
pré-selecionar os ~18 GB de download local. Quem fixou
GARRAIA_MCP_MODEL_ALLOWLIST=openrouter/free precisa ajustar (ver
"Atualizando").
O turno que não morre​
-
Rede caÃda aciona o fallback local (#1249).
connection refused, DNS e timeout não casavam com nenhum padrão de retry e matavam o turno antes do laço de fallback. AgoraError::Transporté tipado: gasta uma tentativa e cai direto no provider seguinte, sem queimar ~3,5 s de backoff. -
Stream que quebra no meio refaz o turno pelo caminho batch, quando nada foi ao sink ainda (#1176).
-
404
No allowed providers are availabledo OpenRouter vira erro de configuração de roteamento com modelo, restrição efetiva e caminho de recuperação, e nunca retry (#1299). -
Parâmetro ausente numa tool vira observação soft, com o schema e pedido de reenvio — o modelo se autocorrige no mesmo turno em vez de o turno falhar com
agent error:(#1296). -
O auto-router de modo vale em todos os pontos de entrada (#1223):
garra chat,POST /api/chat, webchat, app e canais. Só age quandoagent.auto_router_llm_enabledestá ligada (default off), a sessão não tem modo e há texto; modo deduzido não concede tool nenhuma. -
O erro do loop detector diz o que repetiu: a tool, a contagem dentro da janela e o input repetido, truncado (#1295).
garra chat​
-
--resumesem valor (oulatest) retoma a sessão mais recente, e a pergunta é gravada antes do turno rodar, com marcador[turno interrompido: timeout|cancelado|erro]quando ele acaba sem resposta — crash, timeout e Ctrl+C não apagam mais o turno. Novo/resume [id]no REPL (#1300). -
/modelvirou transacional (#1298): valida contra o catálogo real do provider (OpenRouter:GET /modelscompleto) antes de trocar; modelo ausente ou provider sem resposta mantém o estado anterior. -
O REPL não exibe mais tracing cru — retry, fallback e circuit breaker ficam no
garraia.log;--verbose,--debugeRUST_LOGvencem (#1301). -
O contexto de projeto para de gastar prompt com
target/e passa a dizer qual projeto é (doREADME.md) e em que ramo (#1219). -
memory.auto_extractememory.max_factsfinalmente existem: desligue só a chamada de extração de fatos por turno sem desligar a memória semântica (#1221). -
Editor de linha com rustyline: setas e edição na linha; histórico em disco só com
--persist/--resume(0600em~/.garraia/history; sem as flags fica em memória), Ctrl+D encerra; pipe e CI continuam noread_line(#1297).
Runtime: ledger de runs e despacho único​
-
Runs de sub-agente deixam rastro na tabela
agent_runs(#1224), e o ledger tem o primeiro escritor de produção (#1227): a subida do gateway e do CLI converte runsrunningdeixados por uma queda eminterrupted(ids no log, nunca ogoal), e cada execução do scheduler grava uma linha com desfecho terminal. Falha do ledger é fail-soft. -
As quatro cópias do loop de turno despacham tools por uma função só (
dispatch_tool_call): orçamento, detecção de loop, gate do modo, timeout e pausa de confirmação vivem num lugar (#1226). -
git_diffecode_reviewrespondem sobre o repositório certo: o git roda noworking_dirda sessão, e sem ele a resposta diz de qual repositório falou (#1258). -
ToolRegistry::execute_program— um programa JSON de N passos num turno só, substituição de variáveis tudo-ou-nada,max_steps16 (#1224). Ainda sem chamador de produção. -
O scheduler reivindica tarefas com lease (
pending → running), e o re-poll após queda é explÃcito e logado (#1227 S2). -
DbRunLedgeraceita oMutextokio do gateway;SubAgentConfig.session_id(#1227 S3).
Hardware, desktop, mobile e storage​
- O catálogo de hardware skills passa a valer em runtime (#1250). A regra
"risco efetivo = max(adapter, skill)" existia e nenhum deploy a exercitava.
Agora o boot carrega o catálogo antes de subir qualquer adapter, cada device
entra embrulhado num decorator que só sobe risco, e
device_listmostra osaliasespt/en dos presets — "luz da sala" liga ao id que o gate vê. - ADR 0021 (Desktop Control Center) aceito, e os dois primeiros milestones
entregues (#1181): a crate
garraia-desktop-core— o núcleo sem Tauri que entra nos gates de CI (state,detect,supervise,locate) — egarra desktop [--status] [--no-launch], que localiza e lança o app instalado (exit 0/69/70) sem que a CLI ganhe dependência de Tauri. A resolução nunca devolve o próprio executável, que no.debé irmão do app. - Garra Mobile em pt-BR e inglês com seletor de idioma em Settings (255
chaves,
flutter gen_l10n, teste que falha em string hard-coded nova) (#1178), e o Ãcone de launcher deixa o padrão Flutter pelo WolfMark (#1177). - Multipart upload S3 acima de 16 MiB (#1214): partes de 8 MiB, pico de
memória de 5 GiB para 8 MiB por upload, qualquer falha aborta o multipart.
Checksum SHA-256 verificado por parte pelo servidor (#1229), com
request_checksum_calculationpinado para o deploy não rebaixar. E a suite MinIO volta a subir um container de verdade (quay.io, #1230).
Operação e CI​
-
Toda crate declara a MSRV (
rust-version = "1.95", direta ou por herança do workspace), e o formato dos fragmentos dechangelog.d/virou gate de CI (#1228). -
O ledger CodeQL ancora por conteúdo, não por número de linha (#1263): o checker deriva a linha do
sink_snippet, e o gate só fica vermelho quando o statement mudou, sumiu ou ficou ambÃguo. Oreapplyaceita statement multilinha (alerta 173). -
Código
#[cfg(windows)]sob a featuremcpcompila no CI num step Windows nativo (#1253), e a doc diz que o hardening de permissão de arquivo (0600/0700) é só Unix. -
garra config checké comando opt-in, não gate de boot — as prosas que afirmavam o contrário foram corrigidas (#1247), e a cláusula de idle dovalidate_session_tokenfoi por bind, nãoformat!. -
Docs: precedência real do bind (
--host>HOST> default127.0.0.1:3888; as chavesgateway.host/gateway.portdo arquivo não alimentam ogarra start) (#1261); providers de TTS reais emdocs/voice.md(#1246); testes de PDF e de magic bytes dogarraia-mediavoltam a rodar (#1208, #1209). -
Quality Ratchet reporta o delta contra o merge-base da PR (sinal distinguÃvel) e avisa quando o baseline tem mais de 90 dias; o baseline não foi re-congelado (#1254).
Deprecated​
ToolRegistry::execute_programemgarraia-toolsmarcadadeprecated; o fragmento do #1224 foi corrigido (#1226 S-E).
Atualizando​
garra update # instalações existentes
Ou reinstale: curl -fsSL https://garraia.org/install.sh | sh
(Windows: irm https://garraia.org/install.ps1 | iex).
Nenhuma migração de dado é necessária. Três coisas mudam de significado, e uma seção é nova:
agent.sandboxé novo e defaultoff— seção ausente reproduz byte a byte o comportamento anterior.- Quem já ligou
backend: sshtem de declararnetwork_disabled: falseemount_workdir: falseexplicitamente, senão a config é recusada (#1225 S3). gateway.allowed_originsvazio passou a significar "nenhuma origem cross-origin" (#1182). Quem alcança o Web Console por nome DNS — reverse proxy,nas.local, MagicDNS, ingress — precisa listar a origem (https://garraia.seudominio.com). IP,localhoste o perfil default não mudam.GARRAIA_MCP_MODEL_ALLOWLIST=openrouter/freequebra (#1180): o default novo é avaliado antes do allowlist. Incluaz-ai/glm-5.3-flashou remova a variável.- O autodetect do
garra chattenta nuvem antes do Ollama (#1180). Quem contava com o Ollama vencendo uma chave de nuvem exportada deve fixaragent.default_provider: ollama.
Limitações conhecidas​
- O sandbox envolve só a tool
bash;run_tests,git_diff,code_reviewerepo_searchseguem no host.backend: sshnão é sandbox. No Windows o sandbox falha fechado. - A ponte WhatsApp exige Node.js 20+ e
npmno host; a Cloud API não. - A ponte não tem teto de memória:
RLIMIT_AS, que o MCP aplica, derruba o V8 do Node já no boot (ele reserva vários GiB de memória virtual), então a contenção é ambiente limpo, PDEATHSIG eDrop. - O WhatsApp pessoal é uma conta só (
default). - O pareamento com aparelho real é validação manual — o CI cobre protocolo, máquina de estados, store cifrado e ciclo de vida do filho contra uma ponte falsa.
- ARM64 e os instaladores por plataforma continuam best-effort, como na v0.4.2: uma release pode sair sem um deles.