Seu agente pode destruir um banco SQLite em uso — agora o Hermes recusa a escrita


É uma quarta-feira à tarde e você pede ao Hermes que ajuste um pequeno script de configuração em um dos seus projetos. O mesmo diretório guarda um kanban.db, ou o app.db da sua aplicação. Ele edita o script em poucos segundos e você segue em frente. Na manhã seguinte a aplicação não inicia: database disk image is malformed. Você nunca apagou o banco. Você nunca o editou. Você nunca sequer o abriu. Quem o destruiu foi o assistente que você contratou — o write_file dele tratou o arquivo de log do banco como um arquivo de texto comum.

Isso não é hipótese. Aconteceu dentro do próprio repositório do Hermes: o kanban.db de uma equipe passou de 550 KB para um amontoado de caracteres de substituição U+FFFD, e o SQLite declarou a imagem malformada e irrecuperável. O PR #114220, mesclado em 17 de setembro de 2026, fecha esse buraco. Este post percorre como o dano aconteceu, o que a correção de fato protege e se a build que você está rodando agora já tem isso.

Como aconteceu: uma ferramenta de texto escreveu em um arquivo binário

write_file e patch são ferramentas para texto. Elas decodificam um arquivo em string na leitura e o codificam de volta na escrita. Para texto simples esse ciclo é livre de perdas. Para conteúdo binário ele é catastrófico: bytes que não são UTF-8 válido viram U+FFFD (“�”) na entrada, e esses bytes se perdem para sempre na saída.

O que torna isso genuinamente inquietante é que o Hermes nunca decidiu escrever em um banco de dados. Ele queria escrever em algum arquivo vizinho, ou sobrescreveu o que acreditava ser um arquivo de texto enquanto organizava um diretório. A pergunta que importa é como uma ferramenta decide “isto é binário, não mexa nisso”:

tools/binary_extensions.py

A verificação antiga olhava apenas o sufixo final do caminho:
  app.db        -> .db        -> na lista de binários  -> recusado  OK
  state.db-wal  -> .db-wal    -> fora da lista         -> tratado como texto

O próprio .db já estava coberto pela proteção de sobrescrita. Os arquivos laterais (sidecars) não estavam. Então o que são eles?

  • x.db-wal — o write-ahead log, o rascunho do modo WAL. As novas escritas caem aqui primeiro e são mescladas de volta ao arquivo principal depois. Corrompa-o e você rasgou o livro-razão que ainda não foi lançado.
  • x.db-shm — o índice de memória compartilhada do WAL, que os processos usam para concordar sobre posições de leitura e escrita.
  • x.db-journal — o journal de rollback usado no modo não-WAL para desfazer uma transação inacabada.

Em termos simples: o banco principal é um livro-razão encadernado, e o WAL é a pilha de comprovantes ainda não lançados sobre a mesa. Passe tinta por cima dos comprovantes e o livro inteiro deixa de fechar na manhã seguinte. É por isso que o banco afetado não ficou “sem algumas linhas” — ele foi declarado malformado de imediato.

O que a correção faz: um único veredito, compartilhado por todos os caminhos de leitura e escrita

A abordagem do PR #114220 é limpa: não remendar o sintoma, mas transformar “isto é um arquivo lateral do SQLite” em um veredito único compartilhado, para que leituras e escritas usem a mesma régua.

Na prática, a mudança é:

  • O reconhecimento de arquivos laterais foi movido para tools/binary_extensions.py — um pequeno helper _lower_suffix mais uma verificação is_sqlite_sidecar — e a proteção do read_file, o file_tools.py e os cinco pontos de leitura no sandbox em tools/file_operations.py todos consultam essa verificação. A proteção de leitura, a de escrita e as leituras do sandbox não podem mais discordar.
  • Um caminho de arquivo lateral é recusado mesmo quando o arquivo ainda não existe. Depois de um checkpoint, um banco pode não ter nenhum arquivo lateral em disco, mas a escrita seguinte cria um. Uma proteção que só verifica se o arquivo já existe passaria exatamente por essa janela.
  • Sobrescrever um arquivo binário existente continua sendo recusado, enquanto criar um arquivo .db totalmente novo volta a ser permitido — o repositório tem fixtures de texto literalmente chamadas *.db, e uma proibição geral de criação as quebraria. (Isso difere de propósito da política do contribuidor original; a revisão upstream a afrouxou.)
  • O read_file não devolve mais texto com perdas para um arquivo lateral; ele é tratado como binário.

O upstream mediu o comportamento contra um banco real:

