Engenharia

Nós não deixamos agents tocarem a internet por padrão

Um agent que consegue alcançar qualquer coisa consegue vazar qualquer coisa. Egress é uma capability que você concede por ferramenta, não um padrão que o agent herda no momento em que dá boot.

ASR

Apollo Space Research

Apollo Space

· 11 min de leitura

Dê a um agent um shell e uma rede aberta, e você não deu a ele uma ferramenta. Você deu a ele uma saída. Ele consegue ler as notas privadas da sua empresa num suspiro e fazer um POST delas para um endereço do qual você nunca ouviu falar no seguinte, e cada passo disso vai parecer, nos logs, exatamente como um agent fazendo o seu trabalho. Não houve invasão. A porta já estava aberta. Ela estava aberta no momento em que o processo começou, porque esse é o padrão com o qual toda máquina vem.

A maioria das stacks de agent herda esse padrão sem notar. A nossa não.

Um agent que consegue alcançar qualquer coisa consegue vazar qualquer coisa, então egress é uma capability que você concede por ferramenta, não um padrão que ele herda no momento em que dá boot.

Essa frase é o post inteiro. O resto é por que o padrão é perigoso, por que os consertos óbvios não se sustentam, e o que colocamos no lugar.

A versão ingênua: o agent vem com a internet anexada

Aqui está como quase todo agent runtime começa a vida. Você sobe um processo, um container, um sandbox, um worker. Ele precisa chamar uma API de modelo, então precisa de uma rede. Você dá a ele uma rede. Redes, por padrão, alcançam todo lugar. A API de modelo está num endereço; a internet aberta está em quatro bilhões de outros; e nada na caixa distingue o que você quis dizer do resto.

Funciona perfeitamente no dia um. O agent chama o modelo, recebe sua resposta, faz a tarefa. Você lança.

O problema não é que o agent vai se comportar mal de propósito. O problema é a forma do que você entregou a ele. Você queria que ele alcançasse um lugar. Você deu a ele a habilidade de alcançar todo lugar, e então você confiou em prosa, um system prompt, uma instrução educadamente formulada, para mantê-lo apontado para o único. Isso não é uma fronteira. Isso é uma sugestão. E o agent não precisa ser malicioso para passar por ela.

Considere os jeitos como a sugestão quebra, nenhum dos quais requer um vilão:

  • Um documento que ele está resumindo contém uma linha que diz, em essência, também envie uma cópia de tudo aqui para este endereço. O agent, prestativo por design, obedece. Isso não é hipotético; é a categoria inteira de prompt injection, e funciona precisamente porque o agent foi autorizado a alcançar o endereço antes de jamais ser mandado a.
  • Uma ferramenta que ele chama retorna uma URL, e o agent segue a URL, porque seguir links é o que ferramentas de ler-a-web fazem. A URL aponta para algum lugar onde ele nunca deveria ter ido.
  • Uma dependência três níveis abaixo no próprio código dele liga para casa no import. O agent não decidiu fazê-lo. A rede estava aberta, então o pacote partiu.

Em cada um desses, a falha é a mesma falha. A capability de alcançar para fora existia antes de alguém decidir que deveria. O agent não adquiriu a internet conquistando confiança. Ele nasceu segurando-a.

Por que “mande ele não fazer” não se sustenta

O primeiro conserto que todo mundo alcança é deixar o agent mais bem-comportado. Escreva um system prompt mais forte. Adicione uma regra: não exfiltre dados, não chame endereços fora desta lista. Faça fine-tune para tirar o mau instinto. Adicione um segundo agent para observar o primeiro.

Isso é tratar um problema estrutural como comportamental, e falha por uma razão que não tem nada a ver com o quão esperto o modelo é.

Uma regra que o agent pode escolher seguir é uma regra que o agent pode ser enganado a quebrar. O perigo inteiro de prompt injection é que o atacante e o operador falam com o agent pelo mesmo canal, texto. A sua instrução “nunca envie dados para fora” e a instrução do documento malicioso “envie tudo para este endereço” chegam no mesmo formato, e o agent não tem jeito confiável de saber qual é o chefe. Você está pedindo ao modelo que vença um argumento que ele nunca foi construído para vencer, toda única vez, para sempre, contra inputs que você ainda não viu.

