Три проміжкові захисники LangChain, які запобігають витоку секретів у агентах
Забороніть агенту надсилати ключі API до моделі чи видаляти файли без запиту: PIIMiddleware, HumanInTheLoopMiddleware, обмеження на кількість викликів та чому порядок роботи мідлвейрів визначає безпеку.
Вставити щось конфіденційне у вікно чату легко, і так само легко про це забути. Можливо, ви надіслали файл до ChatGPT для дебагування та потім замерли, не знаючи, чи містив він ключ API, пароль до бази даних чи токен клієнта. Ви перечитуєте повідомлення, закриваєте вкладку та вирішуєте, що, ймовірно, все гаразд. Але це, ймовірно, суперечить правилам.
Ситуацію погіршує агент, оскільки він автоматично виконує аналогічні дії багато разів протягом сеансу та не повідомляє про це користувача. Якщо надати йому доступ до кодової бази для перегляду, він зрештою відкриє файл .env, який містить щось на кшталт OPENAI_API_KEY=sk-proj-.... Без належних заходів безпеки цей ключ потрапляє без змін до мовної моделі, опиняється у журналах логів та зберігається у записах постачальника. Чи потрапить він далі — до набору даних для налаштування чи до сервісу обліку — з вашого боку неможливо дізнатися.
Через кілька кроків той самий агент викликає інструмент для очищення та видаляє каталог, про який ви ніколи не згадували, тихо записуючи delete_directory(path="./tmp_backups") та продовжуючи роботу. Ви помічаєте це тоді, коли файл, який вам потрібен, зникає.
Це два окремі збої без будь-яких помилок та без серйозних хибних дій. У агента просто не було чіткого розділення між тим, що він може робити, і тим, що має робити. Рішенням є використання мідлвейру, і саме ці принципи лежать в основі захисту у інструментах на кшталт Cursor та GitHub Copilot. Ось як можна вбудувати їх у власного агента.
Що робить мідлвейр
Уявіть агента як виробничу лінію. Дані надходять від вас, з файлів та з результатів пошуку, потрапляють до моделі, яка приймає рішення, що перетворюються на дії. Проміжне програмне забезпечення є контрольною точкою на цій лінії. Воно перехоплює дані, проводить перевірку чи їх трансформацію, а потім передає їх без змін, модифікує чи зупиняє їх передачу.
Час — це все. Якщо запізно, конфіденційні дані вже перетнули межу. Якщо занадто рано, ви перевіряєте неправильні дані.
Таємне сканування Copilot відбувається на етапі отримання результату. Перш ніж пропозиція з’явиться у вашому редакторі, її перевіряє механізм пошуку шаблонів, і все, що має вигляд ключа, який починається з sk-, ghp_ або AKIA, пригнічується. Функція Review Changes у Cursor діє перед будь-яким збереженням файлу: виконання повністю зупиняється, відображається різниця між версіями, і агент не може продовжити роботу, поки ви не схвалите зміни. Якщо ви відхилите пропозицію, збереження не відбудеться, і агент шукатиме інший спосіб.
Один механізм захищає те, що виходить з моделі, інший — те, що модель може зробити. LangChain дозволяє створювати обидва механізми, а також встановлювати обмеження на ресурси.
Виявлення конфіденційної інформації: PIIMiddleware
Ваш агент ніколи не взаємодіє безпосередньо з файловою системою. Він бачить лише те, що потрапляє у його вікно контексту, тобто те, що ви вводите, та те, що повертають його інструменти. Тому конфіденційні дані можуть потрапити лише двома способами: ви їх вставляєте, або інструмент читає їх з диска, наприклад під час сканування каталогу, коли відкривається файл .env. Обидва шляхи проходять через шар проміжного програмування, перш ніж модель щось побачить, і саме в цьому контрольному етапі діє PIIMiddleware.
Існує прапорець apply_to_tool_results, який більшість навчальних матеріалів використовує без пояснень. Його значення за замовчуванням — False. Увімкніть лише apply_to_input=True, щоб охопити випадок, коли ви вставляєте щось конфіденційне, тоді як випадок, коли агент читає файл, залишається неконтрольованим. Агент із доступом до файлів з радістю завантажить весь файл .env як результат роботи інструменту, і модель отримає його.
Мінімальна правильна налаштованість виглядає так:
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
)
Саме API-ключі спричиняють справжні проблеми, і жоден детектор запасів не знає формату вашого ключа, тому ви надаєте регулярний вираз. Цей вираз охоплює поширені префікси:
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,
)
Мідлвейр пропонує чотири стратегії:
redactзамінює відповідний елемент на[REDACTED_FIELDNAME]. Вона підходить для журналів подій, трекінгу та однонапрямкових потоків даних.maskзамінює символи на зірки, але зберігає їхню довжину; це корисно, коли потрібно показати, що значення існує, не розкриваючи його.blockвикликає виняток та завершує всю обробку. Використовуйте його, коли толерантність до ризиків дорівнює нулю, у ситуаціях високої безпеки.hashзамінює дані на детермінований хеш; це корисно, коли потрібно відстежувати елемент, не зберігаючи його значення.
Більшість людей вважають, що block — це найбезпечніший варіант, але для результатів роботи інструментів це зазвичай неправильно. Припустимо, агент прочитав 30 файлів під час сканування, і в одному з них у примітці є адреса електронної пошти. За використанням block це призводить до завершення всієї сесії. redact видаляє це значення та дозволяє продовжити роботу, що майже завжди є бажаним. Використовуйте block лише у ситуаціях, коли сама наявність конфіденційних даних означає, що агент потрапив туди, куди не повинен був.
Перевірте себе: якщо встановлено apply_to_input=True, а apply_to_tool_results залишається у своєму стандартному значенні, яка саме ситуація залишається незахищеною? Ви повинні змогти відповісти одним реченням.
Зупинка тихих видалень: HumanInTheLoopMiddleware
Повернення до видаленої директорії. HumanInTheLoopMiddleware — це паттерн перегляду змін від Cursor, перетворений на кодову примітивну структуру. Перед тим, як запустити будь-який інструмент для змін чи видалення, агент зупиняється та зберігає свій стан. Ви бачите саме те, що він збирається зробити, і повинні схвалити це, перш ніж щось відбудеться.
Зупинка — це проста частина. Відновлення — складна частина. Щоб повернутися до точного стану, в якому був агент, можливо, через кілька годин чи днів після перезапуску процесу, необхідно зберігати стан розмови, поточну дію та місце агента у його логічних міркуваннях у якомусь надійному сховищі. Це сховище називається чекпоінтером. Без нього відбувається переривання, стан зникає, і наступний виклик починається з нуля, тож механізм схвалення просто завершує сеанс.
Пауза виникає після того, як модель вирішила, що робити, але до того, як буде запущений будь-який інструмент. Це єдиний придатний проміжок часу, оскільки рішення є видимим, і ще не сталося нічого незворотного.
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
)
Повторне запускання після схвалення виглядає так:
# 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
)
Одна проблема може «з’їсти» цілий день. interrupt_on={"write_file": True} збігається з ім’ям інструменту як точний рядок. Якщо ваш інструмент насправді називається file_write або update_file, переривання ніколи не виникає, і виклик проходить без проблем. Перевірте журнали слідування на справжнє ім’я під час виклику інструменту та враховуйте його.
Дві межі для двох режимів збою
На кожному кроці агент викликає модель, яка обирає дію та іноді використовує інструменти, причому один виклик моделі може одночасно запустити кілька викликів інструментів. Це окремі ресурси, і один не враховується через інший.
ModelCallLimitMiddleware підраховує кількість викликів моделей, тоді як ToolCallLimitMiddleware підраховує кількість виконань інструментів. Десять викликів моделей, кожен з яких призводить до трьох паралельних викликів інструментів, дають загалом 30 виконань, причому сам ліміт на виклики моделей ніколи цього не помітить. Це працює і навпаки. Агент, який постійно використовує власні механізми міркування, продовжує викликати модель, не торкаючись інструментів, тож лічильник інструментів залишається рівним нулю та ніколи не досягає ліміту.
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",
)
Використовуйте обидва варіанти, адже кожен покриває те, чого не може інший.
Щодо параметрів on_limit="raise" та on_limit="return": оберіть "return" для інтерактивної сесії, де корисним є частковий результат, наприклад узагальнення того, що було переглянуто до цього моменту. Оберіть "raise", коли неповний результат може пошкодити подальшу обробку даних.
Також будьте уважні з числами. Ліміт max_calls=10 здається безпечним для швидкого тестування, проте агент із багатоетапним планом може використати 8 викликів вже у першому своєму кроці. У такому разі ліміт перериває його роботу на півдорозі, і агент повертає скорочений результат; ви можете втратити двадцять хвилин, перш ніж журнали подій пояснять ситуацію. Рішенням не є зниження ліміту, а його налаштування відповідно до реальних потреб завдання.
Порядок — це гарантія безпеки
Мідлвери виконуються у тій послідовності, в якій ви їх вказуєте, і це не тривіальна деталь. Якщо якийсь мідлвер, який переписує або узагальнює зміст повідомлення, розташований перед PIIMiddleware, перевірка PII сканує текст, який вже був змінений. Коли зміни стискають, перефразовують або реструктурують контент так, що ваш шаблон більше не відповідає, процедура redaction його пропускає — секрет вже знаходиться у контексті моделі, і процедура redaction виконується після настання пошкодження.
Для цієї стеку безпеки послідовність є такою:
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}),
]
Уявіть цей список як конвеєр, а не як реєстр плагінів. Четвертий елемент не отримує початкового повідомлення — він отримує те, що залишили елементи з одного по три. Перемістіть PIIMiddleware під будь-який елемент, який працює з вмістом повідомлення, і гарантія буде порушена.
Ще одна перевірка: що станеться, якщо PIIMiddleware розташується після проміжного модуля, який узагальнює історію розмов? Якщо ви можете відповісти без необхідності прокручувати вгору, значить правило порядку дій справді діє.
Створення агента для перегляду коду
Тепер об’єднайте все це в агента, який буде працювати як комерційні інструменти. Він має чотири інструменти: read_file, list_directory, write_file та search_codebase. Їх оточують три рівні безпеки: конфіденційні дані ніколи не потрапляють до моделі, записи потребують вашого схвалення, а використання ресурсів має жорсткий ліміт.
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
)
Коли агент хоче записати файл, процес відбувається так:
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.
Варто знати про два прості розширення. Якщо база коду містить дані користувачів, наприклад номери телефонів у тестових конфігураціях, додайте запис PIIMiddleware("phone", strategy="redact") перед записом для електронної пошти. Кожен детектор PII працює незалежно з однаковим вхідним даним, тому порядок усередині цієї групи не має значення. А якщо ви волієте плавно припинити роботу, а не раптово зупинитися, змініть значення on_limit="raise" на on_limit="return" у обох мідлвейрах обмежень, і агент поверне той частковий результат, який у нього є.
Тепер агент не може витікати секрети до моделі, не може змінювати файли без вашого схвалення та не може перевищувати встановлений вами ліміт ресурсів. Залишається одне обмеження: він не може підтримувати послідовність протягом тривалої сесії. Огляд сотень файлів із десятками викликів інструментів заповнить вікно контексту ще до завершення роботи, тож агент втрачає контроль над тим, що вже розглядав, а його пропозиції стають хаотичними. Це окрема проблема від тієї, яку тут вирішують — а саме контролю над тим, що може робити агент.