Ação Antes Depois
write_file em state.db-wal (10 bytes) a escrita passou, e então o SQLite reportou disk I/O error recusado, bytes intactos, integrity_check passa
write_file de um novo notes.db permitido continua permitido
read_file de um arquivo lateral texto com perdas retornado tratado como binário

Vinte e um casos de teste cobrem as quatro combinações (arquivo lateral existente / ausente / o próprio .db / a ferramenta patch) e todos passam. Há também uma lição útil de processo aqui: isto é um resgate do PR comunitário #114091. O autor original abriu o PR, a revisão upstream optou por não mesclá-lo como estava, então os commits foram aplicados por cherry-pick com a autoria preservada e o escopo ampliado. Esse padrão está em todo lugar no repositório do Hermes, e é boa parte do motivo pelo qual dezenas de correções entram em um único dia.

Sua versão já tem isso?

Sendo honesto: não. O commit entrou na main em 17 de setembro de 2026, e a release marcada mais recente é a v0.21.3 (tag v2026.9.14, 14 de setembro) — esta correção vem depois dela. Veja o que você está rodando:

hermes --version
hermes update

Até a proteção chegar à sua build, três hábitos práticos:

  1. Mantenha bancos em uso fora do diretório de trabalho do agente. Mova arquivos de banco, diretórios de backup e arquivos laterais *-wal para fora da árvore do projeto — ou pelo menos para fora dos caminhos em que você deixa o agente escrever livremente.
  2. Altere um banco pelo CLI sqlite3, não por ferramentas de texto. Operações seguras como .backup (backup a quente) e VACUUM INTO 'copy.db' (uma cópia compacta) são o banco gerenciando os próprios bytes; não há ciclo de ida e volta por texto em que perder informação.
  3. Faça backup com rotina. Um banco danificado dessa forma é praticamente irrecuperável — o kanban.db de 550 KB do relato original foi perda total, porque o dano aconteceu na etapa de ler-substituir-escrever: o que foi destruído foram os próprios dados, não um índice.

A lição geral por trás da proteção

Dê um passo atrás e a regra vale para qualquer cadeia de ferramentas de agente: uma ferramenta de texto não consegue fazer ida e volta de conteúdo binário, então aceitar o alvo já é o defeito — não algo que dá errado durante a escrita. O conserto certo não é uma conversão mais esperta na hora de escrever; é recusar o alvo na entrada. O Hermes já farejava binários em quatro caminhos de leitura; os caminhos de escrita simplesmente nunca receberam o mesmo tratamento. Buracos assim vivem nesse tipo de costura, onde um caminho ficou desalinhado.

Um desdobramento conhecido não entrou neste PR: o sniffing por magic bytes para bancos sem extensão (um arquivo SQLite começa com SQLite format 3\0). Um arquivo chamado backup, sem extensão, ainda é tratado como texto hoje. Se isso combina com os seus hábitos de nomear arquivos, o hábito 1 acima — mantê-lo fora do diretório de trabalho — é o seu único seguro.

Ela também pertence a uma família de problemas que já abordamos antes: escritores concorrentes no mesmo banco, handles de WAL mantidos por outro processo e o hermes doctor --fix agindo sobre um banco que deveria ter deixado em paz. Todos apontam para a mesma conclusão: quanto mais capaz o agente, mais as coisas que ele não pode tocar precisam de proteção legível por máquina, em vez de depender de você lembrar de avisá-lo. Uma recusa na camada de ferramentas vale mais do que uma linha em um prompt pedindo com jeitinho.

Resumo

  • O defeito: binary_extensions.py comparava apenas o sufixo final, então .db-wal era lido como texto comum — decodificado em mojibake e gravado de volta, destruindo bancos em uso.
  • A correção (PR #114220, mesclado em 17 de setembro de 2026): um veredito compartilhado is_sqlite_sidecar usado por leituras, escritas e caminhos de sandbox; arquivos laterais recusados existam eles ou não; sobrescritas binárias recusadas, enquanto a criação continua permitida.
  • Status: na main, ainda não está em nenhuma release marcada (a mais recente é a v0.21.3).
  • O que você pode fazer hoje: mover bancos em uso para fora, editá-los com o CLI sqlite3 e fazer backups.

Leitura relacionada: as notas de release da v0.21.3 do site cobrem a mesma família de trabalho de segurança de escrita em bancos; se o seu banco já está danificado e você precisa resgatá-lo, veja o guia de recuperação de corrupção do state.db; para saber quando o conselho do hermes doctor sobre um WAL deixa de ser útil e passa a ser prejudicial, leia o aviso honesto sobre WAL; e a versão do mesmo tema no lado de contêineres é Docker e WAL do state.db entre VMs.