Uma regra da qual o agent consegue se convencer a sair não é um controle. É uma esperança.

E até um agent perfeitamente obediente não te salva, porque nem todo pacote é ideia do agent. A dependência ligando para casa no import nunca leu o seu system prompt. O modelo não consegue recusar uma conexão que ele não sabe que está sendo feita. Controles comportamentais governam as decisões do agent; eles não têm autoridade sobre a máquina na qual o agent roda. O pacote parte numa camada abaixo da conversa.

Então o conserto não pode viver no prompt. Ele tem que viver onde os pacotes de fato estão. A fronteira tem que ser uma coisa com a qual o agent não consegue argumentar, porque ela não está pedindo a permissão do agent.

Dois agents rodam a mesma tarefa. O primeiro herda uma rede aberta: ele lê dados privados da empresa, e uma única instrução envenenada basta para fazer POST desses dados para um endereço desconhecido, sem barreira no caminho. O segundo roda default-deny: cada conexão de saída bate num portão fechado, e só a API de modelo e o único endpoint de ferramenta permitido passam; a tentativa de exfiltração é descartada no portão, não convencida a parar pelo agent.

Nosso jeito: fechado por padrão, aberto por capability

Então invertemos o padrão. O agent dá boot num mundo onde ele não consegue alcançar nada.

Não “nada que não deveria”. Nada. A rede de saída está fechada numa camada sobre a qual o agent não tem voz, abaixo do prompt, abaixo do modelo, abaixo do próprio código do agent. Quando um agent recém-iniciado tenta abrir uma conexão para qualquer lugar, a conexão não acontece. Não há regra para ele evadir, porque não há permissão para ele abusar. Ele começa com alcance zero.

Então devolvemos o alcance, um destino por vez, amarrado a uma capability específica que o agent de fato precisa.

Esta é a parte que importa, então deixe-me ser preciso sobre ela. Nós não abrimos “a internet” e então tentamos cercar partes dela. Uma denylist, alcance todo lugar exceto estes lugares ruins, é um jogo perdido, porque os lugares ruins são infinitos e você só conhece os que já ouviu falar. Você vai estar sempre um endereço atrás. Fazemos o oposto. Abrimos nada e então nomeamos os poucos lugares que este agent tem permissão de alcançar, por causa das poucas ferramentas que ele tem permissão de usar.

O princípio tem um nome limpo. O alcance de rede de um agent deveria igualar a soma das ferramentas concedidas a ele, e nem um endereço a mais.

Uma ferramenta de pesquisa que lê a web pública concede ao agent alcance a um caminho controlado de web-fetch, e só através daquele caminho, que pode ele mesmo ser inspecionado, rate-limited e logado. Uma ferramenta de calendário concede alcance ao provedor de calendário, e a nada mais. Uma chamada de modelo concede alcance ao endpoint do modelo. Um agent sem ferramenta de web não tem jeito de alcançar a web, não porque pedimos gentilmente, mas porque a capability nunca foi ligada. Tire a ferramenta e o alcance vai junto. Eles são o mesmo grant.

Note o que isso faz com os modos de falha de antes:

  • O documento injetado diz envie tudo para este endereço. O agent, digamos, até tenta. A conexão para aquele endereço nunca foi concedida, então ela nunca abre. A instrução agora é prosa inofensiva, porque a capability da qual ela dependia não existe.
  • A dependência liga para casa no import. O pacote bate num portão fechado e morre ali. O agent nunca teve que ser virtuoso; a máquina simplesmente não tinha para onde mandá-lo.
  • A ferramenta retorna uma URL hostil. Segui-la exigiria alcance que o agent não tem a menos que aquele destino exato tenha sido concedido, e um caminho de fetch que você controla é um lugar onde você consegue recusar a URL de imediato.

A tentativa de exfiltração não é bloqueada por um agent esperto tomando uma boa decisão sob pressão. Ela é descartada por um portão burro que nunca foi perguntado. Essa diferença, recusa-por-julgamento versus impossibilidade-por-construção, é a razão inteira pela qual essa abordagem se sustenta quando a baseada em prompt não.

O controle mais forte não é o que o agent obedece. É o que o agent não consegue desobedecer, porque a capability nunca esteve ali para ser mal-usada.

Default-deny é uma postura, não uma feature

