# Как спроектировать безопасное действие OpenAPI для агентов

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

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

## Операция ещё не стала действием агента

HTTP-endpoint, операция OpenAPI и действие агента отвечают на разные вопросы. Их часто смешивают, потому что операция OpenAPI даёт удобную отправную точку, но различия определяют, останется ли автоматизация понятной.

Endpoint это адрес вроде `/v1/deployments`. Операция добавляет HTTP-метод, поэтому `POST /v1/deployments` отличается от `GET /v1/deployments`. Действие агента добавляет человеческий и операционный контракт: какую цель оно преследует, какие аргументы принимает, какие эффекты создаёт, какие данные считаются признаком успеха и кто должен дать согласие.

Спецификация OpenAPI определяет Operation Object с такими полями, как `operationId`, `parameters`, `requestBody`, `responses` и `security`. Используйте их как свидетельства, а не как автоматический список публикации. Операция с полностью описанной схемой всё равно может быть плохим действием агента, если её описание скрывает изменение в production за безобидным названием.

Сравним две операции:

```text
GET  /v1/projects/{project_id}/builds/{build_id}
POST /v1/projects/{project_id}/builds/{build_id}/promote
```

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

Я видел, как команды открывали универсальный инструмент `request`, потому что у их API уже был аккуратный файл OpenAPI. После этого агент мог собирать произвольные пути, строки запроса и тела. Это не каталог действий, а удалённое выполнение произвольных операций над бизнес-API, только с более аккуратной пунктуацией.

Открывайте операцию лишь после того, как сможете написать одно предложение по шаблону: «Это действие [делает конкретную вещь] с [ограниченным объектом] и возвращает [свидетельство нового состояния]». Если без расплывчатых глаголов вроде «управлять», «обрабатывать» или «работать с» не обойтись, действие пока слишком широкое.

## Начинайте с последствий, а не со схемы запроса

Подтверждение должно зависеть от последствий вызова, а не от HTTP-метода или кажущейся простоты JSON-тела. Небольшой `POST` может создать необратимое обязательство. Объёмный `GET` может раскрыть приватные данные. `DELETE` иногда удаляет лишь ненужный черновик, а `PATCH` способен отозвать доступ у всех пользователей.

До просмотра полей опишите эффект словами, понятными ответственному за систему человеку. Что изменится, если сервер выполнит вызов дважды, применит его не к тому объекту или выполнит через пять минут? Эти вопросы отделяют обычное получение данных от действия, которое требует проверки.

При проверке операции я использую четыре класса последствий:

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

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

Не делайте вывод о безопасности по названию метода. В HTTP `GET` считается безопасным в протокольном смысле: клиент не должен запрашивать через него изменение состояния. Это соглашение, а не доказательство поведения конкретного сервера. Мне встречались диагностические endpoints, которые обновляли кэш, запускали генерацию отчётов и расходовали ограниченные ресурсы при повторных вызовах. Проверяйте реальное поведение, а не то, на что намекает глагол.

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

Хорошая карточка действия фиксирует оба измерения простыми словами:

```text
Action: promote_preview_build
Effect: Changes one named preview build into the staging release channel.
Scope: One project and one build ID.
Result: Release ID, resulting channel, and server timestamp.
Human consent: Required for every call.
Retry: Never retry automatically unless the server accepts the same idempotency token.
```

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

## Входным данным нужны границы, которые агент не сможет обойти словами

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

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

Создавайте действие с входными данными под конкретную задачу. Если задача звучит как «разверни сборку, прошедшую тесты, в preview-окружении», агенту могут понадобиться только `project_id`, `build_id` и короткая `reason`. Исполнитель сам выберет разрешённое окружение и отклонит всё, что выходит за границы действия.

Так форма запроса делает границу явной:

```json
{
  "project_id": "proj_4821",
  "build_id": "build_9017",
  "reason": "Preview requested after integration tests passed"
}
```

Не добавляйте `target_url`, произвольные `headers`, сырое тело запроса или общий объект `options` только потому, что их поддерживает исходный endpoint. Каждая такая лазейка снова превращает аккуратно названное действие в универсальный клиент.

Используйте поля OpenAPI, которые уже задают полезные ограничения. Устанавливайте `additionalProperties: false`, если объект должен принимать только названные поля. Применяйте `enum` для действительно небольшого набора допустимых значений. Задавайте длину и шаблон, если у идентификаторов есть установленный формат. Отмечайте поля как обязательные, когда исполнитель не может безопасно вывести их сам.

