Remplacer l'évaluateur par des Workers Cloudflare isolés
Fait partie de la série Construire le Code Mode pour les API avec MCP et Workers
Dans les parties précédentes, nous avons implémenté des outils de recherche et d'exécution qui utilisaient un évaluateur local de confiance basé sur le constructeur Function pour exécuter le code généré par l'IA. Cette approche fonctionnait pour le développement, mais soulevait de sérieux problèmes de sécurité.
Dans cette partie, nous remplacerons cet évaluateur local par un environnement isolé fondé sur les Workers Cloudflare dynamiques. L'isolation et les capacités explicites nous permettront de limiter les actions du code généré au lieu de lui accorder les permissions de l'hôte.
Les entrées utilisateur ne sont pas fiables
L'une des premières choses que nous apprenons en créant des sites avec des formulaires est que les entrées utilisateur ne sont pas fiables. Le danger ne vient pas seulement des personnes malveillantes, mais aussi des erreurs et des valeurs inattendues.
Le code généré est lui aussi une entrée utilisateur. Un modèle peut se tromper, et une personne malveillante peut essayer de l'orienter vers une action dangereuse.
Imaginez qu'au lieu d'écrire une fonction pour rechercher dans la spécification OpenAPI, le modèle produise le code suivant :
async () => {
return process.env
}Lorsque le code généré s'exécute dans le processus hôte, il peut accéder à toutes les variables globales et capacités exposées par celui-ci. Selon l'environnement, cela peut inclure les variables d'environnement, le système de fichiers, les API du processus ou un accès réseau sans restriction. Le constructeur Function est un évaluateur. Il n'a jamais été conçu pour exécuter du code non fiable.
Nous avons donc besoin d'un bac à sable. Ne faites jamais confiance à l'utilisateur ni au code généré.
Les bacs à sable sont lents et coûteux
Un bac à sable est un environnement isolé dans lequel du code non fiable ne peut pas affecter le reste du système. Dans notre cas, c'est la bonne manière d'exécuter le code généré.
Les bacs à sable traditionnels utilisent souvent des machines virtuelles ou des conteneurs. Leur temps de démarrage, leur consommation de mémoire et leur prix varient selon le fournisseur et la configuration, mais un système d'exploitation complet dépasse largement les besoins de notre tâche.
Un autre problème se pose. De nombreux Agents ont besoin d'environnements pour exécuter du code. Si chaque outil éphémère démarre un système d'exploitation complet, les besoins en capacité et les coûts augmentent vite.
Il nous faut une solution plus légère, plus rapide et capable de monter en charge. Nous exécutons de petites fonctions JavaScript. Un environnement JavaScript limité suffit.
Isoler la mémoire
Prenons un peu de recul. Existe-t-il déjà une solution qui exécute du code non fiable dans un environnement isolé ?
Les navigateurs !
Les navigateurs exécutent couramment sur une même machine du code provenant de nombreux sites. Pour séparer ce code, V8 utilise des isolats, c'est-à-dire des tas JavaScript indépendants qui peuvent partager un processus sans partager leur mémoire.
Un isolat est bien plus léger qu'une machine virtuelle ou un conteneur. Chaque exécution dispose de son propre espace mémoire. Le code d'un isolat ne peut donc pas inspecter ni modifier directement les objets d'une autre exécution.
L'isolation de la mémoire ne suffit cependant pas. Le code non fiable doit recevoir uniquement les capacités dont il a besoin : aucun accès au système de fichiers, aucun secret arbitraire et aucun accès réseau illimité.
Les Workers Cloudflare conviennent particulièrement bien à ce besoin. Ils reposent sur les isolats V8 et sur un environnement fondé sur les capacités. Un Worker ne reçoit que les liaisons et les accès sortants que nous lui accordons explicitement.
Node.js utilise lui aussi V8, mais Node n'est pas un bac à sable sécurisé. La documentation du module node:vm précise explicitement qu'il ne faut pas l'utiliser pour exécuter du code non fiable. Notre cas nécessite un environnement conçu pour faire respecter ces limites.
Les Workers dynamiques à la rescousse
Les Workers Cloudflare fournissent ce type d'environnement isolé. Avec les Workers dynamiques, nous n'avons pas besoin de déployer un nouveau Worker par l'intermédiaire d'un endpoint HTTP pour chaque exécution.
Le Worker parent utilise plutôt la liaison Dynamic Worker Loader pour créer un nouveau Worker à l'exécution, attendre sa réponse RPC, puis la renvoyer au client MCP. Cloudflare indique qu'un nouvel isolat démarre en quelques millisecondes et utilise quelques mégaoctets de mémoire, ce qui convient aux exécutions courtes.
Passer de l'évaluation locale aux Workers dynamiques
Le concept est posé, implémentons-le maintenant dans notre code.
Pour le moment, nous avons ceci :
/* eslint-disable no-new-func */
let searchFunction: unknown
try {
searchFunction = new Function(
'spec',
`"use strict"; return (${trimmedCode});`
)(searchSpec)
}
catch (error) {
throw new Error(`Could not compile search code: ${errorMessage(error)}`)
}Nous allons le remplacer par un appel à un nouveau Worker créé à la volée.
Note
Cette série suppose que vous connaissez déjà les liaisons Cloudflare.
async function runSearch(code: string): Promise<unknown> {
const worker = env.LOADER.get(`markethub-search-${crypto.randomUUID()}`, () => ({
compatibilityDate: '2026-07-11',
globalOutbound: null,
mainModule: 'worker.js',
modules: {
'worker.js': `
import { WorkerEntrypoint } from "cloudflare:workers";
const spec = ${JSON.stringify(searchSpec)};
export default class SearchExecutor extends WorkerEntrypoint {
async evaluate() {
try {
const result = await (${code})();
return { result, err: undefined };
} catch (error) {
return {
result: undefined,
err: error instanceof Error ? error.message : String(error)
};
}
}
}
`
}
}))
const entrypoint = worker.getEntrypoint() as unknown as SearchExecutorEntrypoint
const response = await entrypoint.evaluate()
if (response.err)
throw new Error(response.err)
return response.result
}Cela peut sembler compliqué, mais ça ne l'est pas tant que cela.
env.LOADER.getest une liaison Cloudflare qui permet de créer un Worker à la volée. Nous lui donnons un nom unique à chaque appel d'outil pour garantir l'isolation de chaque exécution.globalOutbound: nulldésactive tous les appels sortants àfetch()etconnect()depuis le Worker dynamique. L'exécuteur de recherche peut donc inspecter la spécification embarquée, mais ne peut pas accéder au réseau.mainModuledésigne le point d'entrée de notre Worker. C'est comme demander à Vite de lireindex.tsoumain.tsdepuis le fichier HTML. Ici, nous lui demandons de lire le fichierworker.jsdéfini dans la propriétémodules.modulesassocie les noms des modules à leur contenu, comme un système de fichiers virtuel. Il s'agit ici de modules ECMAScript. Nous définissons un seul module,worker.js, qui contient le code chargé d'exécuter celui de l'IA.
Cloudflare utilise RPC entre notre Worker et celui qui vient d'être créé, par l'intermédiaire de Cap'n Proto. Nous pouvons donc appeler entrypoint.evaluate() comme s'il s'agissait d'une fonction locale. Cloudflare sérialise la requête, l'envoie au nouveau Worker, l'exécute et renvoie le résultat.
Donner à l'exécution une passerelle avec liste d'autorisations
La recherche n'a besoin d'aucun accès sortant, mais l'exécution doit appeler l'API MarketHub locale. L'implémentation de l'exécution ne modifie que cette frontière de capacité :
const worker = env.LOADER.get(`markethub-execute-${crypto.randomUUID()}`, () => ({
compatibilityDate: '2026-07-11',
globalOutbound: exports.GlobalOutbound({}),
mainModule: 'worker.js',
modules: {
'worker.js': executeWorkerSource(code),
},
}))La valeur null signifie refuser tout accès sortant. La valeur exports.GlobalOutbound({}) achemine à la place chaque appel fetch() et connect() du Worker dynamique vers la passerelle du Worker parent. Le bac à sable ne dispose toujours d'aucun accès réseau direct. Il reçoit uniquement le comportement autorisé par la passerelle.
export class GlobalOutbound extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
const allowedOrigin = new URL(this.env.MARKETHUB_API_BASE).origin
const requestedOrigin = new URL(request.url).origin
if (requestedOrigin !== allowedOrigin) {
return new Response('Forbidden: outbound destination is not allowed', {
status: 403
})
}
return fetch(request)
}
}L'environnement de test n'utilise aucune authentification. Sa passerelle autorise donc uniquement l'origine MarketHub. En production, cette même passerelle peut ajouter un identifiant conservé par le Worker parent avant de transmettre la requête. Le code généré ne voit jamais cet identifiant. C'est la propriété essentielle de cette approche.
Borner chaque exécution
L'isolation contrôle les ressources auxquelles le code généré peut accéder. Les limites contrôlent la quantité de travail qu'il peut effectuer.
L'implémentation associée rejette déjà les programmes générés de plus de 20 000 caractères et tronque les résultats formatés après 12 000 caractères. Ces bornes empêchent une réponse ou un résultat du modèle de consommer indéfiniment le contexte de l'Agent.
Les Workers dynamiques peuvent également imposer des limites de CPU et de sous-requêtes pour chaque appel. Une configuration de production pourrait commencer avec le budget prudent suivant, puis l'ajuster selon les charges observées :
const worker = env.LOADER.get(workerId, () => ({
compatibilityDate: '2026-07-11',
limits: {
cpuMs: 50,
subRequests: 10,
},
globalOutbound: exports.GlobalOutbound({}),
mainModule: 'worker.js',
modules: { 'worker.js': executeWorkerSource(code) },
}))Si le Worker dynamique dépasse l'une de ces limites, il lève une exception. Les bonnes valeurs dépendent du travail attendu. La recherche demande peu de CPU et aucune sous-requête, tandis que l'exécution doit disposer d'assez de sous-requêtes pour les appels API prévus.
Vous êtes prêt
Vous disposez maintenant d'un serveur MCP qui utilise le Code Mode tout en conservant le code généré dans un environnement neuf et isolé. La recherche ne possède aucun accès sortant. L'exécution dispose uniquement d'une passerelle avec une liste d'autorisations. Enfin, les budgets d'entrée, de sortie, de CPU et de sous-requêtes empêchent une exécution de consommer des ressources sans limite.
J'espère que cette série vous a plu et qu'elle vous a appris quelque chose. Le dépôt associé, code-mode, contient tout le code abordé. Chaque article possède sa propre branche.
Si vous avez des questions, vous pouvez me contacter sur Twitter ou LinkedIn.
Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.
Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.
Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.
J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous, cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.