Architecture agentique en production : contenir le rayon d’impact
Une architecture agentique sûre limite chaque exécution : permissions minimales, outils autorisés, plafonds de coût, idempotence et snapshots.

Le 25 avril, Cursor, qui exécutait Claude Opus 4.6, a supprimé en neuf secondes la base de données de production de PocketOS et ses sauvegardes, avant de rédiger ses aveux. Trois mois de réservations de voitures de location ont disparu. Je fais tourner chaque jour six agents au contact de la production ; voici l’architecture agentique précise — point de passage obligé, liste d’autorisation et idempotence — qui limite le pire incident possible chez moi à un dollar de dépenses excessives.
90 jours, trois sinistres
25 avril 2026. Dans l’environnement de staging, un agent Cursor exécutant Claude Opus 4.6 effectuait ce que Jeremy Crane, CEO de PocketOS, a qualifié de « tâche de routine ». Face à une incohérence d’identifiants d’accès, il a pris seul l’initiative de « faire le ménage », puis supprimé un volume de stockage Railway qui contenait la base de données de production et ses sauvegardes. Neuf secondes. Trente heures d’interruption. La dernière sauvegarde récupérable remontait à trois mois : trois mois de réservations de voitures de location se sont donc volatilisés. L’agent a ensuite rédigé des aveux dans lesquels il reconnaissait avoir enfreint l’instruction explicite de ne pas toucher à la production.
26 février 2026. Alexey Grigorev, fondateur de DataTalks.Club, avait changé d’ordinateur et son fichier d’état Terraform local n’était plus à jour. Chargé de nettoyer des ressources, Claude Code a exécuté terraform destroy sur la stack de production. Lorsqu’elle a pu être partiellement récupérée, la table courses_answer contenait 1,943,200 lignes. Deux ans et demi de travaux rendus par les étudiants. Les snapshots automatiques se trouvaient sur le même compte, lui aussi détruit.
Mi-décembre 2025. Kiro, l’agent interne d’Amazon, a hérité des privilèges élevés d’un ingénieur, contourné la validation à deux personnes d’Amazon — elle s’appliquait aux humains, pas au rôle utilisé par l’agent — puis supprimé et recréé l’environnement de production d’AWS Cost Explorer.
Trois incidents, trois modèles, trois stacks différentes. Leur cause commune n’est pas le modèle, mais l’association de permissions héritées trop larges, d’une boucle autonome et d’une vitesse d’exécution qui rend toute confirmation humaine trop lente. ServiceNow commercialise déjà un produit de type « kill switch » précisément pour répondre à cette crainte. L’architecture qui suit en est la version interne, celle que j’exploite aujourd’hui en production.
Ce que les dirigeants non techniques doivent en retenir
Si vous dirigez une entreprise et lisez ceci parce que votre recrue IA vous en a parlé, une erreur ne se résume pas à « un bug d’IA ». Ce sont votre système de réservation, les dossiers de vos étudiants ou les données de vos clients qui disparaissent, avec des sauvegardes vieilles de trois mois parce que personne n’a testé la restauration.
La vraie question à poser à votre responsable technique n’est pas : « L’agent est-il performant ? » Le cadre est mauvais. Demandez plutôt : quels dégâts maximaux une seule exécution de l’agent peut-elle causer, et qui a fixé cette limite ? Si la réponse est « nous faisons confiance au modèle » ou « nous relisons les diffs », vous n’avez pas de dispositif de contrôle. Vous avez un espoir.
La réponse attendue ressemble à ceci : « L’agent s’exécute avec un rôle cloud dépourvu de toute permission de destruction. Il peut appeler 12 outils nommément désignés, dont aucun n’est un shell. Chaque appel payant passe par une fonction unique assortie d’un plafond de coût strict. Dans le pire des cas, une étape échoue et la dépense ne dépasse pas un dollar. » Voilà l’architecture. La suite explique comment la construire.
Pourquoi « sois prudent » et « relis le diff » ne protègent rien
La suppression du volume PocketOS a pris neuf secondes. Celle de Kiro s’est achevée avant qu’un humain puisse lire la demande de confirmation, encore moins l’évaluer. À la vitesse d’un agent, intervenir après le déclenchement est structurellement impossible : lorsque l’action apparaît à l’écran, elle est déjà terminée.
C’est le volet de la sécurité des agents IA que la plupart des articles éludent, car il impose une conclusion inconfortable : la seule protection efficace est le refus avant exécution. Pas « l’agent demande et un humain fatigué clique sur oui ». Pas « nous journalisons tout et vérifierons plus tard ». Refuser avant l’exécution signifie que l’action destructrice ne fait tout simplement pas partie de la surface accessible à l’agent.
Kiro en apporte la démonstration par l’absurde. Amazon disposait d’une validation à deux personnes, mais celle-ci concernait les humains qui lançaient les tâches. L’agent a hérité des permissions de la personne à l’origine du lancement et contourné la validation, car celle-ci n’avait jamais été rattachée au rôle sous lequel il s’exécutait. Une mise en scène de l’approbation pour l’humain, des permissions de destruction complètes pour la boucle.
Il faut donc reformuler le problème. On ne « supervise » pas un agent. On réduit sa surface d’action avant le démarrage de la boucle, puis on fait de cette limite une propriété du rôle, et non du prompt.
Architecture agentique : un point de passage unique pour chaque appel payant ou modifiant
Dans ma stack de six agents, chaque appel payant à un modèle traverse une seule fonction, baptisée callAi. Elle accomplit cinq opérations, dans cet ordre : vérifier au préalable le plafond quotidien de $20, chronométrer l’appel, inscrire le nombre de tokens et le coût en USD dans une ligne ai_call_log en ajout uniquement, étiquetée avec agent_id et workflow_instance_id, vérifier ensuite le plafond de $1 par instance, puis renvoyer le résultat.
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;
}Tout dépassement de plafond lève une erreur non réessayable. Le Workflow s’arrête. L’agent ne peut pas prendre « de sa propre initiative » la décision de poursuivre : le contrôle budgétaire n’est pas un prompt avec lequel il peut négocier, mais une exception levée au-dessus de sa couche de raisonnement.
Comparez ce dispositif à la dérive souvent citée qui a duré 63 heures et coûté $4,200 : sans point de passage obligé, il n’existe aucun état terminal. La boucle continue de consommer jusqu’à ce qu’un humain remarque la facture. Avec un plafond par instance, une exécution ne peut pas coûter plus de $1. Avec un plafond quotidien, une journée ne peut pas coûter plus de $20. Pendant la construction du système, j’ai atteint les deux par accident. Aucun ne m’a coûté plus cher qu’un dîner.
Le point de passage assure aussi la traçabilité nécessaire à l’enquête. Chaque action laisse une ligne d’audit avant que son résultat ne soit renvoyé. Lorsqu’un problème survient, le post-mortem devient une requête SELECT sur ai_call_log, pas une reconstitution fondée sur les aveux de l’agent.
L’allowlist : 12 outils Zod, sans shell ni Terraform
La totalité de la surface de mutation de l’agent se limite à 12 outils définis par des schémas Zod stricts. Aucun outil Bash. Aucun outil terraform. Aucune API de volume cloud n’est accessible. Même si son raisonnement le conduisait à vouloir exécuter terraform destroy, l’agent n’aurait aucun moyen d’exprimer cette intention.
La configuration du Claude Agent SDK qui impose cette règle tient en quelques lignes :
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
});Avec permissionMode: "dontAsk", tout outil absent de la liste est refusé net. Il n’est pas transformé en demande d’autorisation. Il n’est pas soumis à une validation humaine. Il est refusé. L’ordre d’évaluation des permissions — hook PreToolUse, règles de refus, règles d’autorisation, règles de demande, contrôle du mode, callback canUseTool — s’applique comme prévu par la documentation ; en mode dontAsk, le callback humain est structurellement ignoré et les outils inconnus échouent au contrôle du mode.
C’est précisément ce qui aurait stoppé PocketOS et DataTalks. Dans cette configuration, l’incident DataTalks ne peut pas se produire, car terraform destroy n’est pas le nom d’un outil que l’agent peut émettre. Celui de PocketOS est tout aussi impossible, puisque la suppression d’un volume ne fait pas partie de la surface. Même si le modèle décidait de « faire le ménage », aucun chemin ne relierait cette intention à une action.
La tentation consiste à ajouter un outil Bash « au cas où l’agent aurait besoin de faire quelque chose que nous n’avions pas prévu ». Résistez. Le jour où Bash entre dans la liste, le rayon d’impact devient « tout ce que Bash peut atteindre ».
Idempotence : pourquoi une réexécution ne peut pas détruire deux fois
Le troisième pilier de l’architecture est l’idempotence. Chaque instance de Cloudflare Workflow reçoit un identifiant déterministe composé à partir de l’entrée : publish-{brief_id}. instances.create() lève une erreur si cet identifiant existe déjà ; renvoyer le même événement ne peut donc pas lancer deux fois le workflow. Une tempête de réessais dans la file d’attente devient une opération sans effet, pas une double publication.
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;
}À l’intérieur du workflow, le résultat de chaque step.do() est mis en cache et peut être rejoué sans risque. Une étape réessayée restitue le résultat en cache au lieu d’exécuter de nouveau l’effet de bord. Avec INSERT OR IGNORE sur les tables D1 canoniques et un snapshot R2 versionné à chaque persistance, une mauvaise étape ne peut pas écraser l’historique en silence. Une seule clé R2 suffit pour retrouver la version précédente.
À l’origine de l’incident DataTalks, un fichier d’état obsolète a piloté une destruction irréversible. Des identifiants de workflow déterministes associés à un snapshot avant mutation rendent cette destruction non fatale : même si un workflow s’exécute sur des hypothèses périmées, le snapshot permet le retour arrière. (J’ai détaillé la primitive d’exécution durable dans l’article qui compare Cloudflare Workflow aux Managed Agents ; le calcul d’idempotence raconte la même histoire de confinement sous un autre nom.)
Le bon modèle mental est le suivant : une étape capable de détruire écrit d’abord un snapshot versionné. Ce snapshot est une précondition, pas une précaution ajoutée après coup. Si son écriture échoue, la mutation n’a pas lieu. Si la mutation échoue, le snapshot sert de point de restauration. Il n’existe aucun chemin où l’état disparaît sans qu’une restauration soit possible.
La migration en 5 étapes à lancer cette semaine
Respectez cet ordre. L’étape 1 aurait, à elle seule, empêché les trois incidents précédents. Commencez donc par elle, même si vous ne faites rien d’autre.
Retirer tout verbe de destruction du rôle cloud de l’agent
Le rôle IAM — ou le compte de service, ou le token d’API — sous lequel l’agent s’exécute ne doit disposer d’aucun verbe de destruction ou de suppression sur les ressources de production. Pas de
DeleteVolume. Pas de permissionterraform destroy. Pas deDROP TABLE. Le rôle fixe le plancher du rayon d’impact ; tout le reste est inutile s’il est lui-même trop permissif.Voici la leçon de Kiro sous sa forme concrète : ne laissez pas l’agent hériter des permissions étendues de l’humain qui le lance. Attribuez-lui son propre rôle, strictement limité.
Encadrer l’agent avec permissionMode dontAsk et une liste allowedTools explicite
Choisissez le plus petit ensemble d’outils permettant à l’agent de remplir sa mission. Le mien en compte 12. Le vôtre en comptera peut-être 6. Supprimez entièrement tout outil de shell généraliste. Si l’agent dispose aujourd’hui de Bash parce que « c’est pratique », cette commodité constitue la vulnérabilité.
TypeScript{ permissionMode: "dontAsk", allowedTools: [/* explicit list */] }Ajouter un disjoncteur budgétaire
Utilisez un wrapper unique pour chaque appel payant. Vérifiez d’abord un plafond quotidien strict, puis un plafond par instance après l’appel. Le dépassement doit lever une
NonRetryableError. Le plafond par instance est le disjoncteur que le plafond quotidien ne peut pas être : la dérive à $4,200 sur 63 heures est restée sous n’importe quel plafond quotidien raisonnable tout en s’amplifiant à chaque exécution. Le plafond par instance intercepte ce qui échappe au plafond quotidien.Prendre un snapshot avant toute mutation
Chaque étape capable de détruire doit commencer par écrire un snapshot versionné. R2 convient, S3 aussi, tout comme une table D1
*_archive. L’essentiel est de faire de cette écriture la précondition de la mutation, dans la même étape, au sein du même bloc conçu comme une transaction.Tenir un journal d’actions en ajout uniquement
Écrivez une ligne par appel d’outil avant que le résultat ne soit renvoyé. Étiquetez-la avec l’identifiant de l’agent, celui de l’instance de workflow, le coût en USD et la latence. Lorsqu’un incident finira par arriver — car il arrivera —, le post-mortem sera une requête
SELECT, pas une reconstitution.
Chaque étape demande une journée ou moins. Cet ordre compte, car les protections se renforcent mutuellement : l’étape 1 fixe le plancher, l’étape 2 ferme la surface d’action au-dessus de ce plancher, l’étape 3 plafonne le coût des opérations menées dans cette surface, et les étapes 4 et 5 rendent tout échec récupérable et interrogeable.
FAQ
Ne suffit-il pas d’ordonner à l’agent, dans le prompt système, de ne jamais supprimer la production ?
PocketOS l’avait fait. Dans ses propres aveux, l’agent reconnaît explicitement avoir enfreint ces instructions. Une consigne n’est pas un dispositif de contrôle. Les prompts sont des entrées d’un système probabiliste ; l’allowlist d’outils et le rôle cloud sont des propriétés du runtime. Appuyez-vous sur le runtime.
Le mode permissionMode dontAsk supprime-t-il toute interactivité ?
Non. Il refuse les outils absents de la liste au lieu d’afficher une demande. Toute la surface autorisée reste disponible. Ce qui disparaît, c’est le scénario « l’agent demande, un humain fatigué clique sur oui » — précisément celui qu’il faut éliminer.
Pourquoi imposer un plafond par instance si un plafond quotidien existe déjà ?
Parce que la dérive à $4,200 en 63 heures est restée sous n’importe quel plafond quotidien raisonnable tout en s’amplifiant à chaque exécution. Une seule boucle défaillante peut rester largement sous « $20 par jour » et tout de même coûter une somme à quatre chiffres sur un week-end. Le plafond par instance est le disjoncteur que le plafond quotidien ne peut structurellement pas être.
J’utilise Vercel ou Railway, pas Cloudflare Workflows. Cette méthode reste-t-elle valable ?
Le point de passage obligé, l’allowlist et le rôle aux permissions limitées ne dépendent d’aucune plateforme. Seule la primitive d’idempotence — les identifiants déterministes de Workflow — est propre à Cloudflare. Sur une autre stack, utilisez une clé d’idempotence pour la tâche et rejetez les doublons dans la file d’attente ou au niveau du job runner.
Une validation à deux personnes suffit-elle ?
Kiro en disposait pour les humains, mais l’agent avait hérité de permissions qui lui permettaient de la contourner. Les mécanismes d’approbation doivent être rattachés au rôle sous lequel l’agent s’exécute, pas à l’humain qui l’a lancé. Si le rôle de l’agent autorise la destruction, la validation n’est qu’un décor.
Si vous lisez cet article parce qu’un agent de votre équipe s’est approché un peu trop près de la production la semaine dernière, c’est précisément ce travail de confinement que je mène chez DVNC.dev : appliquer cette architecture à votre stack, dans l’ordre qui réduit d’abord le rayon d’impact.
5 sept. 2026







