Контроль над агентом имеет смысл только в тот момент, когда он уже собирается что-то сделать. Модель, которая «в целом безопасна», может в конкретном вызове инструмента выполнить опасное действие — и именно на этом шаге нужен перехват. Ниже разобрано, как устроен путь от вызова инструмента до вердикта, как из конфигурации собирается исполняемая политика, как работает заморозкарежим, при котором политика становится неизменяемой и любые попытки её ослабить блокируются и аудит, и почему часть проверок живёт на агенте, а часть — на шлюзе.
Точка перехвата: путь от вызова инструмента до вердикта
Любой вызов инструмента проходит через точку перехватаместо в коде, через которое обязательно проходит вызов инструмента до его выполнения. В синхронном варианте сначала запускается телеметрия: журнал аудита открывает запись и возвращает её идентификатор. Затем движок политик проверяет нарушение по агенту, имени инструмента и аргументам.
Дальше возможны три исхода. При нарушении инструмент не выполняется, а вызывающей стороне сообщается статус blocked и флаг mute=True — этот флаг означает, что ошибку не нужно показывать пользователю, а следует подавить её вывод. Если нарушения нет, но включён shadow-режимрежим, в котором инструмент не выполняется, а только имитируется его результат, инструмент тоже не запускается: возвращается статус simulated с результатом Success (Simulated) и пометкой meta={'shadow': True}. Иначе успех пишется в журнал аудита, и вызов разрешается к выполнению.
Асинхронный вариант делает те же шаги, но сообщает результат через поле allowed: при нарушении allowed=False и mute=True, в shadow-режиме allowed=False и shadow=True, при разрешении — {'allowed': True}.
Отдельная цепочка submit_request → execute работает иначе. submit_request создаёт ExecutionRequestобъект запроса на выполнение с идентификатором, контекстом агента, типом действия, параметрами и временем создания, затем проверяет права и при отказе помечает запрос статусом DENIED — это отказ в правах, запрос дальше не пойдёт. После этого проверяются политики — снова DENIED при отказе. Если проверки пройдены, оценивается риск, и запрос получает статус APPROVED — это значит, что он допущен к исполнению. Метод execute выполняет только запрос со статусом APPROVED; иначе запрос помечается статусом FAILED с ошибкой Request not approved for execution. При выполнении статус меняется на EXECUTING и вызывается диспетчеризация.
Создание и привязка агента
Агент создаётся через create_agent(agent_id, permissions, quota), который создаёт сессию в ядре и, если задана квота, устанавливает её в движке политик. В ядре генерируется session_id как UUID4 и создаётся AgentContextконтекст агента, включающий идентификатор, идентификатор сессии, время создания и права. Контекст сохраняется в таблице активных сессий по session_id.
В пространстве ядра create_agent_context(agent_id) регистрирует агента, создаёт для него диспетчер сигналов и виртуальную файловую систему, а контекст получает ссылку на ядро и уровень защиты RING_3_USERпользовательский уровень защиты в кольцевой модели: код выполняется с наименьшими привилегиями, без доступа к ядру и системным ресурсам. Идентификацию обеспечивают agent_id, session_id, а также права и квота.
Контекст для ABACмодель контроля доступа, где решение принимается по атрибутам субъекта, объекта и окружения задаётся отдельно через set_agent_context(agent_role, context), где context — словарь атрибутов. Например, is_shadow_mode читает из этого словаря атрибут shadow_mode.
Fail-closed: что происходит при недоступности контроля
Если движок политик отсутствует, _check_policy возвращает (False, "No policy engine configured; denying by default (fail closed)") для любого системного вызова, который не является self-scoped и не разрешён явным permissive-режимом. Для self-scoped вызовов или при _permissive возвращается (True, None).
В syscall() этот результат приводит к SyscallResult с кодом ошибки -2 (нарушение политики) и блокировке вызова. SYS_CHECKPOLICY при отсутствии движка возвращает success=True, но return_value={"allowed": False, "reason": "No policy engine configured; denying by default (fail closed)", ...}, если не включён permissive. При permissive возвращается {"allowed": True}.
Блокируются все вызовы, кроме self-scoped — собственной файловой системы и жизненного цикла. Они остаются разрешёнными, чтобы агент мог использовать свою память и корректно завершиться.
Порядок применения слоёв политики
В validate_request порядок такой: сначала проверяется rate limitограничение частоты запросов во времени, и при неудаче запрос сразу отклоняется с причиной rate_limit_exceeded. Затем перебираются пользовательские правила, и для правила, чей список типов действий содержит тип запроса, запускается его валидатор; если правило не пропускает запрос, он отклоняется с указанием имени сработавшего правила.
В _validate_policies перебираются правила политик, и если тип действия подходит, а валидатор правила не пропускает запрос — проверка немедленно завершается отказом. В _check_permission сначала вычисляется требуемый уровень прав, затем берётся уровень агента, и разрешение даётся только если уровень агента не ниже требуемого.
В validate_risk отказ происходит, если оценка риска не ниже порога deny_above — это настроечный порог из политики риска, задающий максимально допустимую оценку риска запроса, при достижении или превышении которого запрос отклоняется. Отказ также происходит, если какой-то из high-risk patternsшаблонов повышенного риска встречается в строковом представлении параметров, либо если для URL или домена не удалось извлечь хост, либо если хост совпал с заблокированным доменом.
В check_rate_limit при отсутствии квоты для агента запрос разрешается. Иначе сбрасываются счётчики по минуте (при прошествии не менее 60 секунд) и по часу (не менее 3600 секунд). Затем отказ при превышении минутного лимита, часового лимита, лимита одновременных выполнений или если тип действия не входит в непустой список разрешённых. При успехе минутный и часовой счётчики увеличиваются на единицу.
Условное разрешение при require_all=True требует выполнения всех условий (логическое И), иначе достаточно одного (логическое ИЛИ).
Сборка исполняемой политики из конфигурации
create_policies_from_config принимает либо путь к YAML-файлу, либо готовый объект конфигурации. Если объект не передан, но есть путь — конфигурация загружается из файла; если не передано ничего — создаётся конфигурация со значениями по умолчанию. В любом случае вызывается _build_policy_rules, который собирает список правил.
create_default_policies помечена как deprecated и предупреждает, что встроенные правила — это образцы, не гарантирующие полноту. Внутри _build_policy_rules определены три встроенных валидатора:
no_system_file_accessдля чтения и записи файлов берёт путь из параметров и запрещает доступ, если путь содержит один из опасных путей:/etc/,/sys/,/proc/,/dev/,C:\Windows\System32.no_credential_exposureпреобразует параметры запроса в строку в нижнем регистре и запрещает запрос, если в ней встречается одно из слов:password,secret,api_key,token,credential.no_destructive_sqlприменяется только к запросам и записям в базу данных. При пустом запросе возвращает разрешение. При неудачном разборе SQL срабатывает fail-closed и запрос запрещается. При успешном разборе запрос блокируется, если среди выражений естьDROP; также проверяютсяALTER,GRANT,TRUNCATEи другие опасные конструкции.
Заморозка политики и журнал мутаций
Заморозка включается вызовом freeze(), который выставляет флаг _frozen и заменяет изменяемые словари на неизменяемые прокси, чтобы прямое изменение атрибутов тоже вызывало TypeError.
После заморозки запрещены вызовы add_constraint(), set_agent_context(), update_agent_context() и add_conditional_permission(). Каждый из них сначала вызывает _assert_mutable, который при активной заморозке добавляет в журнал мутаций запись с пометкой blocked=True и именем операции, пишет предупреждение в лог и возбуждает RuntimeError. Успешные изменения фиксируются _log_mutation, добавляющим запись с именем операции, деталями, временем и пометкой blocked=False.
Свойство is_frozen возвращает текущее значение флага, а mutation_log возвращает копию списка записей. Это защищает от атак самоподмодификациипопытки агента ослабить собственную политику во время выполнения через методы изменения и даёт аудит: журнал хранит и успешные изменения, и заблокированные попытки.
Правила рисков: атрибут, оператор, значение
Conditionусловие правила риска, состоящее из пути к атрибуту, оператора сравнения и эталонного значения хранит путь к атрибуту в контексте, оператор сравнения и эталонное значение. При оценке условие сначала извлекает фактическое значение по пути; если значения по этому пути нет, условие считается невыполненным. Извлечение разбивает путь по точкам и последовательно спускается по словарям: если текущее значение — словарь и ключ в нём есть, берётся вложенное значение, иначе условие не выполняется.
Затем применяется оператор. Операторы сравнения проверяют равенство, неравенство и порядок фактического значения относительно эталонного. Операторы принадлежности проверяют, входит ли фактическое значение в эталонный набор как один из его элементов. Оператор вхождения проверяет, содержится ли эталонное значение как подстрока в фактическом. Неизвестный оператор означает невыполненное условие.
Свободный текст в условиях риска исключён: при проверке домена, если из URL или домена не удаётся извлечь хост, условие считается невыполненным (fail closed). Сопоставление подстрок небезопасно, и политика должна работать по хосту, а не по произвольному тексту.
Проверка сети
Сеть проверяется через извлечение нормализованного имени хоста и сопоставление его с правилами домена. _extract_host принимает URL или строку хоста, разбирает её, берёт свойство .hostname (исключая userinfo и порт), приводит к нижнему регистру, убирает завершающую точку. Если хост не удалось разобрать или он не соответствует регулярному выражению допустимого DNS-имени, хост считается неопределённым.
_host_matches считает хост совпавшим с правилом, если он равен правилу или является его поддоменом:
host == rule_host or host.endswith("." + rule_host)При этом правило тоже приводится к нормализованному виду, а подстрочное сопоставление намеренно не используется.
В validate_risk при наличии параметров URL или домена извлекается хост; если хост не извлечён — отказ. Затем для каждого заблокированного домена вызывается сопоставление, и при совпадении запрос отклоняется. Проверка разрешённых доменов выполняется только если список непуст: если ни один разрешённый домен не совпал, запрос отклоняется. Пустой список разрешённых доменов не ограничивает домены.
Квоты и rate limit
set_quota сохраняет объект квоты в словаре по ключу идентификатора агента. check_rate_limit берёт идентификатор агента из контекста запроса; если квоты нет, запрос разрешается по умолчанию. При наличии квоты счётчики сбрасываются по времени: минутный — при прошествии не менее 60 секунд с последнего сброса, часовой — не менее 3600 секунд.
Затем проверяются лимиты: превышение минутного лимита, часового лимита, лимита одновременных выполнений, а также допустимость типа действия в непустом списке разрешённых — при любом нарушении запрос отклоняется. Если все проверки пройдены, минутный и часовой счётчики увеличиваются на единицу.
get_quota_status возвращает словарь со счётчиками и лимитами, а если квоты нет — {"error": "No quota set for agent"}.
Граница ответственности: ядро и шлюз
_check_policy в пространстве ядра выполняет проверку разрешённости системного вызова через движок политик. При отсутствии движка он либо разрешает self-scoped вызовы и permissive-режим, либо запрещает всё остальное по принципу fail closed. При наличии движка он преобразует системный вызов в имя инструмента (для SYS_EXEC берётся имя инструмента из аргументов, иначе — через отображение), добавляет в аргументы имя вызова и уровень защиты и вызывает проверку нарушения.
register_message_security и register_context_router только сохраняют переданные объекты и, если есть реестр, делегируют регистрацию в него. Никаких проверок политики они не выполняют. Таким образом, проверка политики сосредоточена в ядре, а control_plane лишь регистрирует провайдеров безопасности сообщений и маршрутизатор контекста.
Shadow mode
execute_in_shadow принимает запрос и необязательную цепочку рассуждений, создаёт результат симуляции с исходом WOULD_SUCCEED и оценкой риска из запроса, затем вызывает _validate_request, чтобы определить итоговый исход и заметки. При включённой генерации результатов вызывается _simulate_execution, всегда вызывается анализ последствий, результат добавляется в журнал симуляций, а цепочка рассуждений сохраняется по идентификатору запроса.
_validate_request проверяет статус: если запрос отклонён, возвращается PERMISSION_DENIED с заметкой о недостаточных правах; если оценка риска выше 0.8 — RISK_TOO_HIGH с заметкой о превышении порога; иначе — WOULD_SUCCEED с заметкой, что все проверки пройдут.
_simulate_execution выбирает симулятор по типу действия из словаря (чтение и запись файлов, запрос и запись в базу данных, выполнение кода, вызов API), а для неизвестных типов использует обобщённую заглушку.
Возможные исходы симуляции:
- WOULD_SUCCEEDдействие прошло бы все проверки и выполнилось бы успешно;
- WOULD_FAILдействие прошло бы проверки, но завершилось бы ошибкой при самом выполнении — в отличие от нарушения политики, здесь блокировки на этапе допуска нет;
- POLICY_VIOLATIONдействие нарушило бы правила политики и было бы заблокировано ещё до выполнения;
- RISK_TOO_HIGHоценка риска действия превысила бы допустимый порог;
- PERMISSION_DENIEDу агента не хватило бы прав на это действие.
get_simulation_log возвращает копию всего журнала или отфильтрованный по агенту список. get_shadow_statistics делегирует в статистику исполнителя, которая считает общее число симуляций, распределение исходов, долю успеха, нарушения политики и отказы по риску.
Вердикт и его обоснование в аудите
set_agent_context сохраняет переданный словарь контекста агента целиком и пишет в журнал мутацию с типом set_agent_context и ролью. update_agent_context обновляет отдельные атрибуты существующего контекста (создавая пустой словарь, если агента ещё нет) и логирует мутацию с ролью и списком ключей.
check_violation при проверке условных разрешений строит контекст оценки из аргументов и контекста агента, добавляет туда верхнеуровневые атрибуты контекста и при невыполнении условий возвращает строку Conditional permission denied for {tool_name}: Conditions not met. Другие ветки возвращают строки вида Role {agent_role} cannot use tool {tool_name}, Path Violation: Cannot access protected directory {protected}, Dangerous pattern detected: {pattern.pattern}, Destructive SQL blocked: {pat} или Internal endpoint blocked: {endpoint}.
_audit добавляет в журнал аудита словарь с временем, типом события и деталями. В flight recorderжурнал аудита с цепочкой хешей, фиксирующий каждое действие агента start_trace создаёт запись со статусом pending и хешами, а log_violation обновляет её, устанавливая вердикт blocked, причину нарушения и пересчитанный хеш содержимого — так обоснование вердикта сохраняется в аудите.
DLP на аргументах и выводе
Проверка аргументов на предмет учётных данных преобразует параметры запроса в строку и запрещает запрос при наличии ключевых слов password, secret, api_key, token, credential.
За контур данные не выпускает контур агента: он реализует быстрые проверки — полномочия, белые списки инструментов, ограничения аргументов, MCP-обёртка, локальные предохранители, запрет при недоступности контроля (fail-closed). Тяжёлые ML-классификаторы на агенте намеренно не запускаются, семантический трафик уходит на шлюз. При отсутствии политики ядро запрещает системные вызовы, выходящие за пределы песочницы агента, по принципу fail closed.
Целостность журнала: Merkle-цепочка
Целостность строится на двух хешах. Первый формирует звено Merkle-цепочкиструктура, в которой каждая запись содержит хеш предыдущей, что делает незаметную подмену невозможной: он склеивает хеш предыдущей записи (или строку genesis, если предыдущей нет) с данными через двоеточие и возвращает SHA-256 от этой строки. Второй хеш считается по всем содержательным полям записи (идентификатор трассы, время, агент, инструмент, аргументы, вердикт, причина нарушения, результат, время выполнения), чтобы ловить подмену полей.
verify_integrity сначала сбрасывает буфер, затем идёт по всем записям в порядке идентификатора и для каждой проверяет, что хеш предыдущей записи совпадает с хешем предыдущей записи в цепочке (для первой записи допускается отсутствие предыдущего хеша как genesis). При нарушении связи возвращается valid=False с идентификатором первой испорченной записи и сообщением о разрыве цепочки. Дополнительно сверяется хеш содержимого с заново вычисленным; при несовпадении возвращается valid=False с сообщением о подмене полей.
Буферизация записей и WAL
Отложенные записи хранятся в очереди под защитой блокировки. _queue_write добавляет в неё словарь с SQL и параметрами, а затем вызывает _maybe_flush. _maybe_flush при выключенной пакетной записи сразу вызывает сброс; иначе сбрасывает буфер, если длина очереди достигла размера пакета или с момента последнего сброса прошло не меньше заданного интервала.
_flush_buffer под той же блокировкой забирает операции слева из очереди, выполняет их курсором и делает commit(), обновляя время последнего сброса. При ошибке выполняется rollback() и исключение пробрасывается.
WAL включается при инициализации базы данных через PRAGMA journal_mode=WAL, что даёт конкурентное чтение во время записи.
Replay и time-travel
replay_agent_history запускает воспроизведение последних N минут жизни агента: если time-travel выключен или нет отладчика, выбрасывается RuntimeError, иначе вызывается воспроизведение временного окна, а при переданном callback дополнительно вызывается воспроизведение истории отладчика.
capture_agent_state_snapshot сохраняет снимок состояния агента в конкретный момент: при выключенном time-travel просто возвращает управление, иначе формирует словарь из идентификатора сессии, времени создания, прав и метаданных и передаёт его в захват снимка.
get_replay_summary возвращает сводку сессии воспроизведения, а при выключенном time-travel выбрасывает RuntimeError. get_time_travel_statistics возвращает статистику time-travel, а при выключенном режиме — {"enabled": False}.
create_read_only_agent создаёт агента только с правами чтения файлов и запросов к базе данных уровня READ_ONLY и квотой 30 запросов в минуту, 500 в час и списком разрешённых действий из чтения файлов и запросов к базе данных. Поэтому воспроизведение ограничено действиями чтения.
Что из этого следует на практике
- Контроль встроен в момент действия, а не в модель: любой вызов инструмента проходит через точку перехвата до side effect. Это значит, что безопасность не зависит от того, «насколько хороша» модель — она зависит от того, что вызов физически не может пройти мимо проверки.
- Отказ по умолчанию — базовое свойство. При недоступности движка политик блокируются все системные вызовы, кроме self-scoped, чтобы агент мог использовать свою память и корректно завершиться. То же касается разбора SQL и извлечения хоста: неудача разбора ведёт к отказу, а не к разрешению.
- Порядок слоёв задаёт приоритет отказов: сначала rate limit, затем пользовательские правила, затем права, риск, сеть и квоты. Первый отказ прерывает проверку, поэтому причина отказа всегда конкретна.
- Заморозка политики закрывает атаку самоподмодификации: после неё изменение правил невозможно, а попытки фиксируются в журнале мутаций с пометкой о блокировке. Журнал хранит и успешные изменения — это даёт воспроизводимость вердикта.
- Свободный текст в условиях риска исключён: решения принимаются по хосту и атрибутам, а не по подстрокам. Это устраняет класс обходов через манипуляцию строками.
- Разделение семантики: на агенте — быстрые проверки и fail-closed, на шлюзе — семантический трафик и тяжёлые классификаторы. Тяжёлые ML-классификаторы на агенте намеренно не запускаются, чтобы не тормозить сам сценарий.
- Целостность журнала обеспечивается двумя хешами: цепочкой звеньев и хешем содержимого. Проверка целостности обнаруживает как разрыв связи между записями, так и подмену отдельных полей.
- Shadow mode позволяет прогонять запросы без side effect: исходы симуляции показывают, что произошло бы, и накапливается статистика по нарушениям политики и отказам по риску.
Где смотреть в коде
- policy_engine.py: check_rate_limit
- shadow_mode.py: __init__
- kernel_space.py: _check_policy
- agent_kernel.py: execute
- agent_kernel.py: intercept_tool_execution
- agent_kernel.py: submit_request
- control_plane.py: get_replay_summary
- flight_recorder.py: _recompute_content_hash