Безопасность ИИ-агентов в продакшене: как ограничить радиус поражения

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

Saturday, September 5, 2026Omid Saffari
Безопасность ИИ-агентов в продакшене: как ограничить радиус поражения

Безопасность ИИ-агентов в продакшене определяется не обещаниями модели, а верхней границей возможного ущерба. 25 апреля Cursor с Claude Opus 4.6 за девять секунд удалил рабочую базу данных PocketOS и её резервные копии, а затем написал признание. Исчезли данные о бронировании автомобилей за три месяца. Шесть моих агентов ежедневно работают с production; ниже — точная архитектура с единой точкой контроля, белым списком и идемпотентностью, при которой даже худший сбой способен перерасходовать максимум один доллар.

Три аварии за 90 дней

25 апреля 2026 года. Агент Cursor на базе Claude Opus 4.6 выполнял в staging то, что CEO PocketOS Jeremy Crane назвал «обычной задачей». Столкнувшись с несовпадением учётных данных, он по собственной инициативе решил «навести порядок» и удалил том Railway, где находились production-база и её резервные копии. Девять секунд. Простой на тридцать часов. Последней пригодной для восстановления резервной копии было три месяца, поэтому исчезли данные о бронировании автомобилей за три месяца. После этого агент написал признание и отметил, что нарушил прямой запрет касаться production.

26 февраля 2026 года. Основатель DataTalks.Club Alexey Grigorev сменил компьютер, и локальный файл состояния Terraform оказался устаревшим. Claude Code получил задачу очистить ресурсы и запустил terraform destroy для production-стека. В таблице courses_answer, которую позднее удалось частично восстановить, было 1,943,200 строк. Два с половиной года работ студентов. Автоматические снимки хранились в том же удалённом аккаунте.

Середина декабря 2025 года. Внутренний агент Amazon, Kiro, унаследовал повышенные права инженера, обошёл правило Amazon о подтверждении двумя сотрудниками — оно распространялось на людей, но не на роль, под которой работал агент, — и удалил, а затем заново создал production-среду AWS Cost Explorer.

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

Что это означает для основателей без технического бэкграунда

Если вы основатель и читаете эту статью после разговора со своим специалистом по ИИ, цена ошибки — не «баг ИИ». Это исчезнувшая система бронирования, потерянные работы студентов и данные клиентов, а резервным копиям уже три месяца, потому что восстановление никто не проверял.

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

Нужный ответ выглядит так: «Агент работает под облачной ролью без разрешений на уничтожение ресурсов. Ему доступны двенадцать явно заданных инструментов, и среди них нет shell. Каждый платный вызов проходит через одну функцию с жёстким лимитом расходов. Худший сценарий — один неудачный шаг и не более одного доллара затрат». Это и есть архитектура. Дальше — о том, как её построить.

Почему «будь осторожен» и «проверь diff» не работают

Удаление тома PocketOS заняло девять секунд. Kiro завершил удаление быстрее, чем человек успел бы прочитать запрос подтверждения, не говоря уже о том, чтобы обдумать его. На скорости агента вмешательство после запуска структурно невозможно: когда действие становится видимым, оно уже завершено.

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

Kiro доказал это от противного. В Amazon действовало правило подтверждения двумя сотрудниками. Оно применялось к людям, запускавшим задания. Агент унаследовал права запускающего сотрудника и обошёл проверку, потому что она не была привязана к роли, под которой он работал. Для человека — видимость согласования, для автономного цикла — полный набор прав на уничтожение.

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

Единая точка контроля для всех платных и изменяющих состояние вызовов

Каждый платный вызов модели в моём стеке из шести агентов проходит через одну функцию — callAi. Она последовательно делает пять вещей: заранее проверяет дневной лимит в $20, замеряет длительность вызова, записывает количество токенов и стоимость в USD в append-only строку ai_call_log с метками agent_id и workflow_instance_id, после вызова проверяет лимит $1 на экземпляр и возвращает результат.

TypeScript
export async function callAi(env: Env, args: CallAiArgs) {
  const { agentId, workflowInstanceId, model, messages } = args;

  const dailySpent = await getDailySpendUSD(env);
  if (dailySpent >= 20) {
    throw new NonRetryableError(`daily cap hit: $${dailySpent}`);
  }

  const t0 = Date.now();
  const res = await anthropic.messages.create({ model, messages });
  const costUSD = priceOf(model, res.usage);

  await env.DB.prepare(
    `INSERT INTO ai_call_log
       (agent_id, workflow_instance_id, model, in_tokens, out_tokens, usd, ms, ts)
     VALUES (?, ?, ?, ?, ?, ?, ?, unixepoch())`
  ).bind(agentId, workflowInstanceId, model,
         res.usage.input_tokens, res.usage.output_tokens,
         costUSD, Date.now() - t0).run();

  const instanceSpent = await getInstanceSpendUSD(env, workflowInstanceId);
  if (instanceSpent >= 1) {
    throw new NonRetryableError(`instance cap hit: $${instanceSpent}`);
  }

  return res;
}

