# Должны ли массовые операции API сначала показывать свои цели?

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

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

## Количество не описывает масштаб последствий

Количество показывает, сколько записей затронет операция. Оно не говорит, какие именно. «482 подписки» звучит правдоподобно, но среди них могут оказаться корпоративный тенант, приостановленные аккаунты, которые нужно сохранить по закону, или записи из региона, о котором в запросе не говорилось.

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

Хороший предварительный просмотр отвечает на пять конкретных вопросов:

- Какой ресурс API и какое окружение получат запрос на изменение?
- Какой селектор сформировал этот набор?
- Какие записи в него входят, если указать стабильные ID и понятные человеку поля?
- Какое изменение получит каждая запись?
- Какие записи исключены явным правилом?

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

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

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

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

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

```http
POST /v1/subscriptions/selections
Content-Type: application/json

{
  "filter": {
    "status": "past_due",
    "region": "eu",
    "exclude_tags": ["disputed", "legal_hold"]
  },
  "fields": ["id", "customer_name", "status", "amount_due", "tags"]
}
```

Надёжный ответ может выглядеть так:

```json
{
  "selection_id": "sel_7f2c",
  "expires_at": "2025-03-08T15:00:00Z",
  "count": 482,
  "digest": "sha256:4c76...",
  "records": [
    {"id":"sub_104","customer_name":"Northwind Parts","status":"past_due","amount_due":3100,"tags":[]},
    {"id":"sub_219","customer_name":"Orchard Studio","status":"past_due","amount_due":450,"tags":[]}
  ]
}
```

Массив `records` может приходить через курсор, но ID выборки должен ссылаться на полный замороженный состав, а не только на текущую страницу. Интерфейс проверки может постранично показывать результат, не меняя объект, который проверяется.

После подтверждения агент отправляет сохранённую выборку, а не исходный фильтр:

```http
POST /v1/subscriptions/bulk-actions
Content-Type: application/json
Idempotency-Key: 9b03c6f0-7dfa-4f22-b0e5-4b52ca4f1a51

{
  "selection_id": "sel_7f2c",
  "expected_digest": "sha256:4c76...",
  "action": {"type": "pause_collection", "reason": "approved credit hold review"}
}
```

Сервер должен отклонять несовпадающий дайджест с ответом о конфликте. Успешный запрос должен возвращать ID операции и адреса результатов по отдельным записям, а не просто `{\"ok\": true}`. Общее сообщение об успехе скрывает частичное выполнение, а именно оно чаще всего становится проблемой в массовых операциях.

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

## Стабильная пагинация определяет смысл проверки

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

Используйте курсор, выданный сервисом, и уточните у поставщика API, читает ли этот курсор данные из снимка. Курсор, который лишь кодирует положение сортировки, тоже может сместиться, если отсортированное поле изменится. Особенно плохой вариант, когда действие само обновляет поле `updated_at`, по которому выполняется сортировка.

Если вы управляете API, явно укажите следующие свойства:

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

RFC 9110 относит методы POST, PUT, PATCH и DELETE к небезопасным, потому что они могут менять состояние сервера. Это не готовый дизайн рабочего процесса, но из него следует практическое правило: список, за которым идёт небезопасный метод, нельзя считать одной атомарной операцией только потому, что оба вызова стоят рядом в коде.

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

## Подтверждение должно описывать изменение, а не только записи

Проверяющему нужно подтвердить и состав набора, и эффект. Фраза «применить изменения к 482 записям» ничего не значит, пока интерфейс не сообщает, будут ли записи удалены, отключены, переназначены, списаны, опубликованы или изменены в конкретном поле. Для каждого изменяемого поля укажите прежнее и предполагаемое новое значение. Если всем записям назначается одно значение, достаточно краткого сводного описания.

Различайте безусловные и условные записи. Безусловная запись говорит: `status = archived` независимо от того, что произошло после предварительного просмотра. Условная запись говорит: «архивировать, только если status всё ещё равен inactive, а version всё ещё равна 17». Условные записи обычно безопаснее, потому что при изменении записи другим пользователем они завершаются отказом.

Используйте версию, ETag или последнюю известную ревизию в каждом изменении, если API это поддерживает. Это не заменяет манифест целей. Такой механизм решает отдельную проблему: к моменту выполнения проверенная запись может больше не соответствовать условиям.

Компактную запись о подтверждении можно представить так:

```json
{
  "request_id": "req_91a8",
  "selection_id": "sel_7f2c",
  "selection_digest": "sha256:4c76...",
  "target_count": 482,
  "action": {
    "type": "pause_collection",
    "precondition": {"status": "past_due"}
  },
  "approved_by": "operator account identifier",
  "approved_at": "2025-03-08T14:16:02Z"
}
```

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

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

## Частичное выполнение требует журнала и правила остановки

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

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

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

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

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

## Доступ на чтение может раскрыть столько же, сколько неудачная запись

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

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

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

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

## Агенту нужны полномочия на вызов, а не владение учётными данными

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

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

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

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

## Массовый рабочий процесс должен прекращаться при неоднозначном намерении

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

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

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

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

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