Aller au contenu
Développement medium

Sandbox d'exécution de code pour agents IA

14 min de lecture

logo python

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.

  • 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.

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.5 pour la démonstration.
  • Les bases de Python et de la ligne de commande Docker.

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.

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.

Aucune option n'est « la bonne » dans l'absolu : le choix dépend du niveau de confiance envers le code et de vos contraintes.

SolutionIsolationCoût de mise en placeLatencePour quel code
Interpréteur restreintFaible — même processusNulNégligeableCode peu risqué, modèle de confiance
Conteneur durciBonne — noyau partagéFaible (Docker)Démarrage conteneurDéfaut raisonnable
gVisorForte — noyau applicatifMoyen (runtime à installer)Faible surchargeCode 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éeNul (compte)Réseau + démarragePas 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.

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, uuid
from dataclasses import dataclass
@dataclass
class 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.

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.

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 : 5050

La 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.

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.

SymptômeCause probableSolution
permission denied sur le socket DockerUtilisateur hors du groupe dockerAjouter l'utilisateur au groupe, rouvrir la session
Le conteneur reste actif après un délaiClient tué sans nettoyageTuer le conteneur par son nom (docker rm -f)
Le code échoue à écrire un fichierRacine en lecture seuleÉcrire dans /tmp (monté en tmpfs)
Un accès réseau attendu échoue--network noneSi le réseau est requis, restreindre plutôt que couper
OOMKilled dans les logsPlafond mémoire atteintRelever --memory si la tâche le justifie
  • 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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn