Garder un agent dans son dossier
Un dossier confiné est le seul dossier où un agent peut lire et écrire. Chaque chemin qu’il demande doit finir dans ce dossier.
C’est plus difficile qu’il n’y paraît. Un chemin peut en sortir avec deux points, ou passer par un lien qui pointe ailleurs. La solution est de vérifier où le chemin finit vraiment.
À la fin de cette leçon
Remarque
Je peux garder un agent dans un seul dossier en vérifiant le chemin résolu.
Mots clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Dossier confiné | le seul dossier qu’un agent peut utiliser | |
| Résoudre | calculer où va vraiment un chemin | |
| Lien | un fichier qui pointe vers un autre endroit | raccourci |
| Évasion | finir en dehors du dossier | |
| Parent | le dossier au-dessus, écrit avec deux points | .. |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Résoudre d’abord, comparer ensuite
Résoudre un chemin, c’est suivre chaque paire de points et chaque lien jusqu’à l’adresse réelle. Seulement alors, demandez : est-il dans le dossier ?
Comparez avec une barre oblique à la fin. Le dossier bac n’est pas le dossier bac-ancien, même si un nom commence comme l’autre.
Exemple détaillé : dossier /travail/bac
| Demandé | Va vraiment à | Dedans ? |
|---|---|---|
| a.txt | /travail/bac/a.txt | Oui |
| ../dehors/s.txt | /travail/dehors/s.txt | Non |
| /etc/hosts | /etc/hosts | Non |
| ../bac-ancien/x.txt | /travail/bac-ancien/x.txt | Non |
| raccourci/s.txt | /travail/dehors/s.txt | Non : raccourci est un lien |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur courante
Attention
Beaucoup pensent qu’enlever les deux points d’un chemin le rend sûr. Ce n’est pas vrai. Un lien dans le dossier peut pointer dehors : le texte a l’air sûr et le vrai endroit ne l’est pas. Vérifiez toujours le vrai chemin, jamais le texte.
Si vous ne pouvez pas résoudre, refusez
Un chemin peut pointer vers un dossier qui n’existe pas, ou vers des liens qui bouclent sans fin. Vous ne pouvez pas dire où il va, donc vous ne pouvez pas dire qu’il est sûr.
Refusez-le. Un refus coûte une étape à l’agent. Un oui à tort peut coûter un disque entier.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la vérification du dossier, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const fs = require(\"fs\");' 'const path = require(\"path\");' 'const base = path.join(process.cwd(), \"bac-a-sable\");' 'const bac = path.join(base, \"bac\");' 'fs.rmSync(base, { recursive: true, force: true });' 'fs.mkdirSync(bac, { recursive: true });' 'fs.mkdirSync(path.join(base, \"bac-ancien\"));' 'fs.mkdirSync(path.join(base, \"dehors\"));' 'fs.writeFileSync(path.join(bac, \"note.txt\"), \"dedans\");' 'fs.writeFileSync(path.join(base, \"dehors\", \"secret.txt\"), \"secret\");' 'fs.symlinkSync(path.join(base, \"dehors\"), path.join(bac, \"raccourci\"));' > confiner.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la vérification du dossier, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const racine = fs.realpathSync(bac);' '// Où va vraiment un chemin : suivre les liens, puis comparer au dossier.' 'function dedans(p) {' ' try {' ' const t = path.resolve(racine, p);' ' const reel = fs.existsSync(t) ? fs.realpathSync(t) : path.join(fs.realpathSync(path.dirname(t)), path.basename(t));' ' return reel === racine || reel.startsWith(racine + path.sep);' ' } catch (e) { return false; }' '}' 'function naif(p) { return path.resolve(racine, p).startsWith(racine); }' >> confiner.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la vérification du dossier, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const demandes = [\"note.txt\", \"nouveau.txt\", \"../dehors/secret.txt\", \"/etc/hosts\", \"../bac-ancien/x.txt\", \"raccourci/secret.txt\"];' 'async function main() {' ' demandes.forEach(function (p) { console.log(p + \" texte seul:\" + naif(p) + \" resolu:\" + dedans(p)); });' ' console.log(\"fichiers du bac : \" + fs.readdirSync(bac).sort().join(\" \"));' ' const q = \"En une courte phrase, pourquoi faut-il résoudre un chemin avant de le vérifier ?\";' ' const echantillon = \"Un chemin peut utiliser des points ou un lien pour aller ailleurs, donc on vérifie où il finit vraiment.\";' ' const r = await demander([{ role: \"user\", content: q }], echantillon, 60);' ' console.log(\"le modèle dit : \" + r.texte);' '}' 'main();' >> confiner.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer la vérification du dossier
docker exec -w /home/node/project agent node confiner.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que le Run affiche
| Ligne | Ce qu’elle montre |
|---|---|
| note.txt texte seul:true resolu:true | Un fichier dans le dossier |
| ../dehors/secret.txt texte seul:false ... | Les deux vérifications repèrent les points |
| ../bac-ancien/x.txt texte seul:true resolu:false | Un nom qui commence pareil trompe le texte |
| raccourci/secret.txt texte seul:true resolu:false | Un vrai lien qui sort du dossier |
| fichiers du bac : note.txt raccourci | Le Run a créé ces fichiers pour de vrai |
| le modèle dit : ... | Une phrase. Les mots changent à chaque exécution |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.