Salta ai contenuti

Sandbox e sicurezza

Un agente legge file, esegue comandi shell e chiama API esterne per tuo conto. Il guasto che ti preoccupa raramente è la malizia — è un agente che fraintende un percorso e riscrive il tuo checkout di lavoro, o che esegue il push di un branch che non avevi finito di revisionare. Quindi la domanda a cui il prodotto deve rispondere non è «l’agente è affidabile?» ma «qual è il raggio d’impatto quando sbaglia?».

Control Center risponde a questa domanda per strati, e gli strati sono indipendenti di proposito. Se ne manca uno, gli altri tengono comunque. Questo conta, perché lo strato più esterno — il sandbox del sistema operativo — è l’unico condizionale: dipende dal fatto che l’host offra un backend, e su Windows nessun host lo fa.

Cinque cose delimitano un’esecuzione di agente su ogni host, con o senza sandbox. Nessuna di esse è un confine del kernel; tutte sono reali. Dove l’host offre un backend, il sandbox del sistema operativo si colloca sopra di esse e la sezione «Cosa aggiunge il sandbox e dove si ferma» più avanti spiega cosa cambia.

Il suo worktree copy-on-write. Ogni conversazione predispone un proprio checkout di ciascun repository collegato, clonato in copy-on-write tramite il native rift incluso anziché copiato byte per byte, così l’isolamento è abbastanza economico da essere il comportamento predefinito. Il repository registrato che hai aggiunto allo spazio di lavoro non è la directory in cui lavora l’agente — due conversazioni che toccano lo stesso repository non possono vedere le modifiche l’una dell’altra e nessuna delle due sta modificando il tuo checkout.

Vale la pena precisare la granularità: il worktree è per conversazione, non per agente. Due agenti nella stessa conversazione ricevono directory di lavoro overlay distinte che condividono la repos/ di quella conversazione — che è esattamente il punto, dato che stanno collaborando a una sola modifica — mentre le loro directory private di agente restano separate. Il copy-on-write è l’unico backend ovunque rift sia distribuito: un filesystem che non sa fare reflink, o una directory dati su un volume diverso da quello del repository, fa fallire la predisposizione anziché ripiegare. Windows è l’eccezione — lì non esiste alcun backend copy-on-write, quindi il backend è git worktree, non un degrado silenzioso.

Il confinamento dei percorsi negli strumenti per i file dell’harness integrato. Gli strumenti read, write, edit, apply_patch, search, find e search_files dell’harness risolvono ogni percorso rispetto alla directory di lavoro dell’esecuzione più le radici condivise — la directory repos/ della conversazione, dove vivono i worktree, e la directory degli allegati dello spazio quando esiste — e rifiutano qualsiasi cosa che finisca all’esterno. I percorsi assoluti sono accettati solo se erano già all’interno. Anche un symlink dentro il worktree che punta fuori viene risolto e rifiutato. Ecco perché un agente non può fare write su ~/.ssh anche senza alcun sandbox in gioco.

La superficie degli strumenti della modalità. La modalità di una conversazione decide quali strumenti vengono materializzati per l’esecuzione. Nelle modalità di sola lettura (plan, review, orchestrate) gli strumenti mutanti semplicemente non sono nel registro e la policy dei comandi shell per quelle modalità nega del tutto ogni comando mutante — git commit, git push, npm install, docker e gli altri — anziché chiedere conferma. Uno strumento che non è presente non può essere chiamato, il che è una garanzia più forte di uno strumento presente e vincolato.

I guardrail unificati delle azioni. Ogni strumento mutante dichiara le classi di effetto che può produrre e il motore di policy risolve ciascuna in allow, prompt o deny prima che la chiamata venga eseguita. Un prompt senza alcun approvatore collegato si risolve in denied — una domanda a cui nessuno può rispondere non è mai un sì. Vedi Guardrail per il modello di risoluzione.

Credenziali del broker e ambiente ripulito. Le credenziali vengono coniate a ogni avvio e revocate alla fine dell’esecuzione, e il token GitHub di un’esecuzione può leggere ma non eseguire push — vedi «I push passano da Control Center» più sotto. ACP, il bash dell’harness e Claude Code sotto l’incapsulamento nativo ricevono un ambiente ripulito dai vettori di iniezione del linker dinamico e dell’interprete (LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS, GIT_SSH_COMMAND e simili) prima che il processo parta. Claude Code tramite NoSandboxAdapter eredita l’ambiente dell’host.

