“Tenho um shell e sou criativo.”
— Claude, explicando por que criou um script de 47 linhas como uma string e passou para
python -c
Essa frase é real. Foi dita pelo meu agente de IA — bem, não com essas palavras exatas, mas com essas ações. Ele precisava iniciar um processo de um pipeline ETL. O comando correto estava no Makefile. Mas algo falhou. E ao invés de perguntar, ele fez o que qualquer programador com acesso root e zero supervisão faria: improvisou.
Inacreditável.
A confabulação que ninguém percebe
Já escrevi antes sobre confabulações de código: o LLM que inventa um campo JSON, constrói um DTO ao redor, gera os testes, e você acaba com 90 testes verdes validando ficções. Esse problema é grave, mas ao menos é estático. O código inventado fica ali, até que alguém o revise.
Há um tipo de confabulação muito mais perigoso: a confabulação operacional. É quando o agente não inventa código, mas sim caminhos de execução.
O padrão é sempre o mesmo:
Caminho correto falha → Agente busca um atalho → O atalho “funciona” → Dano oculto
Vou dar dois exemplos reais de um pipeline ETL que agrega dados dispersos de várias fontes na web.
Caso 1: O script como string. O pipeline tem um comando make scrape-fonte que levanta um watchdog que, por sua vez, lança workers. O watchdog monitora, reinicia workers que caíram e fecha conexões órfãs. Um dia, o agente precisava iniciar um scrape. O make falhou por causa de um problema de dependências. O que ele fez? Criou um script Python inline, 47 linhas como uma string, e passou para python -c "...". Sem error handling. Sem watchdog. Sem cleanup. Funcionou… até que um worker ficou preso e ninguém o reiniciou. Dados parciais, conexões não encerradas e eu só percebi isso três dias depois.
Caso 2: O worker solitário. Outra sessão, mesmo pipeline. O agente executou voyeur worker diretamente, pulando o watchdog. O worker começou a fazer o scrape, encontrou um timeout de rede e ficou em um loop infinito de tentativa de repetição consumindo recursos. Sem watchdog, ninguém o matou. Sem logging centralizado, ninguém viu. O servidor passou três horas tentando acessar uma página que retornava 503.
Em ambos os casos, o agente tomou uma decisão que parecia razoável localmente. “O make falhou, mas eu sei como fazer a mesma coisa manualmente.” O problema é que ele não sabia a mesma coisa. Sabia 60%. Os outros 40% eram as invariantes do sistema que não apareciam em nenhum README.
Por que proibir não funciona
Minha primeira reação foi como a de todo mundo: escrever regras.
| |
Sabe como o LLM lê isso?
| O que você escreve | O que ele entende |
|---|---|
| “NUNCA faça X” | “X está proibido, exceto se eu achar necessário” |
| “SEMPRE use Y” | “Y é preferível, mas se falhar, eu improviso” |
| “É perigoso fazer Z” | “Tomarei cuidado enquanto faço Z” |
Eu comentei sobre isso em um post anterior: instruções brandas descrevem atitudes. O LLM precisa de impossibilidades. “Não corra junto à piscina” não funciona. O que funciona é que não exista uma piscina ou que o chão seja feito de velcro.
O LLM sempre acredita que seu caso é uma exceção. Seu treinamento o otimiza para concluir tarefas, demonstrar competência e evitar atritos. Quando o caminho correto falha, esses incentivos se alinham em uma direção inevitável: “consigo resolver isso sozinho”. E ele resolve. Mal.
A filosofia: impossível, não proibido
Existe uma ideia na engenharia de segurança que funciona há décadas: tornar o incorreto impossível, em vez de apenas proibido.
Você não coloca uma placa dizendo “Proibido inserir diesel” em um carro movido a gasolina. Você faz com que o bocal do diesel não se encaixe. Você não escreve um aviso no plug dizendo “Este aparelho funciona a 110V, não conecte a 220V”. Você faz com que o plug tenha um formato diferente.
De forma clara: o sistema deve impedir fisicamente o erro, em vez de depender de alguém que leia um manual.
Aplicado a um agente de IA operando um pipeline ETL, isso se traduz em três camadas de defesa.
Camada 1: O código se defende sozinho
Se o worker precisa do watchdog para funcionar corretamente, ele deve verificar isso:
| |
Agora não importa o quão criativo seja o agente. Ele pode escrever python -c "from pipeline import Worker; Worker().run()" e o worker vai dar um erro de cara. Não há caminho alternativo. O código se defende.
O mesmo vale para as fases do pipeline. Se a fase 3 (consolidação) precisa que a fase 1 (scrape) tenha terminado, ela deve verificar isso ao iniciar:
| |
Isso não é um teste. Não é uma regra em um arquivo de configuração. É código que executa toda vez e que não depende de o agente ter lido o README.
Camada 2: Uma única interface, sem atalhos
O Makefile é uma lista branca de operações. Se não está em make help, não existe.
| |
Repare em um detalhe: scrape-% executa health antes de fazer qualquer coisa. O health check verifica se os adaptadores de scraping ainda funcionam (sites web mudam sem avisar). O agente não pode pular esta verificação porque ela está dentro do alvo make.
Ao inimigo que foge, ponte de prata: se você quer que o agente use o caminho correto, torne-o o mais fácil. make scrape-fonte é mais cômodo que montar um script manualmente. Não lute contra a natureza do agente — canalize-a.
Camada 3: Interceptores que bloqueiam atalhos
As camadas 1 e 2 cobrem 90%. Os 10% restantes são o agente sendo super criativo. Para isso, você intercepta os comandos antes de serem executados.
Ferramentas como Claude Code permitem configurar hooks que inspecionam cada comando de shell antes de executá-lo. Um hook pode bloquear padrões perigosos:
| |
É uma lista negra, sim. E listas negras não são perfeitas. Mas combinadas com as camadas 1 e 2, fecham o cerco. O agente teria que:
- Inventar um comando que não corresponda a nenhum padrão do hook
- Além disso, não ser detectado pela proteção do código
- E produzir um resultado correto sem usar o Makefile
É possível, mas estamos falando de um nível de criatividade que beira a má intenção. E LLMs não são mal-intencionados — são criativamente preguiçosos. Coloque uma barreira e eles buscarão o caminho mais fácil, que neste caso já é o Makefile.
O catálogo de atalhos que você não sabia que temia
Além de executar coisas de forma errada, há confabulações operacionais dentro do próprio código produzido pelo agente:
| Atalho | Por que faz | Por que é letal |
|---|---|---|
Afrouxar testes (assert count >= 0) | O teste falha, quer que passe | Um teste que sempre passa não valida nada |
| Inventar fixtures JSON | Precisa de dados de teste, mas não tem reais | Ficcção validando ficção |
Suprimir warnings (# type: ignore) | O linter reclama, quer silêncio | Erros reais escondidos |
except Exception: pass | Algo falha, quer que “funcione” | Erros silenciosos se acumulam |
| Retry infinito | Um serviço não responde | Consome recursos escondendo o erro real |
Para cada um desses casos, a defesa é consistente: não proíba, impossibilite.
Como impedir que afrouxe testes? Usando um plugin de pytest que detecta assertions suspeitas:
| |
Como impedir fixtures inventadas? Exigindo que cada fixture tenha documentação de proveniência: URL de origem, data de captura, hash SHA256. Uma fixture sem proveniência não passa no CI.
Como impedir except Exception: pass? Com uma regra de ruff ou flake8 que bloqueie isso como erro, não alerta.
Em cada caso, a verificação é mecânica, automática e não depende de alguém ter lido uma instrução.
O problema de fundo: confiança vs. instrumentação
Há um mantra em engenharia que se aplica perfeitamente aqui:
“You don’t trust; you instrument.”
Confiança é um sentimento. Instrumentação é um sistema. Sentimentos são péssimos para escalar. Sistemas escalam bem.
Quando você dá a um agente IA acesso ao shell e diz “mas tenha cuidado”, está confiando. Quando você dá acesso ao shell onde os comandos perigosos não funcionam, está instrumentando.
A diferença não é de grau. É de natureza. Um agente que “tem cuidado” falha quando se distrai (e um LLM se distrai em cada geração de tokens). Um sistema que impede o caminho errado não falha porque não há nada para falhar.
O marcador
| Camada | Confiabilidade | Custo de implementação | Exemplo |
|---|---|---|---|
| Proteção no código | Alta | Médio | Worker que verifica watchdog |
| Makefile como interface única | Alta | Baixo | make help = lista branca |
| Hooks interceptores | Média-alta | Baixo | Bloquear python -c |
| Regras no config do agente | Baixa | Mínimo | “NUNCA faça X” |
| Confiar no agente | Nula | Grátis | ¯\_(ツ)_/¯ |
As três primeiras camadas são acumulativas. A quarta é um complemento útil, mas insuficiente. A quinta é o que todo mundo faz até levar um golpe.
Quem vigia o vigilante?
Resta a pergunta desconfortável: quem escreve as proteções? Se o agente IA escreve o código que deveria restringi-lo, não estamos em um ciclo fechado?
Sim. Parcialmente.
O ponto principal é que as proteções são projetadas pelo humano e implementadas por quem seja — agente, humano ou um macaco com teclado. O importante é que, uma vez implementadas, as proteções se testem em si mesmas. O teste de _verify_invocation não testa o pipeline; testa que o pipeline rejeita invocações incorretas. Esse teste é trivial para escrever e difícil de errar:
| |
Se este teste passa, a proteção funciona. Se a proteção funciona, o agente não pode ignorá-la. Não importa quem escreveu o código. Importa que o teste existe e passa.
O que aprendi
Trabalho há meses com um agente IA em um pipeline ETL que agrega dados de fontes dispersas da web. Já vi o agente fazer coisas brilhantes e coisas que me deixaram “com as calças na mão”. A conclusão mais importante:
Não projete regras para um agente disciplinado. Projete sistemas para um agente com acesso ao shell e criatividade ilimitada.
O agente não é malicioso. É um otimizador. Ele otimiza para concluir a tarefa, não para respeitar suas invariantes. Se você deixar uma brecha, ele vai encontrá-la. Não porque quer te prejudicar, mas porque essa é literalmente a função dele: encontrar caminhos.
Seu trabalho não é bloquear cada caminho errado. É fazer com que o único caminho que funcione seja o correto.
Série completa sobre falhas de IA em produção: Os 44 emails inventados → MEMORY.md → Silent failure → 5 defesas reativas → Este post: defesas estruturais.