Saltar para o conteúdo

Sandbox e segurança

Um agente lê ficheiros, executa comandos de shell e chama APIs externas em seu nome. A falha que lhe interessa raramente é a má intenção — é um agente que lê mal um caminho e reescreve o seu checkout de trabalho, ou que faz push de um branch que ainda não tinha acabado de rever. Por isso, a pergunta a que o produto tem de responder não é “o agente é de confiança”, mas “qual é o raio de impacto quando ele se engana”.

O Control Center responde a isso em camadas e as camadas são independentes de propósito. Se faltar uma, as outras mantêm-se. Isso é importante, porque a camada mais exterior — a sandbox do sistema operativo — é a única condicional: depende de o anfitrião oferecer um backend e, no Windows, nenhum oferece.

Cinco coisas limitam uma execução de agente em todos os anfitriões, com ou sem sandbox. Nenhuma delas é uma fronteira do kernel; todas são reais. Onde o anfitrião oferece um backend, a sandbox do sistema operativo assenta por cima delas e a secção “O que a sandbox acrescenta e onde para”, mais abaixo, explica o que isso muda.

A sua própria worktree copy-on-write. Cada conversa aprovisiona o seu próprio checkout de cada repo associado, clonado copy-on-write através do rift nativo incluído, em vez de copiado byte a byte, para que o isolamento seja suficientemente barato para ser o comportamento predefinido. O repositório registado que adicionou ao espaço de trabalho não é o diretório em que o agente trabalha — duas conversas que toquem no mesmo repo não conseguem ver as edições uma da outra e nenhuma delas está a editar o seu checkout.

Vale a pena ser preciso quanto à granularidade: a worktree é por conversa, não por agente. Dois agentes na mesma conversa recebem diretórios de trabalho overlay distintos que partilham o repos/ dessa conversa — que é precisamente o objetivo, já que estão a colaborar na mesma alteração —, enquanto os seus diretórios privados de agente permanecem separados. O copy-on-write é o único backend onde o rift existe: um sistema de ficheiros que não consiga fazer reflink, ou um diretório de dados num volume diferente do repo, faz falhar o aprovisionamento em vez de recuar para uma alternativa. O Windows é a exceção — não há lá backend de copy-on-write, pelo que o backend é o git worktree, e não uma degradação silenciosa.

Confinamento de caminhos nas ferramentas de ficheiros do harness integrado. As ferramentas read, write, edit, apply_patch, search, find e search_files do harness resolvem todos os caminhos em relação ao diretório de trabalho da execução mais raízes partilhadas — o diretório repos/ da conversa, onde vivem as worktrees, e o diretório de anexos do espaço, quando existe — e recusam tudo o que caia fora. Os caminhos absolutos só são aceites se já estivessem dentro. Uma ligação simbólica dentro da worktree que aponte para fora também é resolvida e recusada. É por isso que um agente não consegue write para ~/.ssh, mesmo sem sandbox à vista.

A superfície de ferramentas do modo. O modo de uma conversa decide que ferramentas são sequer materializadas para a execução. Nos modos só de leitura (plan, review, orchestrate), as ferramentas que alteram o estado simplesmente não estão no registo e a política de comandos de shell desses modos nega liminarmente todos os comandos que alteram o estado — git commit, git push, npm install, docker e o resto — em vez de pedir confirmação para eles. Uma ferramenta que não está presente não pode ser chamada, o que é uma garantia mais forte do que uma ferramenta presente e sujeita a um portão.

As barreiras de ação unificadas. Cada ferramenta que altera o estado declara as classes de efeito que pode produzir e o motor de políticas resolve cada uma para permitir, pedir confirmação ou negar antes de a chamada ser executada. Um prompt sem nenhum aprovador ligado resolve-se em negado — uma pergunta que ninguém consegue responder nunca é um sim. Consulte as Barreiras para o modelo de resolução.

Credenciais intermediadas e um ambiente saneado. As credenciais são emitidas em cada lançamento e revogadas quando a execução termina, e o token do GitHub de uma execução pode ler, mas não fazer push — consulte “Os pushes passam pelo Control Center” mais abaixo. O ACP, o bash do harness e o Claude Code sob o invólucro nativo recebem um ambiente limpo dos vetores de injeção do linker dinâmico e do interpretador (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND e afins) antes de o processo arrancar. O Claude Code através do NoSandboxAdapter herda o ambiente do anfitrião.

