Salta ai contenuti

Guardrail — una sola policy per ogni azione che modifica lo stato

Gli agenti in Control Center possono eliminare file, fare push di branch, pubblicare pull request e chiamare API esterne. Ognuna di queste è un diverso tipo di rischio, e per molto tempo solo una di esse — i comandi della shell — aveva una policy. I guardrail esistono per rispondere in modo uniforme a una sola domanda, per ogni azione che modifica lo stato e che un agente può compiere, qualunque sia il runtime o lo strumento che la propone: è consentita, richiede un’approvazione o viene negata?

Esiste esattamente un motore di policy. Viene consultato in ogni punto in cui un’azione può uscire dal sistema: il registro degli strumenti dell’harness integrato, il cancello del dispatcher MCP lato server e le mutazioni delle operazioni sui repo. Una regola scritta una volta vale quindi sia per il runtime integrato sia per le CLI degli agenti esterni — non esiste un secondo percorso, più silenzioso, che la aggiri. La vecchia policy dei comandi, valida solo per bash, funziona ancora: una regola può corrispondere a un prefisso di comando invece che a una classe di azione, e le due forme convivono nello stesso archivio.

Il motore, però, non sostituisce la sandbox del sistema operativo, e la sandbox non sostituisce il motore. Rispondono a domande diverse: i guardrail decidono se una chiamata che Control Center riesce a vedere può avvenire o meno, mentre la sandbox delimita fin dove può arrivare il processo che ne deriva. cc_server applica la sandbox dove l’host offre un backend — ma su Windows non ne esiste alcuno e Linux richiede bwrap e socat. Su quegli host i guardrail sono lo strato di applicazione più esterno, non una policy che poggia su una base: cosa utile da sapere quando decidi quanto essere permissivo.

Ogni strumento che modifica lo stato dichiara a quali ActionClasses appartiene, all’interno di un insieme chiuso di tredici classi di effetto. Cinque richiedono conferma fin da subito; le altre consentono, e solo una regola cambia le cose.

Classe Predefinito integrato Cosa copre
fileDelete prompt Eliminare un file (una scrittura a forma di eliminazione, rm)
gitPush prompt Fare push verso un remoto git
prCreate prompt Creare una pull request
prPublish prompt Pubblicare una revisione, unire una PR
vendorSyncWrite prompt Scrivere su un fornitore di ticket esterno (Linear, Jira, sincronizzazione GitHub)
fileWriteOutsideWorktree allow Scrivere un file fuori dal worktree isolato
gitCommit allow Creare un commit git
networkEgress allow Traffico di rete in uscita (web fetch, HTTP arbitrario)
secretAccess allow Leggere un segreto o una credenziale
packageInstall allow Installare un pacchetto (npm, pip, pub, brew)
processSpawn allow Avviare un processo (shell e tutto ciò che invoca una shell)
workspaceMutation allow Modificare la struttura dello spazio di lavoro (repo, spazi, agenti)
enclosureControl allow Pilotare un recinto (rig): avviare una VM, inviarle input

Le otto classi consentite per impostazione predefinita sono state scelte presupponendo che sotto di esse valga la base della sandbox. Vale dove l’host offre un backend, e non vale su Windows né su una macchina Linux priva di bwrap e socat — quindi se lì vuoi mettere fileWriteOutsideWorktree o packageInstall sotto una regola, quella regola devi scriverla.

L’insieme è chiuso di proposito. Un nuovo strumento che modifica lo stato e non dichiara alcuna classe non supera il test a cricchetto, quindi la tassonomia può crescere solo con una decisione esplicita e argomentata — «la proliferazione della tassonomia è la morte di questa funzione». Tredici classi bastano per esprimere le policy che la gente scrive davvero (non fare mai push senza chiedere, non eliminare mai fuori da un worktree, consentire sempre le letture) e sono abbastanza poche da mantenere comprensibile l’interfaccia delle impostazioni.

Una regola associa (scope, ActionClass | command prefix) → allow | prompt | deny. Gli ambiti si annidano dal più specifico al più generico:

spazio > agente > spazio di lavoro > preimpostazione della modalità > predefinito integrato

La risoluzione dà priorità alla specificità: il primo ambito con una regola corrispondente decide, e lì la risoluzione si ferma. La regola più restrittiva è solo uno spareggio — tra due regole ugualmente specifiche dentro uno stesso ambito e quando si combinano le varie classi dichiarate da una singola azione. Dentro un ambito, una regola sui comandi corrisponde al prefisso di comando più lungo. allow non batte deny da nessuna parte.