Например, этот фрагмент отклоняет непроверенные поля конфигурации и показывает предполагаемую область действия:

```yaml
DeployPreviewRequest:
  type: object
  additionalProperties: false
  required:
    - project_id
    - build_id
    - reason
  properties:
    project_id:
      type: string
      pattern: '^proj_[A-Za-z0-9]+$'
    build_id:
      type: string
      pattern: '^build_[A-Za-z0-9]+$'
    reason:
      type: string
      minLength: 8
      maxLength: 240
```

`additionalProperties: false` предотвращает знакомую проблему: агент видит в примере другого API поле `environment_variables`, помещает туда секреты или опасные переопределения, а сервер молча их применяет. Отклонение поля даёт агенту полезную ошибку, а не неожиданное развёртывание.

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

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

## Ожидаемый результат должен помогать принять следующее решение

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

Сначала определите успех в бизнес-терминах, затем выбирайте коды ответа. Для deployment полезный результат указывает deployment, его состояние и место, где сервер сообщит о дальнейшем прогрессе. Для обновления записи он указывает запись и подтверждает изменённые поля. Для удаления он подтверждает объект и возможность восстановления.

Компактный результат асинхронной операции может выглядеть так:

```json
{
  "status": "accepted",
  "deployment_id": "dep_2388",
  "project_id": "proj_4821",
  "build_id": "build_9017",
  "target": "preview",
  "operation_status": "queued"
}
```

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

Здесь многие документы OpenAPI вводят агента в заблуждение. Ответ `202 Accepted` означает, что сервер принял запрос на обработку, но обработка могла ещё не начаться и не завершиться. Если считать `202` тем же успехом, что и завершённый `200`, в журналах и сообщениях пользователю появятся ложные утверждения.

Разделяйте транспортный результат и результат действия. HTTP `200` может содержать бизнес-ошибку вроде `{ "state": "rejected", "reason": "build is not eligible" }`. И наоборот, `409 Conflict` может сообщать, что нужное состояние уже существует. Оболочка действия должна свести такие случаи к небольшому набору явных состояний: `completed`, `pending`, `already_in_desired_state`, `rejected` и `unknown`.

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

Фильтруйте подробности ошибки перед передачей агенту. Ошибка сервера может содержать внутренние URL, заголовки авторизации, трассировки стека или данные другого пользователя. Агенту нужна причина, с которой можно действовать, например «build ID не принадлежит project ID», и безопасный ID корреляции для проверки человеком. Страница исключения upstream ему не нужна.

## Подтверждение нужно в точке обязательства

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

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

Агент может час проверять состояние сборки, не беспокоя человека. Но перед продвижением сборки он должен показать проект, ID сборки, канал релиза и причину. Такой запрос можно оценить. Формулировка «Разрешить инструменту deployment» почти ничего не сообщает.

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

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

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

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

## Тайм-аут создаёт неизвестное состояние, а не указание повторить запрос

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

Рассмотрим знакомую ошибку. Агент вызывает `POST /v1/invoices` с клиентом, суммой и тайм-аутом. Соединение обрывается после сохранения счёта сервером, но до ответа. Агент видит тайм-аут, повторяет запрос с теми же данными, и сервер создаёт второй счёт. В журнале написано, что агент следовал политике повторов. Формально это правда, но практически бесполезно.

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

```text
Idempotency-Key: act_01HZX7FQ2Z9K8M6R4T3V1W0Y
```

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

Если в API нет документированной семантики идемпотентности, не повторяйте изменяющую операцию автоматически после тайм-аута. Верните `unknown` с идентификатором действия и предложите запрос только для чтения, который проверит состояние сервера. Если проверки нет, перед повтором запроса должен разобраться человек. Это неудобный ответ, потому что ситуация действительно неудобна. Притворная уверенность её не исправит.

OpenAPI может документировать параметр заголовка `Idempotency-Key`, но одна документация не гарантирует поведение сервера. Проверьте это специально: дважды отправьте один токен с одинаковым payload, затем отправьте тот же токен с изменённым payload. Первая пара должна привести к одному результату, а изменённый запрос сервер должен отклонить или явно обработать. Если он молча выполняет оба изменения, заголовок лишь создаёт видимость защиты.