Gli strumenti per i file dell’harness vengono eseguiti nel processo del server, non in un processo figlio, quindi nessun profilo di sandbox potrebbe vincolarli anche se ne fosse applicato uno — solo bash passa dall’esecutore di comandi nel sandbox. Il loro confinamento è il controllo dei percorsi descritto sopra, e quel controllo è quindi portante anziché una seconda linea di difesa.

Lo strumento bash è l’altro punto in cui essere precisi. I suoi comandi vengono valutati rispetto alla policy dei comandi della modalità e poi avviati con un ambiente ripulito e il worktree come directory di lavoro. Dove esiste un backend, quell’avvio è incapsulato, quindi è il sandbox che impedisce a un comando shell di scrivere fuori dal worktree; dove non ne esiste alcuno — Windows, o Linux senza bwrap e socat — la policy dei comandi è l’unica cosa tra il comando e il resto del disco. In modalità chat la policy nega in modo rigido un elenco fisso (escalation di privilegi, distruzione di sistema e disco, fuga da container e namespace) e chiede conferma per le mutazioni dello stato remoto (git push, gh pr create, pubblicazioni e installazioni di pacchetti). Qualsiasi cosa non presente in nessuno dei due elenchi viene eseguita. Le pipeline, le catene e le subshell vengono analizzate e ciascun sotto-comando valutato separatamente, con la decisione più restrittiva che vince.

Claude Code, quando è usato come adapter, è il caso in cui il sandbox porta il peso maggiore. Control Center lo avvia con --dangerously-skip-permissions, perché un claude -p non interattivo si bloccherebbe altrimenti per sempre sul proprio prompt di approvazione. Le sue chiamate di lettura, scrittura e modifica avvengono dentro il suo processo e Control Center non le vede mai. Le sue chiamate Bash vengono inviate, prima dell’esecuzione, a un hook sul server che classifica la riga di comando e interroga la regola corrispondente, e gli strumenti mcp__* serviti da Control Center passano dal varco come sempre. Dove esiste un backend, quelle chiamate invisibili avvengono comunque dentro l’incapsulamento del sandbox, quindi il sistema operativo è il confine che le circonda. Dove non ne esiste alcuno, non c’è nulla. Ogni adapter lo dichiara nella propria matrice di applicazione — vedi la matrice di onestà degli adapter.

Non esistono flag di permesso per agente. Se un agente può creare un commit, eseguire un push, aprire una pull request, scrivere su un tracker o raggiungere la rete lo decidono i guardrail — consenti, chiedi prima o nega — e le stesse regole valgono qualunque sia l’adapter che esegue l’agente.

