Читать 7 мин

Как защитить GraphQL-мутации для AI-агентов от разрушительных вызовов

GraphQL-мутациям для AI-агентов нужны типизированные входные данные, узкие области доступа, настоящие предпросмотры и условные записи, чтобы предотвращать разрушительные сгенерированные запросы.

Как защитить GraphQL-мутации для AI-агентов от разрушительных вызовов

Сгенерированные GraphQL-запросы ломаются предсказуемым образом: у модели достаточно информации, чтобы создать синтаксически правильный запрос, но недостаточно ограничений, чтобы опасное действие было сложно выполнить. Мутация с названием updateProject и необязательным флагом archived кажется человеку гибкой. Для агента, который собирает запрос из неполного контекста, это приглашение изменить состояние проекта как побочный эффект обычного редактирования.

Ограничения должны находиться в схеме и резолвере, а не в prompt, который просит агента быть осторожным. Типизированные входные данные, узкие полномочия действий, содержательный предпросмотр, условная запись и ошибки с понятным следующим шагом помогают генерировать правильные запросы, а не разрушительные. Такой дизайн полезен и обычным разработчикам клиентов. Агенты лишь показывают обходные пути, которые слишком свободные API годами оставляли без внимания.

Корректный запрос всё равно может выражать неправильное действие

GraphQL проверяет, соответствует ли запрос схеме. Но он не устанавливает, выбрал ли вызывающий правильного клиента, понял ли состояние записи или действительно хотел что-то удалить. Команды часто принимают типобезопасность за безопасность действия, а затем помещают поле с названием delete, archive или status в широкую мутацию обновления.

Рассмотрим распространённую схему:

input ProjectPatchInput {
  name: String
  description: String
  archived: Boolean
  ownerId: ID
}

type Mutation {
  updateProject(id: ID!, input: ProjectPatchInput!): Project!
}

Здесь безобидные изменения смешаны с передачей владения и переходом жизненного цикла. Агенту, которому поручили «навести порядок в старых проектах», вполне логично решить, что нужно установить archived: true. Агент, которому поручили исправить название проекта, может случайно сохранить поле archived из ранее сгенерированного объекта. Система типов принимает оба запроса, потому что оба составлены правильно.

Не прячьте разрушительное поведение в гибком patch-запросе. Дайте каждому действию название, описывающее его последствия, и input, содержащий только необходимые для этого действия сведения:

type Mutation {
  renameProject(input: RenameProjectInput!): RenameProjectPayload!
  archiveProject(input: ArchiveProjectInput!): ArchiveProjectPayload!
  transferProjectOwnership(input: TransferProjectOwnershipInput!): TransferProjectOwnershipPayload!
}

input RenameProjectInput {
  projectId: ID!
  expectedVersion: Int!
  name: String!
}

input ArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
  confirmation: String!
  idempotencyKey: String!
}

Это не формальность ради формальности. Узкая мутация ограничивает то, что может выразить сгенерированный запрос. Она чётко разделяет «изменить подпись» и «убрать это из обычного использования». Описание инструмента может объяснить разницу, но обеспечивать её должна схема.

Спецификация GraphQL здесь помогает, хотя и ограниченно. Проверка input object отклоняет поля, которых схема не определяет. Если в ArchiveProjectInput нет ownerId, клиент не сможет незаметно передать изменение владельца в вызов архивации. Считайте это защитным ограничителем, а не границей безопасности. Именно резолвер решает, может ли конкретный актор архивировать конкретный проект.

Избегайте универсального поля action: String!, например mutateProject(action: "ARCHIVE"). Оно кажется компактным, пока каждому действию не понадобятся собственные поля, проверки, авторизация, данные для предпросмотра и обработка ошибок. В итоге внутри input object появляется закрытый протокол частных RPC, а возможностей инструментов GraphQL становится меньше.

Input должен называть цель и границу действия

Деструктивный input должен точно указывать, что изменится, какую версию проверял вызывающий и какое ограничение не даст выборке разрастись. Одних ID недостаточно, если резолвер может затронуть дочерние записи, внешние системы или запрос на уровне всего арендатора.

