# Разрушительные параметры API: проверяйте перед удалением

AI-агенты слишком легко выполняют разрушительные API-вызовы, если запрос выглядит синтаксически правильным. Корректное тело JSON, знакомое имя аккаунта и успешный поиск не доказывают, что агент собирается удалить нужный объект. Перед любым внешним запросом, который удаляет, отзывает, отключает, отменяет или перезаписывает данные, проверьте идентичность, принадлежность, область действия и актуальность непосредственно через сервис.

Я видел, как опытные инженеры читали запрос как «удалить это тестовое окружение», тогда как API понимал его как «удалить все окружения в этой организации». В узком смысле в коде часто не было ошибки. Он принимал неточно заданный селектор, находил имя в другом аккаунте или доверял данным, полученным раньше в ходе запуска. Автономный агент выполняет такие обычные ошибки быстрее и с большей уверенностью.

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

## Корректно сформированный запрос все равно может указывать не на тот объект

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

Представьте сервис, который возвращает и отображаемое имя, и ID:

```json
{
  "id": "env_7d3a",
  "name": "staging",
  "account_id": "acct_blue",
  "state": "active"
}
```

Агент, который ищет `staging`, нашел кандидата, но не получил разрешение на его удаление. В разных аккаунтах может быть окружение `staging`. Даже в одном аккаунте имена можно повторно использовать после удаления старого объекта. Единственный безопасный запрос на действие нужно строить с возвращенным неизменяемым ID и проверять по ожидаемому ID родительского объекта.

Это особенно важно, когда API поддерживает родительский путь вроде `/accounts/{account_id}/environments/{environment_id}`. Проверяйте оба сегмента. Не делайте вывод о принадлежности только потому, что ID появился в предыдущем ответе поиска или агент указал в своем плане то же имя аккаунта. Родительский объект в URL действия входит в границу авторизации.

Тип ресурса требует отдельной проверки. API часто используют общий поисковый endpoint или возвращают записи разных типов. Результат с именем `staging` может быть окружением, проектом, группой доступа или сохраненным шаблоном. Если у сервиса разные endpoints для удаления, выбирайте endpoint только после проверки возвращенного типа. Не собирайте endpoint путем конкатенации строки типа, созданной моделью.

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

## Имена помогают людям, а запрос управляется ID

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

Полезная запись цели выглядит так:

```json
{
  "operation": "delete_environment",
  "account": {"id": "acct_blue", "name": "Blue Team"},
  "target": {"id": "env_7d3a", "name": "staging", "type": "environment"},
  "expected_state": "active",
  "effect": "permanently removes this environment and its managed resources"
}
```

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

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

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

Метки, ярлыки и описания дают контекст, но не определяют личность объекта. Они часто меняются, а пользователи могут записать в них что угодно. Метка вроде `temporary=true` может сузить уже проверенный список, но не должна заменять привязку к аккаунту или неизменяемый ID ресурса.

## Область действия должна быть конкретной до запроса подтверждения

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

Для удаления одного объекта нужен простой контракт: один неизменяемый ID, один ожидаемый родительский объект и один тип ресурса. Для массового удаления нужен другой контракт. Сначала он должен сформировать итоговый набор, затем получить подтверждение этого набора или ограниченного сводного представления, которое человек может проверить. Передача селектора прямо в разрушительный endpoint оставляет внешнему сервису решение об области действия уже после подтверждения.

Предположим, агент предлагает такой запрос:

```json
{
  "account_id": "acct_blue",
  "filter": {"label": "cleanup-candidate"},
  "delete": true
}
```

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

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

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

```json
{
  "operation": "delete_resources",
  "account_id": "acct_blue",
  "resource_type": "snapshot",
  "resource_ids": ["snap_104", "snap_105"],
  "selection_observed_at": "2025-03-08T14:32:11Z"
}
```

Отклоняйте пустой массив `resource_ids`, если рабочий процесс явно не допускает его. Отклоняйте повторяющиеся ID. Отклоняйте ID другого аккаунта. Устанавливайте максимальное количество объектов в соответствии с операцией, которую одобрил человек. Лимит не заменяет проверку, но не дает ошибочному циклу превратить удаление двух ресурсов в инцидент с тысячей ресурсов.

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

## Если цель может измениться, прочитайте сервис дважды

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

HTTP предоставляет для этого стандартный механизм. RFC 9110 определяет условные запросы с `If-Match`: сервер выполняет указанный метод только тогда, когда текущее представление совпадает с переданным клиентом тегом сущности. Ответ `GET` может содержать `ETag`, а последующий `DELETE` может передать это точное значение.

```http
GET /v1/accounts/acct_blue/environments/env_7d3a HTTP/1.1
Authorization: Bearer [injected credential]

HTTP/1.1 200 OK
ETag: "v42"
Content-Type: application/json

{"id":"env_7d3a","account_id":"acct_blue","name":"staging","state":"active"}
```

