Tres protectores de middleware de LangChain que impiden que los agentes filtren secretos
Impida que un agente envíe claves API al modelo o elimine archivos sin permiso: PIIMiddleware, HumanInTheLoopMiddleware, límites de llamadas y por qué el orden de los middleware determina la seguridad.
Pegar algo sensible en una ventana de chat es algo fácil de hacer y fácil de olvidar. Quizás se envió un archivo a ChatGPT con fines de depuración y, después de enviarlo, te quedaste paralizado, preguntándote si contenía una clave API, una contraseña de base de datos o un token de cliente. Relees el mensaje, cierras la pestaña y decides que probablemente está bien. Probablemente no se trate de una violación de políticas.
Un agente empeora la situación, ya que realiza lo mismo de forma automática, muchas veces por sesión y sin avisarte. Si se le da acceso a un repositorio de código para su revisión, eventualmente abrirá un archivo .env que contiene algo como OPENAI_API_KEY=sk-proj-.... Al no haber medidas de protección, esa clave llega tal cual al modelo de lenguaje, queda registrada en los logs de seguimiento y permanece en los registros del proveedor. Si llega a transmitirse más allá, a un conjunto de ajustes finos o a un servicio de registro del que no tienes conocimiento, eso es imposible de saber desde tu lado.
Pocos pasos después, el mismo agente llama a una herramienta de limpieza y elimina un directorio del que nunca mencionaste, registrando silenciosamente delete_directory(path="./tmp_backups") y continuando con su tarea. Lo descubres cuando un archivo que necesitas ha desaparecido.
Se trata de dos fallos separados, sin ningún error ni alucinación grave. El agente simplemente no tenía una distinción clara entre lo que podía hacer y lo que debía hacer. La solución está en el middleware, y las mismas ideas subyacen a las protecciones de herramientas como Cursor y GitHub Copilot. Aquí te explico cómo integrarlas en tu propio agente.
Qué hace el middleware
Imagínese al agente como una línea de producción. Los datos llegan desde usted, desde archivos y de los resultados de búsqueda; van al modelo, que toma decisiones que se convierten en acciones. Un middleware es un punto de control en esa línea: intercepta los datos, realiza una verificación o transformación, y luego los transmite sin cambios, los modifica o los detiene.
El momento de intervención es crucial. Si se hace demasiado tarde, los datos sensibles ya han traspasado un límite. Si se hace demasiado temprano, se está inspeccionando algo incorrecto.
El escaneo secreto de Copilot se realiza en la fase de salida. Antes de que aparezca una sugerencia en tu editor, un detector de patrones la examina y cualquier elemento similar a una clave que comience con sk-, ghp_ o AKIA es suprimido. La función Review Changes de Cursor se ejecuta antes de cualquier escritura en el archivo: la ejecución se detiene por completo, se muestra una diferencia y el agente no puede continuar hasta que lo apruebes. Si lo rechazas, la escritura no tiene lugar y el agente busca otra forma de proceder.
Uno protege lo que sale del modelo, el otro lo que el modelo podría hacer. LangChain te permite crear ambos, además de establecer límites de recursos.
Detección de secretos: PIIMiddleware
Su agente nunca interactúa directamente con el sistema de archivos. Solo ve lo que aparece en su ventana de contexto, es decir, lo que usted escribe y lo que devuelven sus herramientas. Por lo tanto, los datos sensibles pueden ingresar de exactamente dos maneras: usted los pega o una herramienta los lee del disco, como cuando un escaneo de directorios abre un archivo .env. Ambos caminos pasan por la capa de middleware antes de que el modelo vea algo, y ese punto crítico es donde actúa PIIMiddleware.
Existe una bandera, apply_to_tool_results, que la mayoría de los tutoriales utilizan sin explicarla. Su valor por defecto es False. Solo active apply_to_input=True para cubrir el caso en que se pega algo sensible, mientras que el caso en que el agente lee un archivo permanece sin solución. Un agente con acceso a archivos obtendrá felizmente todo el contenido de un .env como resultado de una herramienta, y el modelo lo recibirá.
Una configuración mínima y correcta se ve así:
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
)
Las claves API son las que causan incidentes reales, y ningún detector de existencias conoce el formato de tu clave, por lo que debes proporcionar una expresión regular. Esta cubre los prefijos más comunes:
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,
)
El middleware ofrece cuatro estrategias:
redactreemplaza el valor coincidente por[REDACTED_FIELDNAME]. Es adecuado para registros, trazas y pipelines unidireccionales.masksustituye los caracteres por asteriscos pero mantiene la longitud, útil cuando se desea indicar que existe un valor sin revelarlo.blockgenera una excepción y finaliza toda la ejecución. Úselo cuando la tolerancia es cero, en entornos de alta seguridad.hashintroduce un hash determinista, útil cuando es necesario rastrear una entidad sin almacenar su valor.
La mayoría de las personas asumen que block es la opción más segura, pero para los resultados de las herramientas suele ser la incorrecta. Supongamos que el agente lee 30 archivos durante un escaneo y uno de ellos contiene una dirección de correo electrónico en un comentario. Con block, toda la sesión se interrumpe. redact elimina ese valor y permite que el trabajo continúe, lo cual es casi siempre lo que se desea. Reserve block para situaciones en las que la mera presencia de datos sensibles implica que el agente llegó a un lugar al que no debería haber ido.
Ponga a prueba sus conocimientos: si se establece apply_to_input=True y apply_to_tool_results permanece en su valor por defecto, ¿qué situación específica queda sin protección? Debería poder responder en una sola oración.
Detener las eliminaciones silenciosas: HumanInTheLoopMiddleware
Volvemos al directorio eliminado. HumanInTheLoopMiddleware es el patrón de Revisión de Cambios de Cursor convertido en una primitiva de código. Antes de que se ejecute cualquier herramienta destructiva o modificadora, el agente hace una pausa y guarda su estado. Puedes ver exactamente lo que pretende hacer y debes aprobarlo antes de que ocurra algo.
Hacer una pausa es la parte fácil. Reanudar es la parte difícil. Volver al estado exacto en el que se encontraba el agente, posiblemente horas o días después y tras un reinicio del proceso, requiere que el estado de la conversación, la acción pendiente y el lugar donde se encuentra el agente en su razonamiento sean almacenados en algún lugar duradero. Ese almacenamiento se denomina checkpointer. Sin uno, se produce una interrupción, el estado desaparece y la siguiente llamada comienza desde cero, por lo que la puerta de aprobación simplemente termina la sesión.
La pausa ocurre después de que el modelo haya decidido qué hacer, pero antes de que se ejecute ninguna herramienta. Ese es el único momento viable, ya que la decisión es visible y no ha ocurrido nada irreversible.
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 continuación después de la aprobación se ve así:
# 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 pequeño error puede arruinar toda una tarde. interrupt_on={"write_file": True} busca el nombre de la herramienta como cadena exacta. Si su herramienta realmente se llama file_write o update_file, la interrupción nunca se activa y la llamada pasa desapercibida. Revise los registros de seguimiento para ver el nombre real cuando se invoque la herramienta y utilícelo como referencia.
Dos límites para dos modos de fallo
Cada paso implica que el agente llame al modelo, el cual elige una acción y a veces invoca herramientas; además, una sola llamada al modelo puede iniciar varias llamadas a herramientas al mismo tiempo. Estos son recursos separados, y uno no cuenta como el otro.
ModelCallLimitMiddleware cuenta las llamadas al modelo, mientras que ToolCallLimitMiddleware cuenta las ejecuciones de las herramientas. Diez llamadas al modelo que cada una derivan en tres llamadas paralelas a herramientas suman un total de 30 ejecuciones, y un límite solo para las llamadas al modelo nunca se daría cuenta de ello. Funciona también en sentido inverso: un agente que se queda en un bucle de razonamiento propio sigue accediendo al modelo sin utilizar ninguna herramienta, por lo que el contador de herramientas permanece en cero y nunca se activa.
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",
)
Utilice ambos, ya que cada uno cubre algo que el otro no puede.
Sobre on_limit="raise" frente a on_limit="return": elija "return" para una sesión interactiva en la que un resultado parcial sea útil, como un resumen de lo revisado hasta el momento. Elija "raise" cuando un resultado incompleto podría dañar algo en etapas posteriores.
Tenga también cuidado con los números. Un límite de max_calls=10 parece un umbral seguro para una prueba rápida, pero un agente con un plan de múltiples pasos puede gastar 8 llamadas solo en su primer turno. El límite entonces lo interrumpe a mitad de tarea y devuelve un resultado truncado, y usted podría perder veinte minutos antes de que los registros lo expliquen. La solución no es reducir el límite, sino establecerlo según lo que realmente necesite la tarea.
El orden es la garantía de seguridad
El middleware se ejecuta en el orden en que lo listas, y eso no es un detalle menor. Si algún middleware que reescribe o resume el contenido del mensaje se coloca antes de PIIMiddleware, la verificación de PII escanea texto que ya ha sido modificado. Cuando el cambio comprime, parafrasea o reestructura el contenido de tal manera que tu patrón ya no coincide, la redacción lo pasa por alto; el dato confidencial ya se encuentra en el contexto del modelo y la redacción se ejecuta después de que ya haya ocurrido el daño.
Para la pila de seguridad aquí, el orden es:
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}),
]
Considere la lista como una tubería y no como un registro de complementos. El cuarto elemento no recibe el mensaje original, sino lo que dejaron los elementos del uno al tres. Mueva PIIMiddleware por debajo de cualquier componente que modifique el contenido del mensaje y la garantía desaparecerá silenciosamente.
Otra verificación: ¿qué sucede si PIIMiddleware se coloca después de un middleware que resume el historial de conversaciones? Si puede responder sin tener que desplazarse hacia arriba, entonces la regla de ordenamiento es correcta.
Construyendo un agente de revisión de código
Ahora combine todo esto en un agente que funcione como las herramientas comerciales. Cuenta con cuatro herramientas: read_file, list_directory, write_file y search_codebase. Tres capas de seguridad las protegen: los datos confidenciales nunca llegan al modelo, las escrituras requieren su aprobación y el uso de recursos tiene un límite estricto.
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
)
Cuando el agente desea escribir un archivo, el intercambio se realiza de la siguiente manera:
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.
Dos extensiones simples merecen ser conocidas. Si la base de código contiene datos de los usuarios, como números de teléfono en las pruebas, añada una entrada PIIMiddleware("phone", strategy="redact") antes de la relacionada con el correo electrónico. Cada detector de PII se ejecuta de forma independiente sobre la misma entrada, por lo que el orden dentro de ese grupo no es importante. Y si prefiere una interrupción gradual en lugar de un cierre brusco, cambie on_limit="raise" a on_limit="return" en ambos middleware de límite, y el agente devolverá cualquier resultado parcial que tenga.
Ahora el agente no puede filtrar secretos al modelo, no puede modificar archivos a menos que usted lo apruebe y no puede superar el límite de recursos que haya establecido. Sigue habiendo una limitación: no puede mantener la coherencia durante una sesión larga. Revisar cientos de archivos, con decenas de llamadas a herramientas, llenará la ventana de contexto antes de que termine, por lo que el agente pierde el rastro de lo que ya ha abordado y sus sugerencias se desvían. Ese es un problema distinto al que se resuelve aquí, que consiste en controlar qué puede acceder el agente.