Начните с объекта, который определяет один ресурс в арендаторе вызывающей стороны. Не принимайте произвольный фильтр в мутации удаления, если продукту действительно не нужна массовая обработка. Фильтр вроде where: { status: INACTIVE } создаёт неоднозначность: по какой временной отметке определяется неактивность, о каком арендаторе идёт речь и какое скрытое значение используется по умолчанию? Модель может передать такой фильтр лишь потому, что поле существует, а не потому, что она просмотрела итоговый набор.

Для операций над одной записью передавайте во входных данных токен версии. Число легко проверить, но подойдёт и непрозрачная строка ревизии. Резолвер сравнивает её с сохранённой версией в той же транзакции, которая записывает изменение. Если значения различаются, он возвращает конфликт и ничего не меняет.

input ArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
  confirmation: String!
  idempotencyKey: String!
}

enum ArchiveReason {
  CUSTOMER_REQUEST
  DUPLICATE
  END_OF_LIFE
}

Перечисление reason нужно не только для удобной отчётности. Оно не даёт запросу придумать произвольное объяснение, которое последующая автоматизация начнёт считать значимым. Свободный текст можно оставить для заметки, если он нужен людям, но рабочие категории должны быть перечисляемыми.

Поле подтверждения должно быть привязано к реальной цели. Требование буквальной строки ARCHIVE ловит только небрежно составленные запросы. Требование archive acme-project-42 заставляет клиент сначала определить ресурс, а затем повторить его идентификатор. Это не останавливает злоумышленника и ни в коем случае не заменяет авторизацию. Зато оно отлавливает множество сгенерированных запросов, где правильное действие оказалось привязано не к тому ID.

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

Для массовой операции явно задайте верхний предел и возвращайте токен предпросмотра, связанный с точной выборкой. Такой input сообщает намного больше, чем необработанный фильтр:

input DeleteDormantProjectsInput {
  previewToken: ID!
  expectedCount: Int!
  confirmation: String!
  idempotencyKey: String!
}

Резолвер выполнения должен отклонить токен, если он истёк, принадлежит другому актору, описывает другого арендатора или возвращает количество, отличное от expectedCount. Иначе агент может просмотреть пять записей, а затем выполнить изменившийся запрос, который уже соответствует пятидесяти.

Полномочия должны соответствовать мутации, а не существительному

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

Выдавайте полномочия, описывающие действие. Например, сервисный токен автоматизации релизов может иметь project:rename и project:archive, а рабочий процесс поддержки не должен иметь ни одного из них. Отдельная область project:delete должна встречаться редко. Если ваша система идентификации не умеет выдавать настолько узкие области, добавьте серверную проверку возможностей, связанную с названием мутации, и записывайте её в решение авторизации.

Одна область доступа никогда не решает вопрос доступа. Каждый резолвер должен выполнять несколько проверок в заранее определённом порядке:

  1. Аутентифицировать вызывающую сторону и определить её арендатора и принципала.
  2. Проверить, есть ли у принципала полномочия на эту мутацию.
  3. Загрузить цель внутри границы арендатора, а не загружать глобально с последующей проверкой.
  4. Проверить состояние записи и требуемую бизнес-правилом связь с ролью.
  5. Выполнить условную запись и добавить событие аудита в той же транзакции.

Загрузка внутри границы арендатора важна. Если резолвер вызывает findProjectById(id) до проверки принадлежности арендатору, он может раскрыть существование объекта через время ответа или формулировку ошибки. Кроме того, глобально загруженный объект может попасть в помощник, который предполагает, что авторизация уже выполнена. Сделайте принадлежность арендатору частью предиката поиска.

Не выводите права из заявленной агентом задачи. Заголовок запроса X-Agent-Goal: cleanup может быть полезен для аудита, но не даёт полномочий. Prompt, метки задачи и идентификатор модели могут помочь человеку при проверке действия, однако любой клиент способен их подделать.

То же различие относится и к доступу к инструментам. Агент может иметь право обращаться к GraphQL endpoint, но не иметь права на конкретную мутацию. Если среда запуска агента это позволяет, описывайте инструменты чтения и инструменты действий отдельно. Окончательное решение должно оставаться в API, потому что клиент может обойти метаданные инструмента и отправить HTTP-запрос напрямую.

Пробный запуск должен строить тот же план, что и выполнение

Пробный запуск полезен только тогда, когда отвечает на вопрос: «Что именно сделает этот запрос прямо сейчас?» Ненастоящий предпросмотр, который считает строки упрощённым запросом, создаёт у агентов ложное чувство безопасности. Итоговая мутация может применять другие правила выбора, использовать другую ветку авторизации или запускать внешнее действие, которое предпросмотр вовсе не учитывал.