As ferramentas de ficheiros do harness correm no processo do servidor, e não num processo filho, pelo que nenhum perfil de sandbox as conseguiria limitar mesmo que fosse anexado — só o bash passa pelo executor de comandos em sandbox. O seu confinamento é a verificação de caminhos descrita acima e essa verificação é, por isso, estrutural em vez de uma segunda linha de defesa.

A ferramenta bash é o outro ponto a precisar. Os seus comandos são avaliados em função da política de comandos do modo e depois lançados com um ambiente saneado e a worktree como diretório de trabalho. Onde existe um backend, esse lançamento é envolvido, pelo que é a sandbox que impede um comando de shell de escrever fora da worktree; onde não existe — no Windows, ou no Linux sem bwrap e socat — a política de comandos é a única coisa entre o comando e o resto do disco. No modo chat, a política nega liminarmente uma lista fixa (elevação de privilégios, destruição do sistema e do disco, fuga de contentores e namespaces) e pede confirmação sobre mutações de estado remoto (git push, gh pr create, publicações e instalações de pacotes). Tudo o que não está em nenhuma das listas é executado. Os pipelines, encadeamentos e sub-shells são analisados e cada subcomando avaliado separadamente, com a decisão mais restritiva a prevalecer.

O Claude Code, quando usado como adaptador, é o caso em que a sandbox tem o maior peso. O Control Center lança-o com --dangerously-skip-permissions, porque um claude -p não interativo bloquearia para sempre no seu próprio pedido de aprovação. As suas chamadas de leitura, escrita e edição acontecem dentro do seu próprio processo e o Control Center nunca as vê. As suas chamadas Bash são enviadas, antes de correrem, para um hook no servidor que classifica a linha de comandos e pergunta à regra correspondente, e as ferramentas mcp__* que o Control Center serve passam pelo portão como sempre. Onde existe um backend, essas chamadas invisíveis continuam a acontecer dentro do invólucro da sandbox, pelo que o sistema operativo é a fronteira à sua volta. Onde não existe, não há fronteira nenhuma. Cada adaptador declara isto na sua matriz de aplicação — consulte a matriz de honestidade dos adaptadores.

Não existem indicadores de permissão por agente. Se um agente pode criar um commit, fazer push, abrir um pull request, escrever num rastreador ou chegar à rede é decidido pelas barreiras — permitir, perguntar primeiro ou negar —, e as mesmas regras aplicam-se seja qual for o adaptador que executa o agente.