Due decisioni portano garanzie ferree:

  • Prompt chiusi per impostazione predefinita. Un prompt senza alcun approvatore collegato si risolve in negato. Una domanda senza risposta non è mai un sì.
  • Combinazione più restrittiva. Un’azione che attraversa più classi le combina nella forma più restrittiva; quando più classi richiedono conferma, un’unica conferma le elenca tutte.

Non esiste di proposito alcun blocco valido per un solo turno. Ogni chiamata a uno strumento risolve di nuovo la policy da zero, quindi le richieste ripetute per la stessa azione sono limitate dal «ricorda questa decisione» dell’operatore, non da qualcosa legato al turno.

Una regola può vincolare gli ARGOMENTI di un’azione, non solo il suo verbo. «Può fare push» e «può fare push verso feature/*» sono affermazioni diverse, e solo la seconda è un controllo — un controllo di capacità che autorizza soltanto il verbo è la forma dietro la maggior parte degli incidenti pubblicati sugli agenti.

Un vincolo può indicare:

  • percorsi — pattern glob (repos/**, !**/.env)
  • ref — ref git, con negazione ! (['**', '!main'] si legge come «qualsiasi branch tranne main»)
  • host — destinazioni di rete, con wildcard sui sottodomini (*.internal)
  • comandi — prefissi di comando, confrontati su un confine di parola
  • tetti — maxCount / maxCents, così «elimina fino a 50 file» è esprimibile

Tre proprietà derivano dal mantenere la grammatica chiusa e senza cicli, e tutte e tre sono il punto: ogni regola si legge come una frase che puoi rileggere, ogni rifiuto può indicare il vincolo che ha corrisposto, e l’intera policy può essere enumerata — puoi vedere cosa un agente può fare prima che venga eseguito.

Tre regole che contano:

  • Una regola vincolata è più specifica di una non vincolata nello stesso ambito, quindi «nega il push verso main» batte «consenti il push» senza che nessuno debba pensare all’ordine.
  • Una regola restrittiva (deny / prompt) si applica quando la richiesta tocca qualsiasi valore che essa indica; una permissiva (allow) si applica solo quando la richiesta è interamente al suo interno. Leggere le due allo stesso modo permetterebbe a un agente di riciclare un percorso proibito affiancandogli un percorso innocuo.
  • Una regola che indica una dimensione su cui la richiesta non dice nulla non viene né applicata né saltata: si trasforma in un’approvazione. Saltarla in silenzio renderebbe inerte ogni regola sui branch protetti di fronte a uno strumento che non dichiara il proprio ref; negare in silenzio rifiuterebbe ogni push che l’estrattore non riesce a descrivere. Senza un approvatore collegato, la regola preesistente di chiusura predefinita trasforma quell’escalation in un rifiuto.

Cosa può e non può intercettare una regola sui comandi

Sezione intitolata “Cosa può e non può intercettare una regola sui comandi”

Una regola commandPrefix corrisponde su un confine di parola: git push copre git push origin main e non git pushx. Non normalizza la shell. git push (due spazi), cd sub && git push e GIT_DIR=… git push sono stringhe diverse e nessuna di esse corrisponde a una regola su git push.

È un limite reale, e viene dichiarato invece che mascherato: le regole sui comandi sono una comodità per l’operatore che vuole modellare il comportamento ordinario di un agente, non un confine contro un agente ostile. I confini sono le classi di effetto (che uno strumento dichiara e non può riformulare), la rete di comandi della modalità sotto l’archivio delle policy e la sandbox del sistema operativo sotto entrambi. Una regola sui comandi che si risolve in allow non allenta nessuno dei due — la rete della modalità viene eseguita dopo e il suo deny è definitivo.