Il push è l’azione che deve reggere anche contro un agente che prova ad aggirare una regola, quindi viene applicato in base a ciò che l’agente possiede, non a ciò che esegue. Con una GitHub App configurata, un’esecuzione in un worktree GitHub riceve un token di installazione limitato al repo che può leggere i contenuti e lavorare con le pull request ma non eseguire push. La configurazione git dell’esecuzione riscrive ogni URL di push GitHub (https://github.com/, git@github.com:, ssh://git@github.com/) verso il gateway delle esecuzioni degli agenti, un endpoint di loopback su cc_server. Il gateway legge quali ref il push aggiorna, interroga la regola Eseguire il push a un remoto — quindi si applica anche una regola limitata ai branch, come «nega i push su main» — e solo allora inoltra il push a GitHub con un token di scrittura che conia apposta e non consegna mai all’agente. Un push rifiutato compare nell’output di git stesso:

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

Il gateway inoltra i push solo al repo del worktree o a un repo GitHub collegato allo spazio di lavoro. I fetch vanno direttamente a GitHub con il token di lettura.

Gli altri comandi di shell incontrano un varco più morbido. Lo strumento bash dell’harness e, tramite un hook, lo strumento Bash di Claude Code classificano ogni riga di comando e interrogano la regola a cui appartiene: git commit, merge, rebase, cherry-pick, revert e am sono Creare un commit; gh pr create è Aprire una pull request; gh pr merge, review, comment e close sono Pubblicare una revisione o unire; gh issue create, edit, comment e close sono Scrivere su un tracker esterno; le installazioni di pacchetti sono Installare un pacchetto; curl e wget sono Accedere alla rete. Questo riconosce il modo convenzionale di scrivere un’azione, e un agente deciso a nasconderne una (eval, un file di script, una chiamata API diretta) ci riesce. I push non dipendono da questo.

In un sandbox non viene messa alcuna chiave API di ticketing. Gli agenti scrivono i ticket tramite gli strumenti MCP di Control Center, che interrogano Scrivere su un tracker esterno.

La regola Accedere alla rete viene letta una sola volta, all’avvio dell’esecuzione. La rete di un sandbox è attiva o disattivata, quindi nega la disattiva, mentre consenti e chiedi prima la lasciano attiva; gli strumenti web_fetch e web_search dell’harness continuano a chiedere a ogni chiamata. Le credenziali vengono revocate quando l’esecuzione viene smantellata, insieme a ogni token di scrittura coniato dal gateway, quindi un handle non sopravvive all’esecuzione.

L’enum SandboxBackend ha tre valori — native (Seatbelt su macOS, bubblewrap su Linux e WSL2), none e microvm. L’incapsulamento dei comandi dell’agente usa solo i primi due: microvm è il backend a VM isolata per terminali interattivi e rig, non un aggiornamento silenzioso di un incapsulamento di bash. Capire la forma del native spiega cosa guadagni dove ne è applicato uno e cosa non ti avrebbe mai dato.

cc_server sonda l’host una volta all’avvio e, dove un backend risponde, alimenta entrambi i punti di innesto del percorso di invio, perché i trasporti non ne condividono uno. Il primo (SandboxPort) incapsula la CLI di Claude Code. Il secondo (SandboxManager) incapsula gli adapter ACP e lo strumento bash dell’harness integrato. Alimentarne solo uno lascerebbe gli altri ad avviarsi senza incapsulamento, che è quello che succedeva prima. Su un host senza backend, o con --sandbox off, si avviano tutti senza incapsulamento e il server dice in quale caso ti trovi sulla propria riga di avvio. ACP e il bash dell’harness ripuliscono comunque l’ambiente; Claude Code tramite NoSandboxAdapter eredita l’ambiente dell’host.

Una superficie si esclude deliberatamente: il terminale ospitato sul server accetta un argomento di backend per sessione e cc_server lo costruisce con il valore predefinito della shell dell’host, quindi un terminale che apri dall’app viene eseguito senza sandbox a meno che un client non indichi esplicitamente native. Quella è una vera shell sull’host del server, non un’esecuzione di agente.

Su Linux, bubblewrap costruisce un namespace di mount, quindi l’agente vede davvero solo ciò che è stato montato al suo interno. Su macOS, sandbox-exec applica un profilo Seatbelt permissivo per impostazione predefinita — un profilo completamente deny-by-default rende inutilizzabile una shell interattiva, perché il numero di chiamate dyld, XPC e Mach da enumerare è impraticabile. Il profilo macOS parte quindi da (allow default) e ritaglia dei dinieghi: letture di percorsi segreti, una policy di scrittura riportata a deny con allow espliciti in aggiunta, blocco di unlink su ogni percorso negato e sui suoi antenati così che una rinomina non possa aggirare un diniego di scrittura, dinieghi di exec per binari sempre pericolosi e per qualsiasi cosa copiata in una directory scrivibile, e rete limitata alle porte proxy di loopback più il DNS.

La conseguenza pratica: su Linux la visione del filesystem da parte dell’agente è ristretta; su macOS l’agente può ancora leggere gran parte dell’host e i tuoi checkout registrati sono protetti da dinieghi espliciti di scrittura e rinomina anziché dall’invisibilità. Entrambi sono isolamento a livello di namespace, non un confine di sicurezza del kernel — i backend lo dichiarano nelle proprie note sulle capacità.

Il set di bind mount è dove viene tracciato il confine tra agenti. L’invio overlay (Claude Code e ACP) monta fino a quattro percorsi, risolti per esecuzione: la directory di lavoro overlay dell’agente in lettura-scrittura, la directory di configurazione dell’agente in sola lettura, la directory repos/ condivisa della conversazione in lettura-scrittura quando esiste, e la directory degli allegati dello spazio in sola lettura quando esiste. La directory di un agente fratello non viene mai montata. L’incapsulamento di bash dell’harness è più ristretto: monta in bind solo la directory di lavoro dell’esecuzione. Questa è la stessa forma che il controllo dei percorsi in-process dell’harness applica via software, quindi i due concordano — ma solo il controllo dei percorsi copre gli strumenti per i file dell’harness stesso, che non raggiungono mai un namespace di mount.

I tuoi checkout registrati si aggiungono all’insieme di diniego della scrittura su tutto questo, in ogni modalità e su tutti e tre i percorsi incapsulati (Claude Code, ACP, bash dell’harness), e ogni regola viene emessa sia nella sua grafia letterale sia in quella risolta dai symlink. La seconda grafia non è un vezzo: il kernel confronta una regola di diniego con il percorso risolto e macOS rende la risoluzione il caso comune anziché quello esotico — /tmp in realtà è /private/tmp, /var/folders/… in realtà è /private/var/folders/… — quindi un checkout raggiunto attraverso l’una o l’altra sarebbe stato scrivibile mentre il profilo sembrava proibirlo.

La rete, sotto un sandbox, è una lista di autorizzazione per dominio per sessione, applicata da proxy di loopback in-process. L’uscita è negata per impostazione predefinita: con la rete disattivata, i proxy si avviano comunque ma la loro lista di autorizzazione è vuota, quindi ogni connessione in uscita viene rifiutata. Con la rete attiva, il resolver costruisce la lista di autorizzazione da una base di riferimento curata — provider di API LLM, hosting git, registry dei pacchetti, il catalogo dei modelli — più i domini di GitHub, e confronta con essa l’host di destinazione di ogni connessione. I caratteri jolly funzionano (*.example.com corrisponde ai sottodomini, non al dominio radice). Un elenco separato, sempre negato, prevale sulla lista di autorizzazione e copre gli endpoint di metadati cloud e le destinazioni di telemetria. Ciò che non è configurabile è la lista stessa: la specifica porta un campo egressAllowlist per domini aggiuntivi, ma nulla nel percorso di invio lo popola, quindi in pratica ogni esecuzione ottiene la base di riferimento più GitHub e nulla di più.

Le negazioni, su macOS, sono osservabili: un monitor intercetta il log di sistema alla ricerca dei dinieghi del Sandbox, filtra il rumore noto, deduplica le tempeste di tentativi e le presenta ciascuna come una SandboxViolation che nomina ciò che è stato tentato e ciò che è stato bloccato. Su Linux il percorso di incapsulamento analizza stderr alla ricerca di EPERM, Operation not permitted e Permission denied ed emette lo stesso tipo di evento — non c’è alcuna intercettazione del log di sistema. Quelli arrivano sul flusso di eventi dell’esecuzione stessa, quindi una syscall negata compare nella trascrizione anziché solo in un log. La telemetria esiste solo mentre un sandbox nativo sta effettivamente incapsulando il processo — su un host senza backend non c’è nulla da osservare. È anche estranea ai guardrail: una violazione deriva da una syscall negata, mai dalla consultazione di una regola.

Le conferme vengono dai guardrail, non dal sandbox

Sezione intitolata “Le conferme vengono dai guardrail, non dal sandbox”

Il sandbox non ha alcun percorso di approvazione. Si avvia, esegue, emette eventi e si smantella; non c’è posto al suo interno perché un umano risponda a una domanda. Quando vedi una conferma in uno spazio — approva questo push, approva questa eliminazione — è stata sollevata dal motore dei guardrail (o dalla policy dei comandi dell’harness) prima che l’azione raggiungesse del tutto il runtime.

Questo ordine è deliberato. Un’approvazione che avvenisse dopo la syscall sarebbe un record di controllo, non un varco. È anche il motivo per cui la regola fail-closed vive nei guardrail: senza alcun approvatore collegato, la decisione viene presa prima che qualsiasi cosa venga eseguita ed è un diniego.

Esegui la diagnostica di un agente è l’unico posto che dichiara cosa si può controllare a mano.

  • Rig e recinti: il gradino sopra il sandbox — quando il lavoro richiede un’intera macchina usa e getta con un confine del kernel, non un processo incapsulato
  • Guardrail: lo strato di policy sopra il pavimento del sistema operativo e da dove vengono davvero le approvazioni
  • Ciclo di vita dell’invio degli agenti: dove si colloca l’isolamento nel flusso di invio
  • Modalità: come una modalità restringe la superficie degli strumenti e la policy dei comandi
  • Riferimento sui backend del sandbox: i backend e la loro configurazione