Aller au contenu
Mohamed Tahiri
Miami --:--
← Tous les articles

Donner de vrais outils à un agent IA, sans lui donner les clés

Un agent utile agit : il cherche, interroge une base, envoie des messages. Comment je lui donne des outils puissants en bornant ce qu’une injection de prompt peut faire, avec un labo interactif.

Un agent devient utile au moment où il peut agir : chercher, interroger une base, envoyer un message. Il devient dangereux pour exactement la même raison. Et l’instruction qui le détourne n’a pas besoin de venir de l’utilisateur : elle peut dormir dans un document, une page web ou un email que l’agent lit. C’est l’injection de prompt indirecte.

Borner la capacité, pas détecter l’attaque

Détecter une injection est une course perdue d’avance : il suffit d’une formulation que le filtre n’a jamais vue. Je pars donc du principe inverse. Le pire cas doit rester un outil que la personne avait déjà, sur des données qu’elle pouvait déjà lire. Tout le reste se joue dans le code, pas dans le prompt.

Le labo ci-dessous rejoue une attaque classique : un email piégé demande à l’agent d’exfiltrer la liste des clients. Activez les défenses une par une et regardez où l’attaque s’arrête.

Marquer le contenu externe comme de la donnée aide le modèle, mais rejouez avec cette seule défense : une fois sur trois, il obéit quand même. Les défenses qui tiennent sont celles qui ne dépendent pas de son jugement.

Trois couches, dont une seule compte

CONSIGNES AU MODÈLECODEUtilisateurPROFIL · RÔLES1 · Liste d’outilsUN OUTIL REFUSÉ N’EST PAS DÉCRIT2 · SchémaCOLLECTIONS RESTREINTES3 · Vérif. dans l’outilLA VRAIE FRONTIÈREDonnéesCOPIE EN LECTURE SEULEUN MODÈLE N’EST PAS UNE FRONTIÈRE DE SÉCURITÉ

Dans l’agent que j’ai mis en production, les droits de chaque utilisateur sont appliqués trois fois. Un outil refusé n’est jamais décrit au modèle. Le schéma des outils restants ne propose que les collections autorisées. Et le code de chaque outil revérifie tout, y compris élément par élément quand une recherche en vise plusieurs.

tools.py
def tools_for(profile):
    # layer 1: a denied tool is never even described to the model
    return [t for t in REGISTRY
            if profile.allows_tool(t.name)
            and t.requires_collections <= profile.collections]

async def run_tool(name, args):
    tool = get_handler(name)        # a hallucinated name never reaches a handler
    for collection in collections_in(args):
        current_profile().require(collection)  # layer 3: code, not a prompt
    return await tool(args)

Un outil SQL écrit par le modèle, contenu par le moteur

L’outil le plus puissant est aussi le plus risqué : le modèle écrit lui-même ses requêtes SQL. Elles tournent sur une copie nocturne, en lecture seule, de colonnes choisies, jamais sur la base de production. Chaque requête ouvre une connexion neuve qui n’attache que les collections autorisées, puis la verrouille :

analytics.py
LOCKDOWN = (
    "SET autoinstall_known_extensions = false",
    "SET autoload_known_extensions = false",
    "SET allow_community_extensions = false",
    "SET enable_external_access = false",   # no files, no network
    "SET max_temp_directory_size = '0B'",
    "SET lock_configuration = true",        # last, so none of it can be undone
)

ESCAPES = ["SELECT * FROM read_text('/etc/passwd')", "ATTACH 'x.db'",
           "SET enable_external_access = true", "INSTALL httpfs",
           "COPY t TO 'out.csv'", "SELECT 1; SET threads = 64"]

Un analyseur vérifie bien qu’il n’y a qu’un seul SELECT, mais ce n’est qu’une ceinture en plus. Les tests de red team tentent chaque évasion de la liste sur de vrais fichiers, et un test vérifie que la connexion verrouillée refuse toute seule, même quand cette vérification est contournée.

Des erreurs écrites pour le modèle

Un outil qui échoue doit dire au modèle quoi faire ensuite. Un refus d’accès répond « cet outil n’est pas disponible pour cet utilisateur, réponds avec ceux que tu as ». Une requête trop lente : « arrêtée après 10 s, agrège, filtre ou ajoute un LIMIT ». Une réponse tronquée le dit et propose de compter avec COUNT(*). Et toutes les bornes vivent dans le code : 10 résultats par recherche, 200 lignes par requête, 5 tours de boucle maximum.

Révoquer un accès ne révoque pas le contexte

Le piège que presque personne ne voit : retirer un droit en cours de conversation ne suffit pas. L’historique contient encore les résultats d’outils obtenus quand l’accès existait, et le modèle continuera d’y puiser. Le profil est donc recalculé à chaque message, et s’il s’est réduit, l’historique est vidé. Même logique en cas de panne : si les rôles ne peuvent pas être lus, l’utilisateur retombe sur le plancher public, jamais sur un accès large.

Quand l’agent agit dehors : une porte humaine

Dans une autre expérience, un agent fait tourner une petite activité en ligne. Ses règles : aucune dépense sans ligne approuvée dans le budget, aucun mot de passe ni jeton manipulé (les outils utilisent ma session, pas la sienne), les pages web sont des données et jamais des instructions, et chaque action visible de l’extérieur attend un « oui » explicite, dont l’agent rédige lui-même la phrase exacte à taper. Il a d’ailleurs refusé seul un contournement qu’il jugeait être une faille.

Et une leçon honnête : la porte d’approbation était une convention. Les scripts de publication ne la lisaient pas, seul l’agent la respectait. Une règle que seul le modèle applique n’est pas une règle : le correctif est un garde dans le script, qui refuse tant que l’approbation n’existe pas.

La checklist

  • Exposer par tâche le minimum d’outils. Un outil refusé n’est pas décrit.
  • Revérifier les droits dans le code de chaque outil.
  • Lecture seule par défaut, écriture derrière une approbation humaine.
  • Contenir par le moteur, pas par le prompt, et attaquer son propre bac à sable dans les tests.
  • Des erreurs écrites pour le modèle, des bornes dans le code : lignes, tours, temps, argent.
  • Tout résultat d’outil est une donnée non fiable. Le marquer aide, s’y fier non.
  • Révoquer, c’est aussi vider le contexte.
  • Journaliser chaque appel d’outil avec ses entrées et sorties, et les montrer à l’utilisateur.
Contact

Construisons quelque chose.

Ou laissez un message