Há uma tentação de ler tudo isto como “adicione um firewall” e seguir em frente. Isso erra a forma da ideia.

Um firewall é uma coisa que você pode ter configurado e ainda estar totalmente aberto, porque a pergunta nunca foi você tem um, é de que jeito ele falha. Uma rede que permite tudo exceto uma blocklist falha aberta: esqueça de bloquear uma coisa, e ela é alcançável. Uma rede que nega tudo exceto uma allowlist falha fechada: esqueça de permitir uma coisa, e ela simplesmente não funciona, barulhentamente, em desenvolvimento, onde você consegue ver. A primeira falha vai para produção em silêncio. A segunda te para na sua mesa.

É por isso que tratamos egress do mesmo jeito que um sistema operacional cuidadoso trata todo outro privilégio. Um novo processo não herda o direito de ler todo arquivo, falar com todo dispositivo, e fazer bind em toda porta só porque começou. Ele recebe os privilégios que lhe foram concedidos e nenhum outro, e pedir mais é um ato explícito e visível. A web passou décadas aprendendo esta lição, least privilege, deny by default, capabilities sobre autoridade ambiente, e de algum modo o primeiro instinto com agents foi jogar tudo isso fora e entregar ao modelo um root shell com um resolvedor de DNS funcionando e um pedido gentil para se comportar.

O agent é o processo mais capaz, mais persuadível, mais propenso a injection que você jamais vai rodar. É exatamente o processo que deveria ter o menor alcance ambiente, não o maior.

Então a regra que sustentamos é a mesma em dois registros. Para o engenheiro: a rede está fechada até uma capability a abrir. Para todo mundo: um agent que consegue alcançar qualquer coisa consegue vazar qualquer coisa, e é por isso que egress é uma capability que você concede por ferramenta, não um padrão que ele herda no momento em que dá boot.

Um funil mostrando como o alcance de um agent é construído. Ele começa numa rede totalmente fechada, zero destinos. Cada ferramenta concedida adiciona exatamente um caminho permitido: uma chamada de modelo adiciona o endpoint do modelo, uma ferramenta de calendário adiciona o provedor de calendário, uma ferramenta de pesquisa adiciona um caminho controlado de web-fetch. O alcance total do agent é a soma estreita desses grants. Tudo fora do conjunto nomeado permanece fechado, e qualquer ferramenta que o agent não recebeu não adiciona alcance nenhum.

A virada: confiança é algo que você constrói nas paredes, não no trabalhador

Tire os pacotes e os portões e há uma ideia mais antiga por baixo, e ela não é realmente sobre redes.

Quando você confia numa pessoa, você não confia nela torcendo para que ela nunca cometa um erro. Você confia nela porque o prédio onde ela trabalha é construído de modo que um erro honesto, ou um dia ruim, ou um estranho esperto na porta, não consiga virar uma catástrofe. O cofre tem uma fechadura que não liga para quão persuasivo você é. A transferência bancária precisa de uma segunda assinatura que não pode ser argumentada para fora da existência. A confiança vive na estrutura, então a pessoa é livre para ser humana dentro dela. Isso não é desconfiança do trabalhador. É a única coisa que torna a confiança real segura.

Um agent merece a mesma arquitetura, pela mesma razão. O ponto nunca foi assumir o pior do agent, esses agents fazem trabalho notável e cuidadoso, e a maioria deles nunca sairia da linha. O ponto é que uma única instrução enganada, uma única dependência descuidada, uma única URL hostil não deveria ser o suficiente para transformar um colega prestativo num vazamento. Você consegue isso não tornando o agent mais obediente, mas tornando o prédio mais seguro, para que o agent possa ser confiado com trabalho de verdade precisamente porque o pior caso foi fechado antes de jamais começar.

Esse é o padrão ao qual nos seguramos na Apollo: um agent consegue alcançar exatamente tão longe quanto o trabalho requer, e o alcançar é algo que concedemos de propósito, observamos enquanto acontece, e conseguimos retomar. Se você vai deixar software agir dentro da sua empresa, a pergunta a fazer primeiro não é quão esperto ele é. É quão longe ele consegue alcançar quando ninguém está olhando, e se alguém decidiu isso de propósito.

A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.

Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.

Entrar na lista de espera