Rispondere a un prompt con «ricorda questa decisione» scrive una vera regola di policy: con ambito (spazio / agente / spazio di lavoro), vincolata agli argomenti (approvare un push verso feature/login concede feature/**, non main) e auto-revocante dopo un TTL esplicito. Dire sì per le prossime ore non è la stessa cosa che riscrivere in silenzio la tua policy permanente, e le regole così scritte finiscono nello stesso archivio con provenienza remembered e una scadenza, quindi possono essere revocate in anticipo.

Ogni regola ne porta uno:

  • advisory — consente, ma registra che la regola ha corrisposto. È il percorso di adozione: introduci una regola severa in modalità advisory, leggi il registro di audit per una settimana per vedere esattamente cosa avrebbe bloccato, poi promuovila. Senza questo livello, le policy severe non vengono mai attivate.
  • soft — nega, ma è sovrascrivibile da chi ha il permesso di sovrascrittura, con una giustificazione registrata. La sovrascrittura è essa stessa un evento sottoposto ad audit.
  • hard (il valore predefinito) — nega. Nulla la sovrascrive.

Chi gestisce un’installazione può fissare policy che nessun amministratore di spazio di lavoro può allentare. Le regole gestite risiedono sul lato server e vengono unite nella forma più restrittiva alla catena di ogni spazio di lavoro — non sono un ambito in cima a quella catena, perché un allow gestito in testa alla catena permetterebbe all’installazione di sovrascrivere un deny di uno spazio di lavoro, l’opposto di ciò a cui serve un limite. Una regola gestita può quindi solo irrigidire.

Impostare CC_SERVER_MANAGED_POLICY su un file JSON prevale del tutto sulle regole archiviate, ed è ciò che permette a un operatore di fissare un atteggiamento che nessuna interfaccia di amministrazione può ribaltare — la risposta a «posso impedire ai miei sviluppatori di disattivare i controlli di sicurezza?».

Il selettore dell’autonomia è un profilo, non un sistema parallelo

Sezione intitolata “Il selettore dell’autonomia è un profilo, non un sistema parallelo”

Il selettore dell’autonomia per spazio è un profilo con un nome sopra questo stesso archivio delle policy, non un secondo meccanismo. Le sue tre impostazioni vengono archiviate e inviate come proposeOnly, actWithApproval e actFreely (le forme con il trattino sono solo prosa dell’interfaccia e autonomy.setForSpace rifiuta qualsiasi altro valore). Se non viene impostato, si comporta come actWithApproval.

Che effetto ha ciascuna impostazione su una decisione risolta:

  • proposeOnly nega senz’altro ogni strumento soggetto a controllo. L’agente può ancora ragionare e rispondere; semplicemente non può agire, e il suo messaggio spiega perché.
  • actWithApproval (e non impostato) è il cancello di approvazione a chiusura predefinita descritto sopra.
  • actFreely pre-approva tutto ciò che non si è risolto in un deny netto.

Quest’ultima merita di essere detta chiaramente, senza ammorbidirla: con actFreely una decisione prompt non viene inoltrata a un approvatore — è consentita e nessuno viene interpellato. Solo una regola deny esplicita sopravvive al selettore. Quindi actFreely è una concessione deliberata di autonomia, non un’impostazione di comodo, e la regola di chiusura predefinita protegge esattamente il caso in cui conta (actWithApproval, il valore predefinito).

La delega tra agenti è sorvegliata nel punto di controllo delegate_task da tutte e quattro le guardie: un tetto di profondità (3 per impostazione predefinita), il rilevamento dei cicli, un tetto di autonomia (un delegato non può mai agire con più autonomia del suo delegante — il privilegio non si ricicla affidando il lavoro a un agente più libero) e un perimetro di budget (la delega addebita il budget residuo del delegante e non può crearne altro). Ognuna viene rifiutata a voce alta, con il motivo della guardia restituito all’agente parola per parola.

Non tutti i runtime permettono a Control Center di intercettare ogni azione. Le CLI degli agenti esterni eseguono alcuni strumenti in modo nativo, nel processo, dove non può esserci alcun controllo. Anziché fingere il contrario, ogni adapter porta con sé una matrice di onestà: per ogni adapter, se Control Center filtra la superficie degli strumenti, intercetta le chiamate agli strumenti, osserva il contratto di completamento, riesce a vedere gli strumenti nativi dell’esecutore e se gli strumenti nel processo sono coperti da un profilo sandbox.

Due voci meritano una lettura prima che ti fidi di una configurazione. L’harness integrato dichiara che i suoi strumenti per i file nel processo non sono isolati nella sandbox — la superficie degli strumenti e questo motore di policy sono l’unico confine del filesystem che ha. Claude Code dichiara che le sue stesse chiamate agli strumenti non sono intercettabili, perché viene avviato con --dangerously-skip-permissions; Control Center vede le sue chiamate mcp__* e, tramite un hook, le sue righe di comando Bash, mentre i suoi strumenti di lettura, scrittura e modifica vengono eseguiti senza essere visti.

La schermata delle impostazioni mostra questa matrice, perché l’unico fallimento imperdonabile per un sistema di guardrail è dichiarare una copertura che non esiste.

I guardrail si trovano in Impostazioni → Spazio di lavoro → Autorizzazioni degli agenti (/workspaces/<id>/settings/workspace/permissions). La schermata contiene la matrice delle policy in cui si scrivono le regole, i modelli di policy, una sonda del tipo «E se?», il registro di controllo delle autorizzazioni e la matrice di onestà degli adapter.

La sonda accetta una classe di azione (o un comando) e un ambito e mostra quale regola ha deciso e perché. Poiché la risoluzione è pura e deterministica — stessa azione, stesso ambito, stessa decisione — la risposta della sonda è la risposta che riceverà l’agente.