Статья ·

Как дать AI-агенту править сайт, сохранив контроль над публикацией

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

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

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

В K39, моём путеводителе по Калининградской области, для допустимых правок страницы места предусмотрено отдельное предложение изменений. Ниже разберу, как оно сохраняется, что при этом получает читатель и какая ошибка возникла в коде применения. Начальный пример — условная ситуация, а не история сбоя у пользователей. Разбор опирается на открытый код 0420863 и локальную проверку от 28 октября 2026 года.

Почему запрета «публиковать» мало

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

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

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

Агент сохраняет предложение, читатель получает прежнюю страницу

Агент работает с K39 через MCP — интерфейс вызова инструментов. Сервер определяет права по токену доступа: draft позволяет готовить материал, editor — публиковать его и редактировать опубликованное.

Для страницы места с правами draft действуют три разных правила:

  • Неопубликованный черновик можно редактировать напрямую.
  • Допустимые текстовые правки опубликованной или архивированной страницы сохраняются отдельно, как предложение.
  • Явная попытка установить published отклоняется с ошибкой 403.

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

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

Разделение имеет смысл только при соответствующих правах. Если выдать агенту редакторский токен, он получит редакторские возможности. В проверенном коде одинаковые значения двух настроенных токенов тоже дают editor. Назвать один из них «черновым» недостаточно — важно, какие права сервер действительно определил. Проверка токенов.

Успешное сохранение ещё не объясняет, что увидел читатель

У этого механизма есть неочевидная деталь: ответ инструмента может одновременно содержать новый заголовок, publicationStatus: published и pendingReview: true.

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

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

Право применить правку не гарантирует, что она применится

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

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

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

Тест «редактору разрешён статус published» такую ошибку не поймал бы. Нужна проверка результата всей операции: какой текст остался в основной записи и что произошло с предложением. Идентификатор объекта и изменение объекта могут выглядеть одинаково в JSON, но означают разные действия.

Что это решение не берёт на себя

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

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

Наконец, право публикации не заменяет редактуру. Отдельное ревью в K39 — правило процесса: проверенный путь не требует подтверждения от второго независимого пользователя. Даже одобренный текст может содержать ошибочные сведения; проверка источников остаётся отдельной работой. Редакторский процесс, путь публикации.

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

Как проверить этот сценарий самостоятельно

При подготовке первой версии статьи прошёл 181 тест из семи выбранных файлов: 28 проверок функций и структуры вызовов и 153 теста с временной PostgreSQL 18. Это локальная проверка от 28 октября 2026 года, а не аудит развёрнутого сервера.

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

Для запуска файла с этим и связанными сценариями нужен checkout коммита 0420863, зависимости по его lockfile, Node 24.21.0, pnpm 12.15.0 и работающий Docker. Из корня K39:

env -u DATABASE_URL_TEST -u DATABASE_URL -u NUXT_DATABASE_URL \
  pnpm exec vp test run --project packages/database \
  packages/database/src/repository/test/pending-publish.writer.test.ts

В этом файле 22 теста из указанной выборки. При отсутствии настроенного URL базы тестовая конфигурация создаёт временную PostgreSQL через Testcontainers. Условия запуска.

Права в этих тестах выбираются внутри процесса; отдельные unit-тесты проверяют соответствие токена и уровня доступа. Настоящий клиент и развёрнутый MCP-сервер в эту проверку не входили. Результат не подтверждает, какие токены выданы агентам в production.