Создайте общую функцию планирования. Она получает аутентифицированного актора и input, проверяет все условия, определяет цели, рассчитывает побочные эффекты и создаёт неизменяемый план. Предпросмотр возвращает безопасное представление этого плана. Путь выполнения использует план только после того, как вызывающая сторона предъявит короткоживущий токен и подтверждение.

type Mutation {
  previewArchiveProject(input: PreviewArchiveProjectInput!): ArchivePreviewPayload!
  archiveProject(input: ArchiveProjectInput!): ArchiveProjectPayload!
}

input PreviewArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
}

type ArchivePreviewPayload {
  previewToken: ID!
  project: Project!
  affectedMemberCount: Int!
  plannedEffects: [ArchiveEffect!]!
  expiresAt: DateTime!
}

enum ArchiveEffect {
  PROJECT_HIDDEN_FROM_DEFAULT_LISTS
  PENDING_INVITATIONS_CANCELLED
}

План должен содержать ID целей, их версии, личность актора, арендатора, хеш input, планируемые эффекты и срок действия. Храните его на сервере или подписывайте непрозрачный токен, ссылающийся на сохранённое состояние. Не помещайте весь план в управляемый клиентом JSON и не доверяйте ему при выполнении.

Input выполнения должен ссылаться на токен предпросмотра, а не повторять свободный селектор:

input ArchiveProjectInput {
  previewToken: ID!
  confirmation: String!
  idempotencyKey: String!
}

Такой процесс из двух вызовов добавляет трение. В этом и смысл для важных действий. Не навязывайте его каждой мутации. Простое правило: требуйте предпросмотр, если операция затрагивает несколько записей, имеет необратимый внешний эффект или делает ресурс недоступным для других пользователей.

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

Идемпотентность и версии предотвращают разные сбои

Одобрите запуск агента
Одобрите новый процесс агента один раз, увидев его полномочия подписи кода до начала работы.

Идемпотентность не даёт одной и той же операции примениться дважды. Проверка версии не позволяет применить действие к состоянию, изменившемуся после того, как вызывающий его прочитал. Команды часто добавляют один механизм и считают, что решили обе проблемы.

Агент может повторить запрос, если HTTP-соединение закрылось после фиксации мутации на сервере. Без идемпотентности второй запрос может создать второй возврат средств, дублировать сообщение или дважды вызвать одну и ту же внешнюю API. Передавайте каждой мутации с побочным эффектом idempotencyKey. Сервер должен хранить его вместе с аутентифицированным актором, названием мутации, хешем нормализованного input и завершённым payload либо стабильной ошибкой.

Когда сервер снова видит того же актора, мутацию, ключ и хеш input, он возвращает исходный результат. Если тот же ключ приходит с другим хешем, сервер возвращает IDEMPOTENCY_KEY_REUSED и ничего не выполняет. Приём изменившихся данных под уже использованным ключом разрушает свойство, на которое клиенты рассчитывают при повторах.

Проверка версии работает в другой ситуации. Агент читает проект версии 7, готовит предпросмотр архивации, а человек переименовывает проект или восстанавливает приглашение. Во время выполнения архивации резолвер сравнивает версию 7 с текущей сохранённой версией. Если она уже равна 8, резолвер возвращает конфликт. Агент должен получить актуальное состояние, заново оценить намерение и при необходимости создать новый предпросмотр.

Спецификация GraphQL выполняет поля верхнего уровня одной операции мутации последовательно. Это не делает последовательными отдельные HTTP-запросы. Два агента всё ещё могут почти одновременно отправить две операции мутации. Используйте условное обновление в базе данных, блокировку строки или транзакционное ограничение. Проверка в памяти приложения с последующей отдельной записью оставляет окно для гонки.

Условное обновление в стиле SQL ясно показывает нужное инвариантное правило:

UPDATE projects
SET archived_at = CURRENT_TIMESTAMP,
    version = version + 1
WHERE id = :project_id
  AND tenant_id = :tenant_id
  AND version = :expected_version
  AND archived_at IS NULL;