Для других ошибок нужны отдельные правила. Считайте `401` и `403` условием остановки, а не поводом искать другую учётную запись. Считайте `429` причиной подождать только тогда, когда API сообщает задержку или у действия есть ограниченная политика повторов. Ошибки валидации полезны агенту лишь тогда, когда указывают разрешённое исправление.

## Аутентификация не даёт агенту права принимать решения

Объявление `security` в OpenAPI описывает, как клиент подтверждает свою личность перед API. Оно не говорит, должен ли агент вызывать операцию, может ли он использовать конкретный credential для определённого объекта или нужно ли человеку проверить эффект.

Security Requirement Object связывает операцию с именованными схемами безопасности. Bearer-схема может сказать клиенту отправить заголовок авторизации. Basic-аутентификация может объяснить, как собрать credential-заголовок. Это транспортная аутентификация. Не приписывайте ей большего смысла.

Разделяйте четыре вопроса:

- Кто или что вызывает это действие?
- Какой credential исполнитель использует во внешнем API?
- Какие объекты и эффекты разрешены этому credential?
- Какие попытки действия подтверждает человек?

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

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

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

Sallyport использует эту модель для HTTP-вызовов и SSH-команд: credentials остаются в зашифрованном хранилище, а агент получает результат действия, а не секрет. Но это помогает только при узких действиях и подходящих последствиям правилах подтверждения.

## Описание действия должно сообщать то, чего не говорит схема

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

Называйте операции по предполагаемому результату пользователя. `getBuildStatus` сообщает больше, чем `getBuildById`; `createPreviewDeployment` лучше, чем `postDeployment`. Название не должно обещать лишнего. Если сервер ставит работу в очередь, не называйте операцию `deployBuild`, пока результат не различает принятие и завершение.

В описании указывайте то, что агент иначе начнёт угадывать:

```yaml
operationId: createPreviewDeployment
summary: Queue one tested build for the preview environment
requestBody:
  required: true
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/DeployPreviewRequest'
responses:
  '202':
    description: Request accepted. Deployment work may still be pending.
  '409':
    description: The build already has a preview deployment or cannot enter preview.
```

Для операций с большими последствиями одного summary мало. Храните рядом с документом OpenAPI целевой объект, класс эффекта, необходимость подтверждения, правило повторов и состояния результата. Если ваши инструменты поддерживают расширения `x-`, их можно использовать, но явно обозначьте это как внутреннее соглашение. Стандартные парсеры OpenAPI проигнорируют неизвестные расширения, поэтому исполнитель должен обеспечивать их соблюдение, а не просто показывать их.

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

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

## Проверяйте действие на небрежном, но способном операторе

Тест happy path доказывает лишь то, что API работает при выполнении всех предположений. Проверяйте действие так, будто быстрый и способный оператор работает с неполным контекстом, устаревшими идентификаторами и склонностью повторять запрос после ошибки. Это достаточно похоже на сбои автономных агентов, чтобы такой тест был полезен.

Создайте небольшой стенд с временными объектами и аккаунтом с правами предполагаемого исполнителя. Затем проверьте границы:

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

Проверяйте не только конечное состояние API. Изучите prompt, который видит человек, точный запрос исполнителя, результат агента и аудиторскую запись. Успешный запрос всё равно нарушает контракт, если в prompt не был виден объект, результат слишком рано объявил завершение или журнал не различает отклонённый запрос и отказ upstream.

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

Отдельно проверьте отозванные и просроченные credentials. Исполнитель должен безопасно остановиться, вернуть понятное объяснение и не повторять вызовы с тем же непригодным credential. Цикл повторов для отклонённого credential переполнит журналы, вызовет ограничения частоты и усложнит простую проблему доступа.

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

## Публикуйте меньше действий и делайте каждое обоснованным

Небольшой каталог действий лучше универсального API-клиента, потому что для каждого действия можно определить честный контракт. Добавлять операции легко. Сложность начинается с поддержания правдивой семантики результатов, границ объектов, понятных подтверждений и корректного поведения при ошибках.

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

Не превращайте операцию в действие агента только потому, что генератор OpenAPI может открыть её за один день. Делайте это, когда можете объяснить, что произойдёт после тайм-аута, что именно подтвердит человек, что увидит агент и как позже доказать, какой запрос был выполнен. Если любой ответ зависит от фразы «агент, скорее всего, поступит разумно», оставьте endpoint за пределами каталога.