O push é a ação que tem de resistir mesmo a um agente que tente contornar uma regra, por isso é imposto por aquilo que o agente detém e não por aquilo que executa. Com uma GitHub App configurada, uma execução numa worktree do GitHub recebe um token de instalação limitado ao repositório que pode ler conteúdo e trabalhar com pull requests, mas não fazer push. A configuração do git da execução reescreve cada URL de push do GitHub (https://github.com/, git@github.com:, ssh://git@github.com/) para o gateway de execuções de agentes, um endpoint de loopback no cc_server. O gateway lê que refs o push atualiza, pergunta à regra Fazer push para um remoto — por isso uma regra limitada a branches, como “negar push para main”, aplica-se — e só então reencaminha o push para o GitHub com um token de escrita que emite para esse fim e nunca entrega ao agente. Um push recusado aparece na própria saída do git:

! [remote rejected] main -> main (Control Center policy: …)

O gateway só reencaminha pushes para o repositório da própria worktree ou para um repositório do GitHub associado ao espaço de trabalho. Os fetches vão diretamente para o GitHub com o token de leitura.

Os restantes comandos de shell encontram um portão mais brando. A ferramenta bash do harness e, através de um hook, a ferramenta Bash do Claude Code classificam cada linha de comandos e perguntam à regra em que se enquadra: git commit, merge, rebase, cherry-pick, revert e am são Criar um commit; gh pr create é Abrir um pull request; gh pr merge, review, comment e close são Publicar uma revisão ou fazer merge; gh issue create, edit, comment e close são Escrever num rastreador externo; as instalações de pacotes são Instalar um pacote; curl e wget são Aceder à rede. Isto reconhece a grafia convencional de uma ação, e um agente decidido a escondê-la (eval, um ficheiro de script, uma chamada direta à API) consegue. Os pushes não dependem disto.

Nenhuma chave de API de tickets é colocada numa sandbox. Os agentes escrevem tickets através das ferramentas MCP do Control Center, que perguntam a Escrever num rastreador externo.

A regra Aceder à rede é lida uma única vez, quando a execução arranca. A rede de uma sandbox está ligada ou desligada, por isso negar desliga-a, enquanto permitir e perguntar primeiro a mantêm ligada; as ferramentas web_fetch e web_search do harness continuam a perguntar em cada chamada. As credenciais são revogadas quando a execução termina, juntamente com qualquer token de escrita que o gateway tenha emitido, para que uma credencial não sobreviva à execução.

O enum SandboxBackend tem três valores — native (Seatbelt no macOS, bubblewrap no Linux e no WSL2), none e microvm. O envolvimento de comandos de agentes usa apenas os dois primeiros: microvm é o backend de VM fechada para terminais interativos e rigs, não uma melhoria silenciosa de um invólucro de bash. Compreender a forma nativa explica o que ganha onde um está anexado e o que ele nunca lhe iria dar.

O cc_server sonda o anfitrião uma vez no arranque e, onde um backend responde, alimenta ambas as costuras que o caminho de despacho tem, porque os transportes não partilham uma. A primeira (SandboxPort) envolve a CLI do Claude Code. A segunda (SandboxManager) envolve os adaptadores ACP e a ferramenta bash do harness integrado. Alimentar apenas uma deixaria as outras a lançar sem invólucro, que é o que costumava acontecer. Num anfitrião sem backend, ou com --sandbox off, todas lançam sem invólucro e o servidor indica qual é o caso na sua linha de arranque. O ACP e o bash do harness continuam a sanear o ambiente; o Claude Code através do NoSandboxAdapter herda o ambiente do anfitrião.

Uma superfície exclui-se deliberadamente: o terminal alojado no servidor aceita um argumento de backend por sessão e o cc_server constrói-o com a shell do anfitrião por predefinição, pelo que um terminal que abra a partir da aplicação corre sem sandbox, a menos que um cliente indique explicitamente native. Isso é uma shell real no anfitrião do servidor, não uma execução de agente.

No Linux, o bubblewrap constrói um namespace de montagem, pelo que o agente vê genuinamente apenas o que foi associado lá dentro. No macOS, o sandbox-exec aplica um perfil Seatbelt que é permissivo por predefinição — um perfil totalmente deny-by-default torna uma shell interativa inutilizável, porque o número de chamadas dyld, XPC e Mach a enumerar não é prático. O perfil do macOS parte, por isso, de (allow default) e recorta negações: leituras de caminhos secretos, uma política de escrita reposta em negação com autorizações explícitas por cima, bloqueio de unlink em todos os caminhos negados e nos seus ascendentes, para que uma renomeação não consiga contornar uma negação de escrita, negações de execução para binários sempre perigosos e para tudo o que seja copiado para um diretório gravável e rede restrita às portas do proxy de loopback, mais o DNS.

A consequência prática: no Linux, a visão do sistema de ficheiros do agente é estreitada; no macOS, o agente ainda consegue ler a maior parte do anfitrião e os seus checkouts registados estão protegidos por negações explícitas de escrita e de renomeação, e não por invisibilidade. Ambas são isolamento ao nível do namespace, não uma fronteira de segurança do kernel — os próprios backends di-lo nas suas notas de capacidade.

O conjunto de montagens bind é onde se traça a fronteira entre agentes. O despacho overlay (Claude Code e ACP) monta até quatro caminhos, resolvidos por execução: o diretório de trabalho overlay do agente em leitura e escrita, o diretório de configuração do agente em leitura, o diretório repos/ partilhado da conversa em leitura e escrita quando existe, e o diretório de anexos do espaço em leitura quando existe. O diretório de um agente irmão nunca é montado. O invólucro de bash do harness é mais estreito: só monta o diretório de trabalho da execução. Esta é a mesma forma que a verificação de caminhos em processo do harness impõe por software, pelo que as duas concordam — mas só a verificação de caminhos cobre as próprias ferramentas de ficheiros do harness, que nunca chegam sequer a um namespace de montagem.

Os seus checkouts registados juntam-se ao conjunto de negação de escrita por cima disso, em todos os modos e nos três caminhos envolvidos (Claude Code, ACP, bash do harness), e cada regra é emitida tanto na sua grafia literal como na sua grafia resolvida por ligação simbólica. A segunda grafia não é um luxo: o kernel compara uma regra de negação com o caminho resolvido e o macOS faz da resolução o caso comum em vez do exótico — /tmp é na verdade /private/tmp, /var/folders/… é na verdade /private/var/folders/… —, pelo que um checkout alcançado por qualquer uma delas teria ficado gravável enquanto o perfil parecia proibi-lo.

A rede, sob uma sandbox, é uma lista de permissões de domínios por sessão imposta por proxies de loopback em processo. A saída é deny-by-default: com a rede desligada, os proxies ainda arrancam mas a sua lista de permissões está vazia, pelo que todas as ligações de saída são recusadas. Com ela ligada, o resolvedor constrói a lista de permissões a partir de uma base curada — fornecedores de API de LLM, alojamento de git, registos de pacotes, o catálogo de modelos — mais os domínios do GitHub, e compara o anfitrião de destino de cada ligação com ela. Os wildcards funcionam (*.example.com corresponde a subdomínios, não ao domínio de topo). Uma lista separada de sempre negados prevalece sobre a lista de permissões e abrange endpoints de metadados da cloud e destinos de telemetria. O que não é configurável é a própria lista: a especificação transporta um campo egressAllowlist para domínios extra, mas nada no caminho de despacho o preenche, pelo que, na prática, cada execução recebe a base mais o GitHub e nada mais.

As negações, no macOS, são observáveis: um monitor vigia o registo do sistema à procura de negações do Sandbox, filtra ruído conhecido, deduplica tempestades de repetições e apresenta cada uma como um SandboxViolation que nomeia o que foi tentado e o que foi bloqueado. No Linux, o caminho do invólucro analisa o stderr à procura de EPERM, Operation not permitted e Permission denied e emite o mesmo tipo de evento — não há vigilância do registo do sistema. Estas negações chegam ao próprio fluxo de eventos da execução, pelo que uma chamada de sistema negada aparece no transcript e não apenas num registo. A telemetria só existe enquanto uma sandbox nativa estiver de facto a envolver o processo — num anfitrião sem backend não há nada para observar. Também não tem relação com as barreiras: uma violação vem de uma chamada de sistema negada, nunca de uma consulta de regra.

As confirmações vêm das barreiras, não da sandbox

Secção intitulada “As confirmações vêm das barreiras, não da sandbox”

A sandbox não tem caminho de aprovação. Lança, executa, emite eventos e desmonta; não há lá nenhum sítio onde um humano possa responder a uma pergunta. Quando vê uma confirmação num espaço — aprovar este push, aprovar esta eliminação — foi levantada pelo motor de barreiras (ou pela política de comandos do harness) antes de a ação sequer chegar ao runtime.

Esta ordenação é deliberada. Uma aprovação que acontecesse depois da chamada de sistema seria um registo de auditoria, não um portão. É também por isso que a regra fail-closed vive nas barreiras: sem nenhum aprovador ligado, a decisão é tomada antes de tudo o resto correr e é uma negação.

Diagnosticar um agente é o único sítio que indica o que pode ser verificado manualmente.

  • Rigs e recintos: o passo acima da sandbox — quando o trabalho precisa de uma máquina descartável inteira com uma fronteira de kernel, e não de um processo envolvido
  • Barreiras: a camada de políticas acima do piso do sistema operativo e de onde vêm realmente as aprovações
  • Ciclo de vida do despacho de agentes: onde o isolamento se encaixa no fluxo de despacho
  • Modos: como um modo estreita a superfície de ferramentas e a política de comandos
  • Referência dos backends de sandbox: os backends e a sua configuração