Сравнив тело ответа с утвержденной целью, отправьте:

```http
DELETE /v1/accounts/acct_blue/environments/env_7d3a HTTP/1.1
If-Match: "v42"
Authorization: Bearer [injected credential]
```

Если сервис вернул `412 Precondition Failed`, считайте это успешной работой защиты. Не просите агента повторить удаление без условия. Снова получите ресурс, сравните его с утвержденными фактами и запросите новое подтверждение, если изменился любой важный факт. Конфликт версии показывает, что старое одобрение может больше не относиться к этому объекту.

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

Не путайте успешный `GET` с разрешением на изменение. Учетные данные для чтения могут видеть больше, чем учетные данные для записи могут менять, а права доступа могут измениться независимо от состояния объекта. Только ответ на изменение показывает, принял ли провайдер запрос.

## Проверка должна выполняться на границе доступа к учетным данным

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

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

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

Минимальный шлюз может выполнять такую последовательность:

1. Убедиться, что запрошенная операция есть в списке разрешенных, а ее метод по замыслу разрушительный.
2. Получить каждый заявленный объект через вызов чтения в том же родительском аккаунте.
3. Сравнить возвращенные ID, тип, родительский объект и требуемое состояние со структурированным предложением.
4. Получить подтверждение установленного последствия, затем повторно проверить актуальность и отправить изменение.
5. Записать результат, включая ID запроса провайдера, если он его возвращает.

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

Учетные данные должны оставаться вне контекста агента. Агенту нужен результат разрешенного действия, а не bearer-токен, который можно скопировать в curl-команду, журнал или интеграцию третьей стороны. Sallyport использует такую модель для своих HTTP-действий: хранит учетные данные в зашифрованном хранилище, сам выполняет запрос и возвращает результат агенту.

## DELETE не означает, что запрос простой или обратимый

Название HTTP-метода не описывает полностью бизнес-последствие. RFC 9110 говорит, что `DELETE` просит исходный сервер удалить связь между целевым ресурсом и его текущей функциональностью. RFC намеренно не обещает, что данные исчезнут немедленно, связанные данные сохранятся или повторный запрос будет безопасным именно в вашем API.

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

Не отправляйте тело с `DELETE`, если провайдер прямо этого не документирует. RFC 9110 говорит, что содержимое запроса `DELETE` не имеет общепринятой семантики и может привести к отказу реализации. API удаления, который использует фильтры в теле, может быть корректным для конкретного провайдера, но требует дополнительного тестирования через документированный клиентский путь. Это не должно становиться поводом передавать агенту свободные селекторы.

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

Для неизвестного результата используйте правило «сначала проверить». Запросите точный неизменяемый ID в точном родительском объекте. Если объект больше не существует и модель удаления провайдера допускает такой вывод, запишите операцию как выполненную, указав неопределенность первого ответа. Если объект все еще существует, изучите его состояние и историю запросов провайдера, если она доступна, прежде чем решать, повторять ли операцию. Используйте ключ идемпотентности для операций, которые его поддерживают, но не приписывайте операции идемпотентность, которой провайдер не предоставляет.

Ответ `204 No Content` означает лишь, что сервер принял и завершил HTTP-взаимодействие согласно определению этого endpoint. Он не доказывает, что вся последующая очистка уже закончилась. Если следующее действие агента зависит от полного удаления, опрашивайте документированный статус операции или состояние ресурса, а не считайте пустое тело ответа гарантией.

## В подтверждении показывайте последствия, а не транспортные данные

Люди принимают более точные решения, когда подтверждение описывает последствие обычными словами и содержит идентификаторы для проверки. Метод, путь и тело JSON полезны API-инженеру, но перекладывают слишком много работы по разбору на человека, который должен заметить неверную цель.

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

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

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

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

## Доказательства аудита должны восстанавливать решение и не раскрывать секреты

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

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

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

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

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

## Создавайте разрушительные действия как узкие контракты

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

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

Затем пропустите через шлюз специально подготовленные ошибочные данные: корректный ID ресурса с неправильным аккаунтом, совпадающее отображаемое имя с двумя результатами, устаревший ETag, пустой список для массовой операции, отсутствующий фильтр, ресурс, пересозданный под тем же именем, и тайм-аут после отправки. Именно такие входные данные показывают, проверяет ли граница смысл или лишь валидирует JSON.

Не решайте неоднозначность, заставляя агента писать более длинный план. Сделайте неоднозначность невозможной в контракте действия. Агент может предложить намерение и собрать доказательства. Граница доступа к учетным данным должна решить, описывают ли доказательства одно разрешенное последствие, в данный момент и для этого аккаунта. Такое разделение дает систему, которую можно проверять в обычной ситуации и которой можно доверять в необычной.