Если обновлено ноль строк, проверьте текущую запись внутри границы арендатора и верните конкретный результат: объект не найден, доступ запрещён, объект уже архивирован или версия конфликтует. Не сообщайте об любом результате с нулём строк как об общей серверной ошибке. Агенту нужно понимать, будет ли повтор вредным, полезным или бессмысленным.

Payload ошибки должен подсказывать агенту следующий шаг

Массив errors верхнего уровня GraphQL подходит для ошибок разбора, проверки и выполнения резолвера. Но заставлять клиентов извлекать бизнес-результаты из английских сообщений неудобно. Ожидаемые результаты мутации лучше помещать в типизированный payload со стабильным кодом и структурированными деталями.

type ArchiveProjectPayload {
  outcome: ArchiveProjectOutcome!
  project: Project
  error: MutationError
}

enum ArchiveProjectOutcome {
  ARCHIVED
  VERSION_CONFLICT
  CONFIRMATION_REQUIRED
  PREVIEW_EXPIRED
  FORBIDDEN
  IDEMPOTENCY_KEY_REUSED
}

type MutationError {
  code: String!
  message: String!
  currentVersion: Int
  requiredConfirmation: String
}

Используйте транспортные ошибки и ошибки выполнения GraphQL, если клиент не смог корректно выполнить операцию. Типизированный результат подходит для запроса, который выполнился штатно, но не изменил состояние, потому что бизнес-правило его отклонило. Выберите один подход и задокументируйте его. Если одни конфликты передаются через errors.extensions.code, а другие через перечисления payload, поведение агента становится хрупким.

Агент должен уметь связать результат с безопасным действием. VERSION_CONFLICT означает, что объект нужно перечитать и заново оценить намерение. PREVIEW_EXPIRED означает, что нужно создать новый предпросмотр. CONFIRMATION_REQUIRED означает, что нужно показать человеку требуемую фразу или запросить её, а не угадывать. FORBIDDEN означает остановку. IDEMPOTENCY_KEY_REUSED означает, что новый ключ можно создать только после явного решения выполнить другую операцию.

Не возвращайте внутренние названия политик, фрагменты SQL или детали графа авторизации. Стабильные внешние коды могут быть точными, не раскрывая внутреннее устройство. Добавляйте ID корреляции в extensions ответа и соответствующую запись аудита на сервере. Тогда у оператора будет конкретный материал для расследования, если агент сообщит об ошибке.

Успешный payload должен содержать достаточно сведений, чтобы прекратить неопределённость. Возвращайте получившееся состояние записи, новую версию, ID операции и фактически выполненные эффекты. Один только Boolean вынуждает клиент делать ещё один запрос и оставляет место для устаревшего чтения. Проверять результат человеком тоже становится сложнее.

Удалению нужен жизненный цикл, а не Boolean-флаг

Храните секреты GraphQL за пределами агентов
Sallyport хранит API-учётные данные в зашифрованном хранилище и выполняет HTTP-вызов за пределами процесса агента.

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

Не называйте операцию архивации deleteProject, если она лишь скрывает запись. Названия объясняют клиентам, какого состояния ожидать. archiveProject должна возвращать ARCHIVED, а purgeProject должна означать, что данные больше не будут доступны. Если API использует delete для всех этапов жизненного цикла, агент не сможет надёжно отличить обратимую очистку от окончательного удаления.

Для окончательной очистки нужны более строгие input и авторизация, чем для архивации. Возможно, ресурс должен оставаться архивированным в течение срока хранения, на нём не должно быть юридической или платёжной блокировки, а оператор с отдельной областью доступа должен одобрить операцию. Резолвер обязан проверять каждое условие. Обратный отсчёт на стороне клиента или инструкция инструмента не имеют полномочий.

К внешним побочным эффектам нужно относиться так же. Если архивация отменяет приглашения, удаляет удалённое окружение или запускает webhook, возвращайте эти эффекты в предпросмотре и итоговом payload. Не добавляйте их незаметно в универсальный резолвер обновления. Человеку, который проверяет запрос агента, нужно увидеть последствия до одобрения, а агенту нужны факты, о которых он сможет сообщить после выполнения.

Для финансовых операций или действий с учётными данными не предлагайте фиктивный пробный запуск, который вызывает рабочую конечную точку провайдера и надеется, что эффекта не будет. Если у провайдера есть официальный предпросмотр или механизм авторизации, используйте его. В противном случае помечайте результат как локальную оценку и перечисляйте то, что сервер не смог проверить. Притворная уверенность хуже необходимости принять решение человеку.

