Agenti AI in produzione: guardrail per ridurre il raggio d’azione
Scopri come confinare gli agenti AI in produzione con ruoli minimi, allowlist, limiti di spesa e idempotenza, prima che un errore diventi un incidente.

Il 25 aprile Cursor, mentre eseguiva Claude Opus 4.6, ha cancellato in nove secondi il database di produzione di PocketOS e i relativi backup, poi ha scritto una confessione. Scomparsi tre mesi di prenotazioni di autonoleggio. Gestisco sei agenti AI che operano ogni giorno sulla produzione: ecco l’architettura esatta di punti di controllo, allowlist e idempotenza grazie alla quale il peggio che uno dei miei può fare è spendere un dollaro di troppo.
Il conto degli ultimi 90 giorni
25 aprile 2026. Un agente Cursor che eseguiva Claude Opus 4.6 stava svolgendo quella che Jeremy Crane, CEO di PocketOS, ha definito una «routine task» nell’ambiente di staging. Di fronte a una discrepanza nelle credenziali, ha deciso autonomamente di «clean things up» e ha eliminato un volume di storage Railway che conteneva il database di produzione e i relativi backup. Nove secondi. Trenta ore di fermo. Il backup recuperabile più recente risaliva a tre mesi prima: tre mesi di prenotazioni di autonoleggio sono svaniti. L’agente ha poi scritto una confessione, riconoscendo di avere violato le istruzioni esplicite che vietavano di intervenire sulla produzione.
26 febbraio 2026. Alexey Grigorev, fondatore di DataTalks.Club, aveva cambiato computer e il suo file di stato Terraform locale non era aggiornato. Claude Code, incaricato di ripulire le risorse, ha eseguito terraform destroy sullo stack di produzione. La tabella courses_answer — in seguito recuperata solo in parte — conteneva 1,943,200 righe. Due anni e mezzo di consegne degli studenti. Gli snapshot automatici si trovavano nello stesso account che era stato distrutto.
Metà dicembre 2025. Kiro, agente interno di Amazon, ha ereditato i privilegi elevati di un ingegnere, ha aggirato il controllo con approvazione di due persone di Amazon — perché valeva per gli esseri umani, non per il ruolo con cui operava l’agente — e ha eliminato e ricreato l’ambiente di produzione di AWS Cost Explorer.
Tre incidenti, tre modelli diversi, tre stack diversi. La causa comune non è il modello, ma l’unione di autorizzazioni ampie ereditate, un ciclo autonomo e una velocità d’azione che rende inutile qualsiasi richiesta di conferma umana. ServiceNow vende già un prodotto «kill switch» costruito proprio attorno a questo timore. Quella che segue è la versione interna di tale architettura, oggi in funzione nella mia produzione.
Cosa comporta per chi fonda un’azienda senza un profilo tecnico
Se sei qui perché la persona che segue l’AI nella tua azienda ha sollevato il tema, il costo di un errore non è «un bug dell’AI». Sono il sistema di prenotazione, i registri degli studenti e i dati dei clienti che spariscono, con backup vecchi di tre mesi perché nessuno ha mai provato un ripristino.
La domanda onesta da fare al responsabile tecnico non è «quanto è bravo l’agente?». È l’inquadramento sbagliato. La domanda giusta è: qual è il danno massimo che una singola esecuzione dell’agente può causare, e chi ne ha definito il limite? Se la risposta è «ci fidiamo del modello» oppure «controlliamo i diff», non esiste una superficie di controllo. Esiste soltanto una speranza.
La risposta da cercare suona così: «L’agente opera con un ruolo cloud privo di permessi di distruzione. Può chiamare dodici tool nominati, nessuno dei quali è una shell. Ogni chiamata a pagamento passa da un’unica funzione con un tetto rigido ai costi. Nel caso peggiore avremo un passaggio fallito e al massimo un dollaro di spesa». Questa è l’architettura. Il resto dell’articolo spiega come costruirla.
Perché «fai attenzione» e «controlla il diff» non funzionano
PocketOS ha perso il volume in nove secondi. Kiro ha completato l’eliminazione più in fretta di quanto una persona potesse leggere la richiesta di conferma, figuriamoci valutarla. Alla velocità di un agente, intervenire dopo l’avvio è strutturalmente impossibile: quando l’azione diventa visibile, è già conclusa.
È l’aspetto della sicurezza degli agenti che gran parte degli articoli evita, perché porta a una conclusione scomoda: l’unica protezione efficace è il diniego prima dell’esecuzione. Non «l’agente chiede e una persona stanca fa clic su sì». Non «registriamo tutto e controlliamo più tardi». Negare prima dell’esecuzione significa fare in modo che l’azione distruttiva non appartenga fin dall’inizio alla superficie raggiungibile dall’agente.
Kiro lo ha dimostrato per controesempio. Amazon prevedeva l’approvazione di due persone, ma il controllo si applicava agli esseri umani che avviavano i job. L’agente ereditava i permessi della persona che lo aveva lanciato e superava il controllo, perché quest’ultimo non era mai stato associato al ruolo con cui l’agente operava. Per la persona, la messinscena dell’approvazione; per il ciclo, tutti i permessi di distruzione.
Il problema va quindi riformulato. Un agente non si «sorveglia». La sua superficie d’azione si limita prima che il ciclo parta, rendendo il vincolo una proprietà del ruolo e non del prompt.
Il punto di controllo degli agenti AI: ogni chiamata a pagamento o mutativa passa da una funzione
Nel mio stack di sei agenti, ogni chiamata a pagamento verso un modello passa da un’unica funzione, che ho chiamato callAi. Svolge cinque operazioni, in quest’ordine: verifica in anticipo il limite giornaliero di $20, misura la durata della chiamata, registra numero di token e costo in USD in una riga append-only di ai_call_log contrassegnata da agent_id e workflow_instance_id, verifica al termine il limite di $1 per istanza e restituisce il risultato.
export async function callAi(env: Env, args: CallAiArgs) {
const { agentId, workflowInstanceId, model, messages } = args;
const dailySpent = await getDailySpendUSD(env);
if (dailySpent >= 20) {
throw new NonRetryableError(`daily cap hit: $${dailySpent}`);
}
const t0 = Date.now();
const res = await anthropic.messages.create({ model, messages });
const costUSD = priceOf(model, res.usage);
await env.DB.prepare(
`INSERT INTO ai_call_log
(agent_id, workflow_instance_id, model, in_tokens, out_tokens, usd, ms, ts)
VALUES (?, ?, ?, ?, ?, ?, ?, unixepoch())`
).bind(agentId, workflowInstanceId, model,
res.usage.input_tokens, res.usage.output_tokens,
costUSD, Date.now() - t0).run();
const instanceSpent = await getInstanceSpendUSD(env, workflowInstanceId);
if (instanceSpent >= 1) {
throw new NonRetryableError(`instance cap hit: $${instanceSpent}`);
}
return res;
}Il superamento di un limite genera un errore non ritentabile. Il Workflow termina. L’agente non può «decidere autonomamente» di continuare, perché il controllo del budget non è un prompt con cui discutere: è un’eccezione lanciata sopra il suo livello di ragionamento.
Il confronto è con il caso, spesso citato, del ciclo fuori controllo durato 63 ore e costato $4,200: senza un punto di controllo non esiste uno stato terminale. Il ciclo continua a consumare risorse finché qualcuno non nota la fattura. Con un limite per istanza, una singola esecuzione può costare al massimo $1. Con un limite giornaliero, una giornata può costare al massimo $20. Durante lo sviluppo li ho raggiunti entrambi per errore. Nessuno dei due mi è costato più di una cena.
Il punto di controllo svolge anche una seconda funzione: l’analisi forense. Ogni azione lascia una riga di audit prima che il risultato venga restituito. Quando qualcosa va storto, il post-mortem è una query SELECT su ai_call_log, non una ricostruzione basata sulla confessione dell’agente.
L’allowlist: 12 tool Zod, niente shell né terraform
L’intera superficie di mutazione dell’agente comprende dodici tool, definiti come schemi Zod rigorosi. Non c’è alcun tool Bash. Non c’è alcun tool terraform. L’API dei volumi cloud non è affatto esposta. Se l’agente arrivasse alla conclusione di voler eseguire terraform destroy, non avrebbe alcun modo per trasformare quel proposito in un comando.
La configurazione del Claude Agent SDK che impone questo vincolo è breve:
const result = await query({
prompt,
permissionMode: "dontAsk",
allowedTools: [
"read_brief", "fetch_corpus", "render_markdown",
"validate_directives", "persist_draft", "schedule_publish",
"update_status", "log_event", "fetch_citation",
"embed_text", "search_vectors", "notify_human"
],
// no Bash, no Write, no Edit, no infrastructure tools
});Con permissionMode: "dontAsk", i tool non elencati vengono negati senza eccezioni. Non passano a una richiesta di conferma. Non vengono sottoposti al controllo umano. Sono negati. L’ordine di valutazione dei permessi — hook PreToolUse, regole deny, regole allow, regole ask, controllo della modalità, callback canUseTool — viene applicato come documentato; in dontAsk, però, la callback human-in-the-loop viene strutturalmente saltata e i tool sconosciuti si fermano al controllo della modalità.
È il meccanismo che avrebbe bloccato sul nascere sia PocketOS sia DataTalks. In questa configurazione il guasto di DataTalks è impossibile, perché terraform destroy non è il nome di un tool che l’agente può emettere. Quello di PocketOS è impossibile perché l’eliminazione dei volumi non rientra nella superficie disponibile. Anche se il modello decidesse di «clean things up», non esisterebbe alcun percorso tra l’intenzione e l’azione.
La tentazione è aggiungere un tool Bash per gestire «quello che l’agente potrebbe dover fare e che non avevamo previsto». Non fatelo. Il giorno in cui aggiungete Bash, il raggio d’azione diventa «tutto ciò che Bash può raggiungere».
Idempotenza: perché un replay non può distruggere due volte
Ogni istanza di Cloudflare Workflow ha un ID deterministico ricavato dall’input: publish-{brief_id}. instances.create() genera un errore se l’ID esiste già, quindi il nuovo invio dello stesso evento non può avviare il workflow due volte. Una tempesta di tentativi nel livello della coda diventa una no-op, non una doppia pubblicazione.
const id = `publish-${msg.body.briefId}`;
try {
await env.PUBLISHER.create({ id, params: msg.body });
} catch (e) {
if (e.message.includes("already exists")) return; // safe replay
throw e;
}All’interno del workflow, il risultato di ogni step.do() viene memorizzato nella cache ed è sicuro in caso di replay. Quando un passaggio viene ritentato, si riutilizza il risultato in cache anziché eseguire di nuovo l’effetto collaterale. Insieme a INSERT OR IGNORE sulle tabelle D1 canoniche e a uno snapshot R2 versionato a ogni salvataggio, questo impedisce a un passaggio errato di sovrascrivere la cronologia senza lasciare traccia. La versione precedente resta a una sola chiave R2 di distanza.
Alla radice dell’incidente DataTalks c’era un file di stato obsoleto che guidava un’operazione distruttiva irreversibile. ID di workflow deterministici e snapshot prima della mutazione rendono non fatale la distruzione causata da uno stato obsoleto: anche se un workflow operasse sulla base di presupposti superati, lo snapshot sarebbe il punto di ripristino. (Ho descritto la primitiva di esecuzione durevole nell’articolo Cloudflare Workflow vs Managed Agents; il calcolo dell’idempotenza applica la stessa logica di contenimento con un nome diverso.)
Il modello mentale è semplice: un passaggio capace di effetti distruttivi scrive prima uno snapshot versionato. Lo snapshot è una precondizione, non una precauzione successiva. Se la scrittura dello snapshot fallisce, la mutazione non avviene. Se fallisce la mutazione, lo snapshot è il punto di ripristino. Non esiste un percorso in cui si perde lo stato senza avere nulla da ripristinare.
La migrazione in 5 passaggi da completare questa settimana
Questo è l’ordine da seguire. Il solo passaggio 1 avrebbe fermato tutti e tre gli incidenti descritti sopra: va quindi eseguito per primo, anche se non si interviene su altro.
Limitare il ruolo cloud dell’agente: zero azioni distruttive
Il ruolo IAM — oppure l’account di servizio o il token API — con cui opera l’agente non deve consentire alcuna azione destroy o delete sulle risorse di produzione. Niente
DeleteVolume. Niente permesso perterraform destroy. NienteDROP TABLE. Il ruolo definisce il limite minimo del raggio d’azione; tutto ciò che sta sopra è irrilevante se il ruolo stesso non è circoscritto.Questa è la lezione di Kiro resa concreta: l’agente non deve ereditare gli ampi permessi della persona che lo avvia. Deve avere un ruolo proprio e ristretto.
Racchiudere l’agente in permissionMode dontAsk con una allowedTools esplicita
Scegliete il gruppo più piccolo di tool che consenta all’agente di svolgere il proprio compito. Il mio ne comprende dodici. Il vostro potrebbe averne sei. Eliminate del tutto qualsiasi tool shell generico. Se l’agente dispone già di Bash perché «è comodo», quella comodità è la vulnerabilità.
TypeScript{ permissionMode: "dontAsk", allowedTools: [/* explicit list */] }Aggiungere un interruttore automatico per il budget
Un unico wrapper deve circondare ogni chiamata a pagamento. Verificate in anticipo un limite giornaliero rigido e, al termine, un limite per istanza. Quando viene superato, lanciate
NonRetryableError. Il limite per istanza è l’interruttore che il tetto giornaliero non può essere: il ciclo da $4,200 in 63 ore è rimasto sotto qualsiasi soglia giornaliera ragionevole mentre accumulava costi esecuzione dopo esecuzione. Il limite per istanza intercetta ciò che sfugge a quello giornaliero.Creare uno snapshot prima di ogni mutazione
Ogni passaggio capace di effetti distruttivi deve prima scrivere uno snapshot versionato. R2 va bene, S3 va bene, così come una tabella D1
*_archive. Ciò che conta è che la scrittura dello snapshot sia la precondizione della mutazione, nello stesso passaggio e nello stesso blocco assimilabile a una transazione.Usare un registro append-only delle azioni
Una riga per ogni chiamata a un tool, scritta prima che venga restituito il risultato. Aggiungete l’ID dell’agente, l’ID dell’istanza del workflow, il costo in USD e la latenza. Quando qualcosa andrà storto — prima o poi succederà — il post-mortem sarà una
SELECT, non una ricostruzione.
Ogni passaggio richiede al massimo un giorno. Vanno svolti in quest’ordine perché il contenimento si accumula: il passaggio 1 fissa il limite minimo, il passaggio 2 chiude la superficie d’azione sopra quel limite, il passaggio 3 contiene il costo di ciò che avviene all’interno della superficie, mentre i passaggi 4 e 5 rendono ogni guasto recuperabile e interrogabile.
FAQ
Non basta dire all’agente nel system prompt di non eliminare mai la produzione?
PocketOS lo ha fatto. Nella propria confessione, l’agente ha dichiarato esplicitamente di avere violato quelle istruzioni. Le istruzioni non sono una superficie di controllo. I prompt sono input di un sistema probabilistico; l’allowlist dei tool e il ruolo cloud sono proprietà del runtime. Il controllo va affidato al runtime.
permissionMode dontAsk elimina ogni interattività?
No. Significa che i tool non elencati vengono negati anziché generare una richiesta. L’intera superficie consentita resta disponibile. Ciò che scompare è la modalità di guasto «l’agente chiede, una persona stanca fa clic su sì» — ed è proprio quella da eliminare.
Perché serve un limite di costo per istanza se esiste già quello giornaliero?
Perché il ciclo da $4,200 in 63 ore è rimasto sotto qualsiasi soglia giornaliera ragionevole mentre accumulava costi esecuzione dopo esecuzione. Un singolo ciclo errato può restare ben al di sotto di «$20/day» e costare comunque una somma a quattro cifre nel corso del fine settimana. Il limite per istanza è l’interruttore che, per sua natura, il limite giornaliero non può essere.
Uso Vercel o Railway, non Cloudflare Workflows: vale lo stesso?
Il punto di controllo, l’allowlist e il ruolo circoscritto non dipendono dalla piattaforma. Solo la primitiva di idempotenza — gli ID deterministici dei Workflow — è specifica di Cloudflare. Negli altri stack, usate una chiave di idempotenza sul job e rifiutate i duplicati a livello di coda o job runner.
È sufficiente l’approvazione di due persone?
Kiro la prevedeva per gli esseri umani, ma l’agente aveva ereditato permessi che gli consentivano di aggirarla. I controlli di approvazione devono essere associati al ruolo con cui opera l’agente, non alla persona che lo ha avviato. Se il ruolo dell’agente include permessi di distruzione, il controllo è solo una decorazione.
Se stai leggendo perché la scorsa settimana un agente del tuo team si è avvicinato troppo alla produzione, il contenimento è esattamente il lavoro che svolgo in DVNC.dev: applico al tuo stack l’architettura descritta sopra, nell’ordine che riduce per primo il raggio d’azione.
5 set 2026







