Главная / Статьи

Три промежуточных компонента LangChain, предотвращающих утечку секретов у агентов

Защитите агента от отправки ключей API модели или удаления файлов без запроса: PIIMiddleware, HumanInTheLoopMiddleware, ограничения на вызовы и почему порядок использования мидлвэров определяет уровень безопасности.

Издатель: REACTAPP.TOP10 мин чтения2230 слов

Источники: **If You’re Not Using These Three Guards in Your AI Agent, It Has Already Sent Your Secrets to the Provider**

Вставка чего-то конфиденциального в окно чата — процесс простой, но его легко забыть. Возможно, файл попал в 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 сканирует уже изменённый текст. Когда изменения сжимают, перефразируют или реструктурируют содержимое так, что ваш шаблон больше не совпадает, процедура удаления конфиденциальной информации её пропускает; секрет уже находится в контексте модели, и процедура удаления выполняется после того, как ущерб уже нанесён.

Для данной стековой схемы безопасности порядок следующий:

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" в обоих мидлварах, отвечающих за ограничения, и агент вернёт любой частичный результат, который у него есть.

Теперь агент не может утечки секретов модели, не может изменять файлы без вашего разрешения и не может превысить установленный вами лимит ресурсов. Остается одно ограничение: он не может сохранять свою целостность в течение длительной сессии. Просмотр сотен файлов с десятками вызовов инструментов заполнит окно контекста до завершения работы, из-за чего агент теряет нить происходящего, и его предложения становятся бессвязными. Это отдельная проблема от той, которая решается здесь — контроля над тем, с чем может взаимодействовать агент.