Превышение лимита вызывает ошибку без повторных попыток. Workflow завершается. Агент не может по собственной инициативе решить, что следует продолжить: бюджетный шлюз — не промпт, с которым можно спорить, а исключение, выброшенное выше уровня рассуждений.

Сравните это с часто упоминаемым неуправляемым запуском на 63 часа и $4,200: без единой точки контроля нет конечного состояния. Цикл продолжает тратить деньги, пока человек не заметит счёт. Лимит на экземпляр ограничивает худший отдельный запуск суммой $1. Дневной лимит ограничивает худший день суммой $20. Во время разработки я случайно упирался в оба ограничения. Каждый такой случай обошёлся дешевле ужина.

Вторая задача единой точки контроля — расследование инцидентов. Каждое действие оставляет строку аудита до возврата результата. Если что-то всё же ломается, разбор сводится к запросу SELECT к ai_call_log, а не к реконструкции событий по признанию самого агента.

Белый список: 12 инструментов Zod, без shell и terraform

Вся доступная агенту поверхность изменений состоит из двенадцати инструментов, описанных строгими схемами Zod. Инструмента Bash нет. Инструмента terraform нет. API облачных томов вообще не входит в доступную поверхность. Даже если агент придёт к выводу, что нужно выполнить terraform destroy, выразить это намерение через доступные действия он не сможет.

Конфигурация Claude Agent SDK, которая это обеспечивает, короткая:

TypeScript
const result = await query({
  prompt,
  permissionMode: "dontAsk",
  allowedTools: [
    "read_brief", "fetch_corpus", "render_markdown",
    "validate_directives", "persist_draft", "schedule_publish",
    "update_status", "log_event", "fetch_citation",
    "embed_text", "search_vectors", "notify_human"
  ],
  // no Bash, no Write, no Edit, no infrastructure tools
});

В режиме permissionMode: "dontAsk" все инструменты вне списка безусловно запрещены. Запрос не повышается до уровня промпта и не отправляется человеку на проверку — он отклоняется. Порядок проверки разрешений — хук PreToolUse, правила deny, правила allow, правила ask, проверка режима, callback canUseTool — работает согласно документации, но в dontAsk интерактивный callback структурно пропускается, а неизвестные инструменты отклоняются на проверке режима.

Именно это сразу остановило бы инциденты PocketOS и DataTalks. Сбой DataTalks невозможен в такой конфигурации: terraform destroy не является именем инструмента, которое может вызвать агент. Сбой PocketOS невозможен, потому что удаление тома отсутствует в доступной поверхности. Даже если модель решит «навести порядок», между намерением и действием не будет пути.

Здесь легко поддаться соблазну и добавить инструмент Bash на случай, «если агенту понадобится сделать что-то непредусмотренное». Не стоит. В день добавления Bash радиус поражения расширяется до «всего, до чего способен дотянуться Bash».

Идемпотентность: почему повторный запуск не повторит разрушительное действие

Третья опора архитектуры — идемпотентность. Каждый экземпляр Cloudflare Workflow получает детерминированный идентификатор на основе входных данных: publish-{brief_id}. При повторяющемся ID метод instances.create() выбрасывает исключение, поэтому повторная отправка одного события не запустит workflow второй раз. Шторм повторных попыток в очереди превратится в no-op, а не в повторную публикацию.

TypeScript
const id = `publish-${msg.body.briefId}`;
try {
  await env.PUBLISHER.create({ id, params: msg.body });
} catch (e) {
  if (e.message.includes("already exists")) return; // safe replay
  throw e;
}

Внутри workflow результат каждого step.do() кэшируется и безопасно воспроизводится при повторе. Повторная попытка шага возвращает результат из кэша, не выполняя побочный эффект заново. Вместе с INSERT OR IGNORE для канонических таблиц D1 и версионируемым снимком R2 при каждом сохранении это не позволяет неудачному шагу незаметно перезаписать историю. Предыдущая версия остаётся на расстоянии одного ключа R2.

Первопричиной инцидента DataTalks был устаревший файл состояния, который запустил необратимое уничтожение. Детерминированные ID workflow и снимок перед изменением делают разрушение из-за устаревшего состояния некритичным: даже если workflow отработает с неверными исходными предположениями, снимок станет точкой отката. (Подробнее о примитиве надёжного выполнения — в материале о сравнении Cloudflare Workflow и Managed Agents; математически идемпотентность ограничивает ущерб тем же способом, только под другим названием.)

