Trois gardiens de middleware LangChain qui empêchent les agents de divulguer des secrets
Empêchez un agent d’envoyer des clés API au modèle ou de supprimer des fichiers sans demande : PIIMiddleware, HumanInTheLoopMiddleware, limites de appel et raison pour laquelle l’ordre des middleware détermine la sécurité.
Coller des informations sensibles dans une fenêtre de chat est facile à faire, mais aussi facile à oublier. Peut-être qu’un fichier a été envoyé à ChatGPT pour le débogage et que vous avez arrêté l’opération après l’avoir envoyé, vous demandant s’il contenait une clé API, un mot de passe de base de données ou un jeton de client. Vous relisez le message, fermez l’onglet et décidez que tout est probablement en ordre. Ce n’est sans doute pas contraire aux règles.
Un agent aggrave encore la situation, car il effectue automatiquement des actions similaires à de nombreuses reprises au cours d’une session, sans vous en informer. Donnez-lui accès à un codebase pour une revue, et il ouvrira tôt ou tard un fichier .env contenant quelque chose comme OPENAI_API_KEY=sk-proj-.... En l’absence de mesures de protection, cette clé est transmise telle quelle au modèle linguistique, se retrouve dans les journaux d’audit et reste stockée dans les archives du fournisseur. Il est impossible de savoir si elle sera transmise plus loin, vers un ensemble de données utilisé pour le fine-tuning ou un service de journalisation, depuis votre côté.
Quelques tours plus tard, le même agent lance un outil de nettoyage et supprime un répertoire que vous n’avez jamais mentionné, enregistrant discrètement delete_directory(path="./tmp_backups") avant de continuer. Vous vous en apercevez lorsque un fichier dont vous avez besoin a disparu.
Il s’agit de deux échecs distincts, sans bug ni hallucination majeure. L’agent ne faisait simplement pas la distinction entre ce qu’il pouvait faire et ce qu’il devait faire. Le middleware constitue la solution, et les mêmes principes sous-tendent les protections présentes dans des outils tels que Cursor et GitHub Copilot. Voici comment les intégrer dans votre propre agent.
Quel est le rôle du middleware
Imaginez l’agent comme une ligne de production. Les données arrivent de vous, de fichiers et de résultats de recherche, se rendent au modèle qui prend des décisions, lesquelles se transforment en actions. Un middleware constitue un point de contrôle sur cette ligne : il intercepte les données, effectue une vérification ou une transformation, puis les transmet telles quelles, les modifie ou les bloque.
Le moment du traitement est essentiel. S’il est trop tard, les données sensibles ont déjà franchi une limite. S’il est trop tôt, vous examinez le mauvais élément.
Le scan secret de Copilot a lieu au niveau de la sortie. Avant qu’une suggestion n’apparaisse dans votre éditeur, un outil de reconnaissance de motifs l’examine, et tout élément ressemblant à une clé commençant par sk-, ghp_ ou AKIA est supprimé. La fonction Review Changes de Cursor intervient avant toute écriture dans le fichier : l’exécution est complètement suspendue, un comparatif est affiché et l’agent ne peut pas continuer tant que vous n’avez pas donné votre accord. Si vous refusez, l’écriture ne a jamais lieu et l’agent cherche une autre solution.
L’un protège ce qui sort du modèle, l’autre ce que le modèle pourrait faire. LangChain vous permet de créer les deux, ainsi que des limites de ressources.
Détecter les secrets : PIIMiddleware
Votre agent ne touche jamais directement au système de fichiers. Il ne voit que ce qui apparaît dans sa fenêtre de contexte, c’est-à-dire ce que vous tapez et ce que ses outils renvoient. Les données sensibles peuvent donc entrer de deux manières uniquement : vous les collez, ou un outil les lit depuis le disque, comme lorsqu’un balayage de répertoire ouvre un fichier .env. Dans les deux cas, le contenu passe par la couche de middleware avant que le modèle ne voie quoi que ce soit, et c’est à cet égard critique que PIIMiddleware intervient.
Il existe un paramètre, apply_to_tool_results, que la plupart des tutoriels utilisent sans l’expliquer. Sa valeur par défaut est False. Activez uniquement apply_to_input=True pour couvrir le cas où vous collez des données sensibles, tandis que le cas où l’agent lit un fichier reste entièrement non sécurisé. Un agent disposant d’accès aux fichiers peut facilement récupérer tout le contenu d’un .env en tant que résultat fourni par un outil, et le modèle le reçoit alors.
Une configuration minimale et correcte ressemble à ceci :
from langchain.agents.middleware import PIIMiddleware
PIIMiddleware(
"email",
strategy="redact", # replaces match with [REDACTED_EMAIL]
apply_to_input=True, # scans what you type
apply_to_tool_results=True, # scans what tools return - never skip this
)
Les clés API sont celles qui provoquent des incidents réels, et aucun détecteur de stocks ne connaît le format de votre clé, c’est pourquoi vous fournissez une expression régulière. Celle-ci couvre les préfixes courants :
import re
API_KEY_PATTERN = r"(?:sk-|ghp_|AKIA)[a-zA-Z0-9]{20,48}"
# sk- → OpenAI and Anthropic keys
# ghp_ → GitHub personal access tokens
# AKIA → AWS access key IDs
PIIMiddleware(
"api_key",
detector=API_KEY_PATTERN,
strategy="redact",
apply_to_input=True,
apply_to_tool_results=True,
)
Le middleware propose quatre stratégies :
redactremplace la valeur correspondante par[REDACTED_FIELDNAME]. Elle convient aux journaux, aux traces et aux pipelines unidirectionnels.maskremplace les caractères par des astérisques tout en conservant la longueur, utile lorsque l’on souhaite indiquer qu’une valeur existe sans la révéler.blockgénère une exception et met fin à l’exécution complète. L’utiliser lorsque la tolérance est nulle, dans des environnements à haute sécurité.hashsubstitue un hash déterministe, utile lorsqu’il faut suivre une entité sans en stocker la valeur.
La plupart des gens pensent que block est le choix le plus sûr, mais pour les résultats des outils, c’est généralement le mauvais choix. Supposons que l’agent lit 30 fichiers lors d’un scan et qu’un d’eux contienne une adresse e-mail dans un commentaire. Avec block, toute la session est interrompue. redact supprime cette valeur et permet la poursuite du travail, ce qui est presque toujours ce que l’on souhaite. Conservez block pour les situations où la simple présence de données sensibles signifie que l’agent est parvenu là où il ne devrait pas aller.
Testez-vous : si apply_to_input=True est défini et que apply_to_tool_results reste à sa valeur par défaut, quelle situation spécifique reste non protégée ? Vous devriez pouvoir répondre en une phrase.
Arrêter les suppressions silencieuses : HumanInTheLoopMiddleware
Retour au répertoire supprimé. HumanInTheLoopMiddleware est le modèle Review Changes de Cursor transformé en primitive de code. Avant que n’importe quel outil destructeur ou modificateur ne s’exécute, l’agent pause et enregistre son état. Vous voyez exactement ce qu’il compte faire et devez l’approuver avant que quoi que ce soit ne se produise.
Pauser est la partie facile. Reprendre là où l’on s’était arrêté est la partie difficile. Pour revenir à l’état exact de l’agent, éventuellement des heures ou des jours plus tard et après un redémarrage du processus, il est nécessaire que l’état de la conversation, l’action en attente ainsi que le point dans son raisonnement soient stockés quelque part de manière fiable. Ce stockage est appelé checkpointer. Sans lui, l’interruption se produit, l’état disparaît et la prochaine invocation commence à zéro, ce qui fait que le mécanisme d’approbation met simplement fin à la session.
La pause a lieu après que le modèle a décidé de ce qu’il doit faire, mais avant que n’importe quel outil ne s’exécute. C’est la seule fenêtre praticable, car la décision est visible et rien d’irréversible n’a eu lieu.
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.sqlite import SqliteSaver
# The checkpointer is not optional. Without it, resume is impossible.
with SqliteSaver.from_conn_string("./review_agent.db") as checkpointer:
agent = create_agent(
model="anthropic:claude-sonnet-4-20250514",
tools=[read_file, list_directory, write_file, search_codebase],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True, # always pause before writing
"read_file": False, # reading is safe - no pause needed
"list_directory": False,
"search_codebase": False,
}
),
],
checkpointer=checkpointer, # saved to SQLite, persists across restarts
)
La reprise après approbation se présente comme suit :
# First call — agent hits the interrupt at write_file and pauses
result = agent.invoke(
{"messages": [{"role": "user", "content": "Review and fix the config files"}]},
config={"configurable": {"thread_id": "session-001"}}
# thread_id ties the saved state to this specific session
)
# result.interrupted == True
# result.pending_tool_call == {"name": "write_file", "args": {"path": "src/config.py", ...}}
# You show this to the user and wait for approval
# User approves - resume the same thread
final_result = agent.invoke(
Command(resume=True),
config={"configurable": {"thread_id": "session-001"}}
# same thread_id - loads state from the checkpointer and picks up where it stopped
)
Un seul problème peut gâcher toute une après-midi. interrupt_on={"write_file": True} correspond au nom de l’outil en tant que chaîne de caractères exacte. Si votre outil s’appelle réellement file_write ou update_file, l’interruption ne se produit jamais et la requête est exécutée en silence. Vérifiez les journaux de suivi pour connaître le vrai nom lorsque l’outil est invoqué, puis utilisez-le comme critère de correspondance.
Deux limites pour deux modes d’échec
À chaque étape, l’agent appelle le modèle, qui choisit une action et invoque parfois des outils ; une seule appel au modèle peut déclencher plusieurs appels à des outils en même temps. Il s’agit de ressources distinctes, et l’une n’influence pas l’autre.
ModelCallLimitMiddleware compte les appels aux modèles, tandis que ToolCallLimitMiddleware compte les exécutions des outils. Dix appels aux modèles, chacun déclenchant trois appels parallèles aux outils, donnent un total de 30 exécutions, ce que un plafond fixé uniquement sur les appels aux modèles ne détecterait jamais. Le principe fonctionne également dans l’autre sens : un agent qui se base uniquement sur sa propre réflexion continue d’appeler le modèle sans utiliser d’outil, de sorte que le compteur des outils reste à zéro et ne déclenche jamais d’alerte.
from langchain.agents.middleware import ModelCallLimitMiddleware, ToolCallLimitMiddleware
ModelCallLimitMiddleware(
max_calls=30,
on_limit="raise", # raises MaxCallsExceeded - catch this in your application
)
ToolCallLimitMiddleware(
max_calls=60,
on_limit="raise",
)
Utilisez les deux, car chacun couvre des aspects que l’autre ne peut pas aborder.
Avec on_limit="raise" contre on_limit="return" : choisissez "return" pour une session interactive où un résultat partiel est utile, comme un résumé de ce qui a été examiné jusqu’à présent. Choisissez "raise" lorsque un résultat inachevé pourrait perturber des étapes ultérieures.
Faites également attention aux chiffres. Une limite de max_calls=10 semble être un plafond sûr pour un test rapide, mais un agent ayant un plan en plusieurs étapes peut consacrer 8 appels rien que dans son premier tour. La limite l’interrompt alors au milieu de la tâche et renvoie un résultat tronqué, ce qui peut vous faire perdre vingt minutes avant que les journaux ne l’expliquent. La solution n’est pas de baisser la limite, mais de la fixer en fonction des besoins réels de la tâche.
Le ordre est la garantie de sécurité
Le middleware s’exécute dans l’ordre dans lequel vous le listez, et ce n’est pas un détail insignifiant. Si un middleware qui réécrit ou résume le contenu du message précède PIIMiddleware, la vérification des PII analyse un texte qui a déjà été modifié. Lorsque les modifications ont compressé, paraphrasé ou restructuré le contenu au point que votre modèle de reconnaissance ne correspond plus, la fonction de censure le manque ; le secret se trouve déjà dans le contexte du modèle et la censure a été exécutée après que les dommages ont été causés.
Pour cette pile de sécurité, l’ordre est le suivant :
middleware=[
# PIIMiddleware always first — it must see raw, untransformed data
PIIMiddleware("api_key", detector=API_KEY_PATTERN, strategy="redact",
apply_to_input=True, apply_to_tool_results=True),
PIIMiddleware("email", strategy="redact",
apply_to_input=True, apply_to_tool_results=True),
# Limits next - exact position within the group is flexible
ModelCallLimitMiddleware(max_calls=30, on_limit="raise"),
ToolCallLimitMiddleware(max_calls=60, on_limit="raise"),
# Human-in-the-loop last in the safety group
# (it fires in the after_model hook regardless of list position,
# but last is a readable convention)
HumanInTheLoopMiddleware(interrupt_on={"write_file": True}),
]
Considérez cette liste comme un pipeline et non comme un registre de plugins. Le quatrième élément ne reçoit pas le message original, mais plutôt ce que les éléments un à trois ont laissé derrière eux. Déplacez PIIMiddleware en dessous de tout élément qui modifie le contenu du message, et la garantie disparaîtra alors.
Autre vérification : que se passe-t-il si PIIMiddleware est placé après un middleware qui résume l’historique des conversations ? Si vous pouvez répondre sans avoir à remonter la page, la règle d’ordre est respectée.
Assemblage d’un agent de revue de code
Combinez maintenant tout cela en un agent qui fonctionne comme les outils commerciaux. Cet agent dispose de quatre outils : read_file, list_directory, write_file et search_codebase. Trois couches de sécurité les entourent : les secrets n’atteignent jamais le modèle, les écritures nécessitent votre approbation et l’utilisation des ressources est soumise à un plafond strict.
from langchain.agents import create_agent
from langchain.agents.middleware import (
PIIMiddleware,
HumanInTheLoopMiddleware,
ModelCallLimitMiddleware,
ToolCallLimitMiddleware,
)
from langgraph.checkpoint.sqlite import SqliteSaver
# Custom pattern for secrets common in codebases
API_KEY_PATTERN = r"(?:sk-|ghp_|AKIA)[a-zA-Z0-9]{20,48}"
# sk- → OpenAI / Anthropic keys
# ghp_ → GitHub personal access tokens
# AKIA → AWS access key IDs
with SqliteSaver.from_conn_string("./review_agent.db") as checkpointer:
agent = create_agent(
model="anthropic:claude-sonnet-4-20250514",
tools=[read_file, list_directory, write_file, search_codebase],
middleware=[
# 1. Catch secrets before they reach the model - on both surfaces
PIIMiddleware(
"api_key",
detector=API_KEY_PATTERN,
strategy="redact",
apply_to_input=True,
apply_to_tool_results=True, # this is the one that catches .env reads
),
PIIMiddleware(
"email",
strategy="redact",
apply_to_input=True,
apply_to_tool_results=True,
),
# 2. Hard resource limits - stops infinite loops and runaway sessions
ModelCallLimitMiddleware(max_calls=30, on_limit="raise"),
ToolCallLimitMiddleware(max_calls=60, on_limit="raise"),
# 3. Approval gate - nothing gets written without your explicit sign-off
HumanInTheLoopMiddleware(
interrupt_on={
"write_file": True,
"read_file": False,
"list_directory": False,
"search_codebase": False,
}
),
],
checkpointer=checkpointer, # required for interrupt/resume to work
)
Lorsque l’agent souhaite écrire un fichier, l’échange se déroule comme suit :
Agent: I'd like to update src/config.py to fix the circular import.
Here is what I plan to write:
--- src/config.py ---
from typing import Optional
from pydantic import BaseModel
class Settings(BaseModel):
debug: bool = False
database_url: str = "sqlite:///app.db"
...
Approve this change? (yes/no)
User: yes
Agent: Written. Moving to tests/test_config.py next.
The agent cannot touch anything silently. Every write surfaces for review before it happens. Secrets in any file the agent reads are replaced with [REDACTED_API_KEY] before reaching the model — which also means they never appear in the approval prompt you read. What you are reviewing is already clean.
Deux extensions simples méritent d’être connues. Si la base de code contient des données personnelles, comme des numéros de téléphone dans les fichiers de test, ajoutez une entrée PIIMiddleware("phone", strategy="redact") avant celle relative aux e-mails. Chaque détecteur de PII fonctionne indépendamment sur la même entrée, donc l’ordre à l’intérieur de ce groupe n’a pas d’importance. Et si vous préférez une dégradation progressive plutôt qu’une arrêt brutale, changez on_limit="raise" en on_limit="return" pour les deux middlewares de limitation ; l’agent renverra alors le résultat partiel qu’il possède.
Désormais, l’agent ne peut pas divulguer de secrets au modèle, ne peut pas modifier de fichiers sans votre approbation et ne peut pas dépasser le plafond de ressources que vous avez fixé. Une limitation reste : il ne peut pas maintenir sa cohérence tout au long d’une session prolongée. L’examen de centaines de fichiers, avec des dizaines d’appels à des outils, remplira la fenêtre de contexte avant même que la session ne se termine, ce qui fait que l’agent perd de vue ce qu’il a déjà traité et que ses suggestions deviennent erratiques. Il s’agit d’un problème distinct de celui résolu ici, qui concerne le contrôle de ce que l’agent peut modifier.