
Un agent qui écrit du code est un agent qui exécute du code — du code produit par un modèle, donc faillible, et exploitable par une injection de prompt. L'exécuter tel quel sur votre machine est une faute de sécurité. Ce guide construit un bac à sable : un environnement dont le code ne peut pas s'échapper, même malveillant. Vous verrez d'abord le spectre de l'isolation — de l'interpréteur restreint à la microVM — avec un comparatif sécurité, coût et latence. Puis vous construirez un bac à sable Docker durci concret : pas de réseau, aucun privilège, système de fichiers en lecture seule, ressources plafonnées. Enfin, vous y confinerez du code généré par un LLM et le branchez à un agent. Public visé : développeur ayant lu le guide smolagents et conscient que l'exécution de code est le risque central des agents.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Pourquoi exécuter le code d'un agent sans isolation est une faute.
- Le spectre de l'isolation : interpréteur restreint, conteneur durci, gVisor, microVM, service géré.
- Construire un bac à sable Docker durci option par option.
- Y confiner du code généré par un LLM.
- Brancher un bac à sable à un agent.
Prérequis
Section intitulée « Prérequis »Ce guide prolonge smolagents, qui pose le risque de l'exécution de code. Il vous faut :
- Docker installé et fonctionnel.
- Une instance Ollama avec le modèle
qwen2.5pour la démonstration. - Les bases de Python et de la ligne de commande Docker.
Le risque : un agent qui exécute du code
Section intitulée « Le risque : un agent qui exécute du code »Quand un agent exécute le code d'un modèle, trois scénarios mènent au même désastre. L'erreur du modèle : un LLM imparfait génère une commande destructrice en croyant bien faire. L'injection de prompt : un agent qui lit une page web piégée se voit dicter du code hostile. L'agent exposé : une interface publique où un attaquant fabrique une entrée qui détourne l'exécution.
Une fois le code lancé sans isolation, il a les droits du processus agent : lire vos fichiers, exfiltrer des secrets par le réseau, abuser d'API, attaquer le réseau interne. Le problème n'est pas hypothétique — c'est la conséquence directe du fait de donner à un modèle le pouvoir d'agir.
La parade n'est pas de faire confiance au modèle. C'est de partir du principe que le code est hostile et de l'exécuter dans un environnement où, hostile ou non, il ne peut rien atteindre d'important.
Le spectre de l'isolation
Section intitulée « Le spectre de l'isolation »Isoler n'est pas binaire. Il existe un gradient de solutions, du plus léger au plus étanche — et plus l'isolation est forte, plus le coût de mise en place et la latence montent.
L'interpréteur restreint — celui de smolagents — analyse le code avant de l'exécuter et bloque les imports dangereux. Léger, sans dépendance, mais il s'exécute dans votre processus : une faille de l'interpréteur, et l'isolation tombe.
Le conteneur durci exécute le code dans un conteneur Docker dépouillé de tout privilège. C'est le sujet du lab : accessible partout où Docker tourne, bonne isolation. Sa limite : le conteneur partage le noyau de l'hôte.
gVisor insère un noyau applicatif entre le conteneur et le vrai noyau : les appels système du code sont interceptés et traités par gVisor, jamais directement par l'hôte. L'isolation gagne une couche, au prix d'une légère surcharge et d'une installation au niveau du moteur de conteneurs.
La microVM — la technologie de Firecracker — exécute le code dans une vraie machine virtuelle, avec son propre noyau, démarrée en quelques dizaines de millisecondes. C'est l'isolation par la virtualisation matérielle, la plus forte. C'est aussi ce que proposent les services gérés comme E2B ou Daytona : un bac à sable distant, prêt à l'emploi, facturé à l'usage.
Comparatif : sécurité, coût, latence
Section intitulée « Comparatif : sécurité, coût, latence »Aucune option n'est « la bonne » dans l'absolu : le choix dépend du niveau de confiance envers le code et de vos contraintes.
| Solution | Isolation | Coût de mise en place | Latence | Pour quel code |
|---|---|---|---|---|
| Interpréteur restreint | Faible — même processus | Nul | Négligeable | Code peu risqué, modèle de confiance |
| Conteneur durci | Bonne — noyau partagé | Faible (Docker) | Démarrage conteneur | Défaut raisonnable |
| gVisor | Forte — noyau applicatif | Moyen (runtime à installer) | Faible surcharge | Code peu fiable, multi-tenant |
| microVM (Firecracker) | Très forte — VM dédiée | Élevé (infra) | Démarrage VM (~ms) | Code non fiable, exposition publique |
| Service géré (E2B…) | Très forte — déléguée | Nul (compte) | Réseau + démarrage | Pas d'infra à gérer, budget dédié |
La règle de décision est simple. Code plutôt fiable, modèle connu : un conteneur durci suffit. Code non fiable ou agent exposé au public : il faut gVisor ou une microVM. Et quand on ne veut pas gérer l'infrastructure, un service géré déplace le problème — au prix d'une dépendance et d'une facture.
Construire un bac à sable Docker durci
Section intitulée « Construire un bac à sable Docker durci »Le lab implémente le conteneur durci : le meilleur rapport isolation / simplicité, et reproductible partout où Docker tourne. L'idée est d'exécuter le code dans un conteneur jetable, lancé avec un jeu d'options qui lui retirent tout pouvoir.
IMAGE = ( "python:3.12-slim@sha256:" "9d3abd9fc11d06998ccdbdd93b4dd49b5ad7d67fcbbc11c016eb0eb2c2194891")
DURCISSEMENT = [ "--network", "none", # aucune connexion réseau "--cap-drop", "ALL", # aucune capability Linux "--security-opt", "no-new-privileges", # pas d'escalade de privilèges "--read-only", # racine en lecture seule "--tmpfs", "/tmp:size=16m", # seul /tmp est inscriptible, borné "--memory", "256m", # plafond mémoire "--pids-limit", "64", # plafond de processus "--user", "65534:65534", # exécution en tant que nobody]L'image est épinglée par digest (@sha256:...), pas par tag : on exécute exactement la couche vérifiée, sans dérive d'une reconstruction du tag. La fonction d'exécution assemble la commande, lance le conteneur et borne sa durée.
import subprocess, uuidfrom dataclasses import dataclass
@dataclassclass ResultatSandbox: succes: bool sortie: str erreur: str
def executer(code: str, delai: int = 10) -> ResultatSandbox: """Exécute du code Python dans un conteneur durci, jeté après usage.""" nom = f"sandbox-{uuid.uuid4().hex[:12]}" commande = [ "docker", "run", "--rm", "--name", nom, *DURCISSEMENT, IMAGE, "python", "-c", code, ] try: proc = subprocess.run( commande, capture_output=True, text=True, timeout=delai ) except subprocess.TimeoutExpired: subprocess.run(["docker", "rm", "-f", nom], capture_output=True) return ResultatSandbox(False, "", f"délai dépassé ({delai} s)") return ResultatSandbox(proc.returncode == 0, proc.stdout, proc.stderr)L'option --rm et le nom unique rendent le conteneur jetable : créé pour une exécution, détruit aussitôt — aucun état ne survit. Et le délai est appliqué côté client : en cas de dépassement, le conteneur est tué explicitement, pour ne pas laisser un processus tourner indéfiniment.
Le durcissement, option par option
Section intitulée « Le durcissement, option par option »Chaque option de DURCISSEMENT ferme une voie d'évasion précise. Aucune ne suffit seule ; c'est leur cumul qui isole.
--network none retire toute interface réseau sauf la boucle locale. Le code ne peut ni exfiltrer de données, ni télécharger une charge utile, ni atteindre le réseau interne. C'est souvent la mesure la plus importante.
--cap-drop ALL retire toutes les capabilities Linux — ces droits fins qu'un processus peut détenir. Combiné à --security-opt no-new-privileges, qui interdit d'en regagner, le code s'exécute sans aucun pouvoir privilégié.
--read-only rend la racine du conteneur non inscriptible ; --tmpfs /tmp:size=16m rouvre uniquement /tmp, et le borne. Le code peut écrire un fichier temporaire, rien de plus — et pas au point de saturer le disque.
--memory et --pids-limit plafonnent mémoire et nombre de processus : une boucle qui alloue sans fin ou une bombe à fork est arrêtée net. Enfin, --user 65534:65534 exécute le code en tant que nobody — jamais root, même à l'intérieur du conteneur.
Confiner du code généré par un LLM
Section intitulée « Confiner du code généré par un LLM »Le bac à sable prend tout son sens face à du code qu'on n'a pas écrit. Le lab génère un script avec Ollama, puis le confine — sans jamais l'exécuter sur l'hôte.
code = generer_code("Affiche la somme des entiers de 1 à 100.")resultat = executer(code)print("Succès :", resultat.succes, "| Sortie :", resultat.sortie.strip())# >>> Succès : True | Sortie : 5050La réponse d'un modèle arrive souvent enrobée de Markdown — des balises ``` autour du code. Une étape d'extraction les retire avant exécution : un détail, mais un script lancé avec ses balises échouerait à la compilation.
La suite de tests du lab vérifie le confinement sur cinq points : l'extraction du code (déterministe, sans modèle), l'exécution d'un code anodin, le blocage du réseau — une tentative d'accès échoue —, l'application du délai sur une boucle infinie, et enfin l'exécution d'un script écrit par le modèle. Le test du réseau est le plus parlant : il prouve que --network none fait son office.
Brancher le bac à sable à un agent
Section intitulée « Brancher le bac à sable à un agent »Reste à connecter ce bac à sable à un agent. Deux voies, selon le framework.
Avec smolagents, l'isolation est un paramètre. Le CodeAgent accepte un executor_type : "docker", "e2b" ou "modal" envoient le code généré vers un bac à sable au lieu de l'interpréteur local. C'est l'intégration la plus directe — une ligne de configuration.
Avec un agent maison — la boucle écrite à la main des premiers guides —, le bac à sable devient un outil comme un autre. Là où l'agent appelait une fonction Python directement, il appelle executer(code) : le code transite par le conteneur, et seul son résultat revient dans la boucle. L'agent garde le contrôle ; l'exécution, elle, est confinée.
Dans les deux cas, le principe est identique : séparer la décision de l'exécution. L'agent décide quel code lancer ; le bac à sable décide ce que ce code a le droit de faire.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
permission denied sur le socket Docker | Utilisateur hors du groupe docker | Ajouter l'utilisateur au groupe, rouvrir la session |
| Le conteneur reste actif après un délai | Client tué sans nettoyage | Tuer le conteneur par son nom (docker rm -f) |
| Le code échoue à écrire un fichier | Racine en lecture seule | Écrire dans /tmp (monté en tmpfs) |
| Un accès réseau attendu échoue | --network none | Si le réseau est requis, restreindre plutôt que couper |
OOMKilled dans les logs | Plafond mémoire atteint | Relever --memory si la tâche le justifie |
À retenir
Section intitulée « À retenir »- Le code d'un agent est potentiellement hostile : erreur du modèle, injection de prompt, agent exposé.
- L'isolation est un spectre : interpréteur restreint, conteneur durci, gVisor, microVM, service géré.
- Plus l'isolation est forte, plus le coût et la latence montent — le choix dépend de la confiance dans le code.
- Un conteneur durci cumule des options qui, ensemble, retirent réseau, privilèges, écriture et ressources.
- Un conteneur durci partage le noyau : pour du code non fiable, passer à gVisor ou à une microVM.
- Le principe constant : séparer la décision de l'exécution — l'agent décide, le bac à sable confine.
Prochaines étapes
Section intitulée « Prochaines étapes »Pour aller plus loin
Section intitulée « Pour aller plus loin »- Exécution de code sécurisée — smolagents — interpréteur restreint et bacs à sable.
- Documentation gVisor — le noyau applicatif et son intégration à Docker.