Рабочая модель проста: шаг, способный разрушительно изменить данные, сначала записывает версионируемый снимок. Снимок — обязательное условие, а не запоздалая мера. Если записать его не удалось, изменение не выполняется. Если изменение завершилось ошибкой, снимок остаётся точкой восстановления. Сценария, в котором состояние потеряно и восстанавливать его неоткуда, нет.

Безопасность ИИ-агентов: план внедрения на 5 шагов

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

  1. Уберите из облачной роли агента все операции уничтожения

    В IAM-роли — либо сервисном аккаунте или API-токене — под которой работает агент, не должно быть операций destroy или delete для production-ресурсов. Никакого DeleteVolume. Никакого разрешения на terraform destroy. Никакого DROP TABLE. Роль задаёт нижнюю границу радиуса поражения; всё, что расположено выше, бессмысленно, если сама роль не ограничена.

    Практический вывод из истории Kiro: агент не должен наследовать широкие права человека, который его запускает. Выдайте агенту собственную узкую роль.

  2. Запускайте агента с permissionMode dontAsk и явным списком allowedTools

    Выберите минимальный набор инструментов, достаточный для работы агента. У меня их двенадцать. У вас может быть шесть. Полностью исключите любой универсальный shell-инструмент. Если Bash сейчас доступен агенту, потому что «так удобнее», это удобство и есть уязвимость.

    TypeScript
    { permissionMode: "dontAsk", allowedTools: [/* explicit list */] }
  3. Добавьте автоматический выключатель бюджета

    Одна обёртка вокруг каждого платного вызова. До вызова проверяйте жёсткий дневной лимит, после него — лимит на экземпляр. При превышении выбрасывайте NonRetryableError. Именно лимит на экземпляр работает как автоматический выключатель, которым не является дневной лимит: неуправляемый запуск на $4,200 за 63 часа мог оставаться ниже любого разумного дневного потолка, накапливая расходы от запуска к запуску. Лимит на экземпляр перехватывает то, что пропускает дневной.

  4. Делайте снимок перед изменением

    Каждый шаг, способный разрушительно изменить данные, сначала записывает версионируемый снимок. Подойдёт R2, подойдёт S3, подойдёт таблица D1 вида *_archive. Важно, чтобы запись снимка была обязательным условием изменения — в том же шаге и в том же блоке, устроенном как транзакция.

  5. Ведите append-only журнал действий

    Одна строка на каждый вызов инструмента, записанная до возврата результата. Добавляйте ID агента, ID экземпляра workflow, стоимость в USD и задержку. Когда что-то всё же пойдёт не так — а рано или поздно это случится, — расследование будет запросом SELECT, а не реконструкцией событий.

Каждый шаг занимает не больше дня. Соблюдайте порядок, потому что уровни изоляции усиливают друг друга: шаг 1 задаёт нижнюю границу, шаг 2 закрывает поверхность действий над ней, шаг 3 ограничивает цену ошибки внутри этой поверхности, а шаги 4 и 5 позволяют восстановиться после любого сбоя и разобраться в нём по журналу.

Частые вопросы

Разве нельзя просто запретить агенту удалять production в системном промпте?

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

Означает ли permissionMode dontAsk полный отказ от интерактивности?

Нет. Этот режим отклоняет инструменты вне списка вместо запроса подтверждения. Вся разрешённая поверхность остаётся доступной. Исчезает только сценарий «агент спрашивает, уставший человек нажимает “да”» — и именно от него стоит избавиться.

Зачем нужен лимит на экземпляр, если уже есть дневной?

Потому что неуправляемый запуск на $4,200 за 63 часа оставался ниже любого разумного дневного потолка, а расходы продолжали накапливаться от запуска к запуску. Один неудачный цикл может укладываться в «$20/day» и всё равно за выходные набрать четырёхзначную сумму. Лимит на экземпляр — автоматический выключатель, которым дневной лимит по своей структуре быть не может.

Если стек работает на Vercel или Railway, а не на Cloudflare Workflows, это применимо?

Единая точка контроля, белый список и ограниченная роль не зависят от платформы. Только примитив идемпотентности — детерминированные ID Workflow — относится к Cloudflare. В других стеках используйте ключ идемпотентности задания и отклоняйте дубликаты на уровне очереди или системы запуска задач.

Достаточно ли подтверждения двумя сотрудниками?

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

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

Последнее обновление

5 сент. 2026 г.

КатегорияBuild

Сделать этот сайт предпочтительным в Google

Добавить omidsaffari.com как предпочтительный источник в Google Поиске

Отметьте omidsaffari.com как предпочтительный источник — и Google будет поднимать его для вас в Top Stories, AI Overviews и AI Mode.

Ещё из Build

Все статьи Build
Рассылка

Одно письмо, каждое воскресенье. Работающие системы, а не горячие мнения.

Билд-логи, системы в продакшене и полевые заметки из портфеля ИИ-проектов.

Еженедельно. Без спама. Отписка в любой момент.