Трендовые github проекты в нашем телеграм канале. Подпишись → Read-only агент и write-credential в соседнем узле
Фраза «агент работает в режиме read-only» попадает в архитектурные описания, security-опросники и решения о запуске в production. Обычно за ней стоит одно проверенное условие: у модели нет учётных данных на запись во внешнюю систему. Лабораторный стенд на n8n, DeepSeek и HubSpot показывает, что этого условия недостаточно, и CRM при нём всё равно меняется.
Где на самом деле лежит право на запись
В стенде модель получала запрос и возвращала структурированное предложение: какую сделку изменить и что записать. Credential HubSpot у неё не было, напрямую вызвать API она не могла. Эта часть ограничения работала.
Но следующий узел n8n принимал предложение и выполнял PATCH со своим собственным HubSpot credential. Никакой проверки параметров между предложением и вызовом не стояло. Право на запись осталось в системе, просто сменило владельца: с модели на оркестратор.
Выходит, что полномочия модели и полномочия системы — разные вещи. Первое можно ограничить удалением ключа. Второе определяется тем, какой компонент цепочки способен сделать внешний вызов и что его останавливает.
Контрольный сценарий: wrong-object
Тест шёл в изолированном окружении с синтетическими данными. В отдельном HubSpot Lab создали две сделки: LAB-042 и LAB-043.
Нормальный прогон: запрос, предложение и запись относятся к LAB-042. Всё совпадает, PATCH проходит.
Затем намеренно создали расхождение:
- человекочитаемый запрос просит изменить
LAB-042; - структурированная цель в предложении указывает на
LAB-043.
Злого умысла модели здесь нет. Расхождение может появиться из-за ошибки генерации, неверного маппинга полей, подмены параметра между шагами или неудачного prompt injection в тексте, который агент прочитал. Для инженера важен результат: цепочка получает предложение с другим target и должна решить, что с ним делать.
В первой конфигурации решать было нечему. n8n взял target из предложения и выполнил PATCH для LAB-043. Отдельное чтение после прогона подтвердило: LAB-043 изменилась, LAB-042 осталась в исходном состоянии. Изменилась не та сделка, которую назвали в запросе, хотя write credential у модели по-прежнему не было.
Детерминированный gateway перед PATCH
Во второй конфигурации между предложением модели и вызовом HubSpot поставили отдельный детерминированный gateway. Он проверяет параметры, которые определяют допустимое последствие:
- целевую систему;
- идентификатор объекта;
- ожидаемое исходное состояние;
- разрешённый переход.
Ожидаемый target хранится отдельно от результата генерации. Это ключевое место: если gateway сверяет предложение модели с самим предложением модели, проверка ничего не даёт. Эталон должен приходить из другого источника, например из исходного запроса пользователя, зафиксированного до вызова модели.
Credential при этом никуда не делся. n8n по-прежнему мог писать в HubSpot. Изменилось условие доступа к этому праву: теперь оно зависит от проверки непосредственно перед внешним действием.
Результаты того же набора сценариев:
| Сценарий | Без gateway | С gateway |
|---|---|---|
Запрос и target совпадают (LAB-042) | allow, запись выполнена | allow, запись выполнена |
Запрос про LAB-042, target LAB-043 | PATCH выполнен, LAB-043 изменена | deny, PATCH не запускался, состояние не изменилось |
После deny повторное чтение подтвердило, что сделка осталась прежней. Состояние читают прямо из целевой системы, лога gateway для этого мало.
Что делать с этим в своих пайплайнах
Класс проблемы известен в security как confused deputy: компонент с широкими правами выполняет действие по просьбе того, у кого таких прав нет. Агентные цепочки воспроизводят его в новом виде. Модель выступает источником запроса, а оркестратор с credential становится тем самым «заместителем».
Шесть вопросов, которые стоит задать перед запуском агента с доступом к CRM, ERP, тикетной системе или инфраструктуре:
- Где физически хранятся credential и полномочия на запись?
- Какой компонент выполняет внешний вызов?
- Какие параметры проверяются непосредственно перед исполнением?
- Может ли target или другой существенный параметр измениться между запросом пользователя и write?
- Есть ли независимая граница между предложением модели и последствием?
- Как подтверждается итоговое состояние после действия?
Для ответов на них нужна восстанавливаемая цепочка событий: что предложено, кто реально может исполнить, что проверялось перед исполнением, что фактически вызвано и какое состояние получилось после вызова. Если в логах n8n, gateway и целевой системы можно связать эти пять шагов одним идентификатором запроса, аудит инцидента занимает минуты. Если нельзя, приходится восстанавливать картину по косвенным признакам.
Практические правила для n8n и похожих оркестраторов
Из стенда следуют несколько конкретных правил, применимых и вне HubSpot:
- Не передавайте идентификатор объекта из вывода модели прямо в узел с write credential. Сначала сверяйте его с идентификатором из исходного запроса.
- Делайте gateway детерминированным: обычные условия и сравнения, без второго вызова LLM. Модель для проверки модели даёт вероятностный ответ там, где нужен строгий.
- Проверяйте ожидаемое исходное состояние объекта перед записью. Так вы поймаете и расхождение по target, и запись поверх уже изменённых данных.
- Ограничивайте допустимые переходы списком. Если агенту нужно менять стадию сделки с одной на следующую, остальные переходы должны блокироваться на границе.
- Ограничивайте сам credential в целевой системе: минимальные scopes, отдельный токен для агентного workflow. Gateway остаётся вторым рубежом поверх этих ограничений.
- Читайте состояние после записи и сохраняйте результат вместе с идентификатором запроса.
Границы результата
Тест лабораторный: два синтетических объекта, один класс расхождения (неверный target). Он не доказывает, что gateway закрывает все варианты атак на агентную цепочку. Например, подмена значения поля при верном target требует отдельных проверок допустимых значений и переходов, и их нужно закладывать в gateway так же явно.
Зато вывод переносится на любую систему с похожей структурой. Если компонент после модели держит write credential и принимает параметры без проверки, утверждение «агент read-only» описывает только модель. Настоящую границу полномочий нужно искать там, где credential используется, и ставить контроль перед этим вызовом.