Leçon 02 · Fondamentaux

La boucle n'a pas changé, juste les outils

« La boucle n'a pas bougé d'une ligne, j'ai juste ajouté des entrées dans le tableau TOOLS. » — s02_tool_use.py

⏱ ~10 min · 📝 3 widgets interactifs · 🧑‍💻 Basé sur shareAI-lab · s02_tool_use.py

Où faut-il toucher le code pour ajouter un outil ?

L'agent de S01 ne sait faire que bash. Pour lui ajouter read_file, write_file et edit_file, que faut-il modifier ?

Le premier réflexe : modifier la boucle. C'est faux. La boucle ne change pas d'une ligne. Il suffit de trois choses :

  1. Écrire une fonction handler Python (run_read(path, limit)).
  2. L'enregistrer dans la table TOOL_HANDLERS ("read_file": lambda **kw: run_read(...)).
  3. Ajouter une déclaration JSON schema dans le tableau TOOLS (pour indiquer au modèle le nom de l'outil et ses paramètres).

Quand la boucle rencontre un bloc tool_use, elle cherche la fonction dans la dispatch map via block.name, l'exécute et réinjecte le résultat dans un tool_result. Exactement le même chemin que pour bash.

# Dispatch map: name → handler lambda
TOOL_HANDLERS = {
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file":  lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}

Voir le dispatch router en action

Le widget ci-dessous vous permet de cliquer sur une requête tool_use que le modèle pourrait émettre, et d'observer comment elle est routée vers la fonction Python correspondante. À noter : c'est block.name qui détermine le chemin emprunté.

safe_path : un garde-fou indispensable

Donner à l'agent un accès au système de fichiers expose au risque le plus classique : la traversée de répertoire. Le modèle est censé travailler dans /home/user/project/, mais il envoie read_file("../../etc/passwd").

s02 inclut une petite fonction qui règle ce problème :

def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()  # normalise les .. et les liens symboliques
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

La clé, c'est l'enchaînement .resolve() + .is_relative_to() : on résout d'abord en chemin absolu, puis on vérifie qu'il est toujours dans le bac à sable. Sans le premier, foo/../../etc passerait ; sans le second, rien n'est vérifié du tout.

Identifier les chemins sûrs

Voici 5 valeurs que le modèle pourrait passer comme paramètre path à read_file. Lesquelles safe_path() laisse-t-il passer, lesquelles rejette-t-il ? Supposons WORKDIR = /home/user/project.

N'ajoutez pas d'outils dangereux

Ajouter un outil est simple, mais n'oubliez pas : chaque outil que vous ajoutez élargit le périmètre d'action du modèle. Dans s02, bash dispose déjà d'une liste noire (rm -rf /, sudo, shutdown) et write_file est limité par safe_path. Avant d'ajouter un outil en production, posez-vous trois questions :

  • Cet outil peut-il produire des effets irréversibles ? (rm, envoi d'e-mail, git push)
  • Peut-il exposer des données sensibles ? (variables d'environnement, .ssh, cookies)
  • Sa sortie pourrait-elle être interprétée comme une instruction ? (injection de prompt via la sortie d'outil)

La leçon s07 introduira une « couche de permissions » pour externaliser ces décisions du code et les rendre déclaratives.

Interactif

Widget 1 · Tool Dispatch · comment tool_use est routé vers le handler

Cliquez sur une requête tool_use pour voir comment block.name est utilisé pour trouver la fonction correspondante, l'exécuter et réinjecter le résultat dans un bloc tool_result.

Requête tool_use émise par le modèle
Table TOOL_HANDLERS

        
Résultat · bloc tool_result
(cliquez sur une requête tool_use ci-dessus pour déclencher le routage)
Interactif

Widget 2 · Safe Path · détection d'échappement sur 5 chemins

Chaque chemin sera traité par safe_path(). En supposant WORKDIR = /home/user/project, cliquez sur « Autoriser » ou « Rejeter » — testez votre intuition sur la traversée de répertoire.

Correct : 0 / 5
Interactif

Widget 3 · Add a Tool · que faut-il modifier pour ajouter glob ?

Supposons qu'on veuille ajouter un outil glob(pattern) pour permettre à l'agent de trouver des fichiers par motif. Complétez les trois emplacements — tous les trois sont obligatoires.

Emplacement 1 · écrire la fonction handler
def run_glob(pattern: str) -> str:
    import glob
    return "\n".join(glob.glob(pattern))
Emplacement 2 · enregistrer dans la dispatch map (choisissez la bonne ligne)
Emplacement 3 · ajouter le schema dans TOOLS (choisissez le bon groupe)