Проверки резолвера делают обещания схемы реальными

Дизайн схемы ограничивает плохо сформированное намерение. Дизайн резолвера не даёт запросу, который выглядит авторизованным, пересечь реальную границу. Разделяйте эти уровни в коде, чтобы будущий рефакторинг не заменил проверку прав комментарием в описании инструмента.

Резолвер деструктивного действия должен выполнять последовательность, в которой отказ происходит дёшево, а запись как можно позже. Сначала аутентифицируйте запрос, затем определите арендатора актора, проверьте input, загрузите цель внутри этого арендатора, проверьте область доступа и состояние, проверьте токен предпросмотра и подтверждение, захватите запись идемпотентности и выполните условную транзакцию. Порядок может отличаться в зависимости от модели хранения, но не выполняйте внешнее действие до того, как убедитесь, что транзакция может зафиксироваться.

С записью идемпотентности нужно осторожно работать, если операция затрагивает базу данных и внешнего провайдера. Если пометить ключ завершённым до внешнего вызова, можно заявить об успехе при фактической ошибке. Если сначала вызвать провайдера, процесс может завершиться до сохранения результата, что приведёт к повтору. Используйте паттерн outbox или поддержку идемпотентности на стороне провайдера, если она доступна. Сохраните устойчивую запись ожидающей операции, зафиксируйте локальное решение и отправьте внешний эффект с ID операции, который переживает повторы.

Записи аудита должны содержать аутентифицированного принципала, запуск агента, если он применим, название мутации, нормализованную цель, хеш input, результат авторизации, ссылку на предпросмотр, ключ идемпотентности, результат и получившуюся версию. Заметки и поля с чувствительным содержимым редактируйте согласно правилам хранения. Запись аудита, в которой сказано лишь «мутация выполнена», почти бесполезна во время инцидента.

Если агенты работают через HTTP- или SSH-вызовы с учётными данными, по возможности храните секреты за пределами процесса модели. Sallyport направляет поддерживаемые действия через локальное хранилище и записывает отдельные вызовы. Это полезно, когда GraphQL-мутации требуется одобрение, видимое человеку, помимо того, что может обеспечить один bearer-токен.

Тестируйте сгенерированный запрос, а не только резолвер

Храните SSH-ключи в хранилище
Направляйте SSH-команды через stateless-помощник Sallyport sp-ssh вместо передачи ключей агенту.

Модульные тесты, которые вызывают резолвер с аккуратно собранными объектами, не ловят нужный вам тип сбоя. Сгенерированные клиенты отправляют пропущенные поля, null, устаревшие ID, алиасы, повторные запросы и переменные, собранные из результатов предыдущих инструментов. Тестируйте публичную границу GraphQL с такими же формами данных.

Составьте матрицу тестов мутации вокруг поведения, а не веток кода. Как минимум проверьте вызывающую сторону из другого арендатора, вызывающую сторону с правом чтения, но без области действия, истёкший предпросмотр, изменившуюся версию цели, неправильную строку подтверждения, повторно использованный ключ идемпотентности и два одновременных вызова с одной ожидаемой версией. После каждого теста проверяйте и ответ, и устойчивое состояние данных.

Этот запрос должен завершиться ошибкой во время проверки GraphQL, потому что input не определяет ownerId:

mutation BadArchive($input: ArchiveProjectInput!) {
  archiveProject(input: $input) {
    outcome
  }
}
{
  "input": {
    "projectId": "prj_42",
    "expectedVersion": 7,
    "reason": "DUPLICATE",
    "confirmation": "archive prj_42",
    "idempotencyKey": "run-18-archive-42",
    "ownerId": "usr_9"
  }
}

Ожидаемый ответ здесь должен использовать формат ошибки верхнего уровня GraphQL, потому что документ передал недопустимый input object. Этот тест показывает, что схема не допускает в мутацию несвязанную возможность. Отдельный тест должен подтвердить, что правильно сформированный запрос архивации всё равно завершается ошибкой, если актор относится к другому арендатору.

Проводите тесты конкурентного доступа на реальном поведении транзакций, а не на имитации в памяти. Отправьте два запроса архивации с одним ID и ожидаемой версией, затем убедитесь, что один возвращает ARCHIVED, а второй возвращает конфликт или результат идемпотентного повтора. Если оба вызова сообщают об успехе с разными ID операций, условная запись не выполняет свою задачу.

Проверяйте и аудит. Если ваш шлюз действий создаёт защищённый от изменений зашифрованный аудит-журнал, включите его проверку в учения по реагированию на инциденты, а не оставляйте её командой, которой никто не пользовался. Sallyport предоставляет sp audit verify для офлайн-проверки хеш-цепочки без ключа хранилища. Запускайте эту команду на копии журнала и убедитесь, что операторы понимают значение неудачной проверки.

Сгенерированным инструментам нужно меньше вариантов, а не более длинные предупреждения

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

Предоставляйте операции чтения, которые возвращают нужные агенту идентификаторы, версии, статусы и названия до предложения мутации. Если агенту приходится придумывать ID по метке, дизайн мутации вас не спасёт. Ясно возвращайте стабильные ID и явно показывайте неоднозначные результаты поиска вместо молчаливого выбора одного из них.

В описании инструмента указывайте предварительное условие и последствие, но оставляйте сервер точкой принудительного контроля. Например: «Архивирует один проект после успешного предпросмотра. Отменяет перечисленные в предпросмотре ожидающие приглашения». Это лучше, чем «Используйте с осторожностью», поскольку такая фраза ничего не говорит о практических действиях.

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

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

Вопросы и ответы

Делает ли проверка GraphQL деструктивные мутации безопасными?

Нет. GraphQL проверяет форму запроса, аргументы полей и приведение скалярных типов, но не знает, разумно ли действие в текущем состоянии системы. Резолвер всё равно должен проверять права доступа, переходы состояний, границы арендатора и правила конкурентного доступа.

Когда нужно создавать отдельный input для GraphQL-мутации?

Создавайте отдельный input для мутации, когда действие меняет состояние особым образом, особенно при удалении, приостановке, возврате средств, передаче или публикации. Универсальный input для частичного обновления подходит для обычных редактируемых полей, но не должен содержать действия жизненного цикла, замаскированные под логические флаги.

Что на самом деле должен делать пробный запуск GraphQL?

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

Как работают ключи идемпотентности в GraphQL-мутациях?

Ключ идемпотентности помогает серверу распознать повтор той же операции и вернуть исходный результат вместо повторного выполнения. Храните ключ вместе с актором, названием мутации, хешем нормализованного input, результатом и политикой истечения срока. Повторное использование ключа с другими данными нужно отклонять.

Должны ли GraphQL-области доступа соответствовать ресурсам или мутациям?

Общая область вроде write:projects обычно даёт агенту слишком много полномочий, если ему нужно лишь архивировать проект. Ограничивайте деструктивные возможности конкретной мутацией или действием, а проверку владельца и арендатора выполняйте в резолвере отдельно.

Как агенту безопасно удалить много записей через GraphQL?

Обрабатывайте массовое удаление как отдельную операцию с количеством целей, ограниченным правилом выборки, явной фразой подтверждения и предпросмотром. Не превращайте удаление одного объекта в случайный цикл только потому, что агент может составить длинный список ID.

Какие ошибки должны возвращать деструктивные GraphQL-мутации агенту?

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

Могут ли два GraphQL-запроса на мутацию столкнуться из-за гонки?

GraphQL сериализует поля верхнего уровня внутри одной операции мутации, но отдельные запросы всё равно могут выполняться одновременно. Передавайте ожидаемую версию во входных данных и делайте запись условной, возвращая конфликт, если сохранённая версия изменилась.

Может ли prompt агента обеспечить безопасность GraphQL-мутации?

Нет. Схема является публичным интерфейсом, и клиенты могут обращаться к API напрямую, обходя prompt агента или описание инструмента. Сервер обязан проверить личность вызывающей стороны, область доступа, связь с арендатором, состояние и подтверждение до изменения данных.

Как не допустить попадания учётных данных агента в сгенерированные API-запросы?

Храните долгоживущие API- и SSH-учётные данные за пределами процесса агента, при необходимости одобряйте запуск агента и записывайте каждое исходящее действие. Sallyport подходит для macOS и направляет поддерживаемые HTTP- и SSH-действия через локальное зашифрованное хранилище, не передавая секреты агенту.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov