InquirerDespite economic problems, Undas flower vendors strive to survivePunchUganda begins troop withdrawal from SomaliaThe Jerusalem PostRapper Ja Rule comes to Tel Aviv, speaks on legacy, aging, and Tupac-Biggie row - interviewZDF heuteEntdecken Sie das ZDF-Nachrichtenstudio20 MinutenWie viel weisst du noch aus deiner Kindheit? Erzähls uns!The South AfricanHow the Springboks can lose World Rugby No 1 ranking this weekendNHK 社会鳴子峡近くの5人けが事故 「アクセルとブレーキ踏み間違えた」VanguardCatholic Priest leads congregation in song for Obi, KwankwasoPremium TimesUniversity cancels final-year students’ exam over ‘wild jubilation’GMA News2 sakay ng habal-habal, patay matapos mabangga at madaganan ng dump truck sa Negros OrientalEl ComercioResultados ONPE en Villa María del Triunfo al 99.676 %: sigue aquí el conteo oficial de las Elecciones Regionales y Municipales 2026GhaflaDCI Arrests Man Who Allegedly Spent KSh43,320 Mistakenly Sent to His Phone
The Daily Newsstand · Free, Always
Monday, November 2, 2026

Прежде чем дать AI-агенту доступ к сайту: права, повторы и потерянные правки

Translate

В нашей небольшой веб-студии решили попробовать передать AI-агентам часть рутинной работы с сайтами. Работаем мы в основном с MODX 2 и 3, поэтому мне поручили подобрать подходящий MCP-компонент. Задача выглядела несложной: посмотреть существующие решения, сравнить возможности и выбрать, что лучше подойдет.

Сначала я так и собирался сделать. Но потом полез в исходники и наткнулся на одну интересную вещь. В одном из компонентов был совершенно обычный PHP-цикл, который обновлял дополнительные поля страницы. Я решил проверить, что произойдет, если при сохранении возникнет ошибка. Первое поле записалось, второе нет, а обработчик завершил работу как ни в чем не бывало.

Вернулся к коду. Оказалось, результат setTVValue() просто никто не проверяет. Если агент получит сообщение об успешном выполнении команды, он будет считать, что все готово. Хотя на сайте могла сохраниться только половина изменений.

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

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

MODX здесь просто пример системы, с которой я хорошо знаком. Сами проблемы встречаются и в других CMS, да и вообще в любых приложениях, где AI-агентам разрешили менять рабочие данные.

Что сравнивал

Для начала отобрал пять проектов. В таблице указаны последние опубликованные версии по состоянию на 30 октября 2026 года.

Проект

Для чего предназначен

Последняя версия

karamble/modxmcp

MCP-сервер для MODX 3

1.2.0

dampilov94/mcp-component

Подключение внешних агентов к MODX 2

1.10.0

rumata-estor/modx3-mcp

Работа агентов с MODX 2 и 3

1.2.4

modxcms/modAI

AI-инструменты внутри панели управления

0.14.0-pl

ShevArtV/mxBoard

Управление задачами и их приемкой

Оформленных выпусков на GitHub нет

Первые три дают внешним агентам инструменты для работы с MODX. modAI работает внутри админки, а mxBoard занимается задачами и их приемкой. Это не пять одинаковых MCP-серверов, поэтому сравнивать их по количеству команд было бы странно. Зато можно посмотреть, как разработчики решают вопросы доступа и контроля результата.

Исходники пяти проектов я зафиксировал 26 октября 2026 года. Затем провел 25 проверок на PHP 8.3.6. Использовал настоящие классы компонентов, но вместо установленного MODX и базы данных подставлял тестовые объекты. Для проверки блокировок и повторных запросов запускал отдельные PHP-процессы.

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

Результаты всех проверок оставил под спойлером в конце статьи.

Проблема №1. От чьего имени агент выполняет команды

Допустим, контент-менеджер может редактировать только раздел с новостями. Цены, настройки сайта и чужие разделы ему недоступны. Подключаем ему AI-помощника, который должен выполнять часть работы. А какие права получит сам помощник?

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

У ResourceGetTool проекта karamble есть две проверки. Сначала вызывается проверка разрешения на операцию, затем проверяется доступ к самому ресурсу:

$this->requirePermission($modx, 'view a resource', ['view']);

$resource = $this->locate($modx, $arguments);

$this->requirePolicy(
    $modx,
    $resource,
    'view',
    'resource ' . $resource->get('id')
);

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

Здесь уже была реальная история с ошибкой. В issue №11 пользователь заметил, что его ограниченная учетная запись не видит некоторые данные в админке, но получает их через MCP. Исправление вошло в версию 1.2.0. После него участник обсуждения прогнал 57 проверок без ошибок и повторил тест на установленном пакете.

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

У dampilov94 и rumata-estor другая схема. Можно ограничить группы команд, доступных агенту, но сами операции идут через служебного пользователя с административными правами MODX.

Например, в коде rumata-estor, если служебная учетная запись не указана, выбирается первый активный пользователь с флагом sudo:

$query = $this->modx->newQuery(\MODX\Revolution\modUser::class);
$query->where(['active' => 1, 'sudo' => 1]);
$query->sortby('id', 'ASC');
$query->limit(1);

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

У modAI проверки находятся непосредственно в инструментах. Например, EditChunk вызывает hasPermission('save_chunk'). Я проверил эту ветку: без нужного права выполнение останавливается еще до чтения чанка.

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

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

Проблема №2. Агент прочитал данные, а они уже изменились

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

Команда выполнилась без ошибок, только изменение сотрудника куда-то исчезло. Для опытов я взял TV, дополнительные поля ресурсов MODX. Там часто хранят цены, характеристики и другие значения, но такая ситуация возможна и при работе с обычными записями в базе данных.

Проверять старое значение или просто сохранять новое

В обработчике обновления TV проекта dampilov94 нашел такой код:

foreach ($data['tvs'] as $tvName => $tvValue) {
    $resource->setTVValue($tvName, $tvValue);
}

Старое значение здесь не сравнивается с текущим. Я смоделировал ситуацию: после чтения поля человек записал human-edit, а агент попытался установить agent-edit. В результате осталось значение агента, а изменение человека пропало.

В ResourceTvUpdateTool проекта rumata-estor есть сравнение с ожидаемым значением:

if (!is_string($beforeValue)
    || (string)$resource->getTVValue($name) !== $beforeValue
) {
    throw new \ModxMCPClientException(
        'STALE_STATE {"status":"STALE_STATE","reason":"tv_values_changed"}'
    );
}

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

В моем тесте исходным значением было 111. После чтения я заменил его на human-edit. Агент попытался записать agent-edit, указав старое значение. Обработчик заметил расхождение и сохранил человеческую правку.

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

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

А если команда выполнилась, но ответ потерялся?

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

У dampilov94 для таких ситуаций есть RequestStore. Он запоминает состояние операции по идентификатору запроса. Когда операция уже завершилась, обработчик возвращает сохраненный ответ:

if ($record['state'] === 'completed') {
    return $this->response($key, $record);
}

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

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

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

Что дают блокировки

У rumata-estor есть общая файловая блокировка для операций записи, которые ее используют. Я запустил два PHP-процесса. Первый удерживал блокировку 350 мс, а второй дождался ее освобождения и только после этого продолжил работу.

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

В итоге получилось три разных механизма, каждый со своей задачей:

Механизм

Что позволяет контролировать

Идентификатор запроса

Повторное выполнение той же команды

Блокировка

Одновременную работу операций, использующих общую блокировку

Проверка ожидаемого значения

Изменение данных после того, как агент их прочитал

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

Проблема №3. Агент сообщил об успехе, но все ли сохранилось?

Вернусь к циклу, с которого все началось. Агент должен изменить два дополнительных поля. Первое записалось, а на втором setTVValue() вернул false. У dampilov94 результат вызова не проверяется, поэтому обработчик продолжает работу.

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

В rumata-estor тот же случай обрабатывается иначе:

$modx->beginTransaction();

try {
    foreach ($data['tvs'] as $name => $value) {
        if ($resource->setTVValue($name, $value) === false) {
            throw new \ModxMCPClientException(
                'Failed to write TV: ' . $name
            );
        }
    }

    $modx->commit();
} catch (\Throwable $e) {
    $modx->rollback();
    throw $e;
}

Источник: ResourceTvUpdateTool

Здесь проверяется результат каждой записи. Если одна не удалась, возникает исключение и вызывается откат. В тесте эта ветка сработала: обработчик вызвал rollback(), а тестовый объект вернулся к исходным значениям. На настоящей базе MySQL я это еще не проверял.

Похожий вопрос возник с modAI, только не с записью полей, а с обновлением кеша. В EditChunk проверяется сохранение чанка, после чего вызывается cacheManager->refresh(). Результат обновления кеша уже не проверяется.

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

У karamble после изменения ресурса выполняется повторное чтение. Можно получить состояние объекта после сохранения, хотя само по себе это еще не подтверждает выполнение всей задачи. А mxBoard предлагает смотреть на результат с другой стороны: отделить выполнение задания от его приемки.

В Transitions.php есть проверка того, кто вправе закрыть задачу:

if (!$isAuthor && !$isManager) {
    return [
        'allowed' => false,
        'reason' => 'mxboard_err_close_author_only'
    ];
}

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

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

К чему в итоге пришел

Компоненты оказались довольно разными. У каждого нашлись интересные решения, но и недостатков хватает. Для агента с правами конкретного сотрудника я бы сначала смотрел karamble. У dampilov94 понравилась обработка повторных запросов. modAI интересен для работы прямо из админки, а mxBoard пригодится, когда нужно распределять задания и отдельно принимать результаты.

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

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

Материалы и результаты тестирования

Здесь оставлю результаты 25 проверок, которые использовал при подготовке статьи. Все они выполнялись 26 октября 2026 года на PHP 8.3.6. Для работы с компонентами использовались их настоящие классы, но вместо установленного MODX и базы данных подставлялись тестовые объекты. В проверках повторных запросов и блокировок участвовали отдельные PHP-процессы.

Все 25 проверок, результаты и ревизии исходников

1. Права доступа

Проверка

Результат

karamble_uninitialized_session_rejected

Неинициализированная сессия обычного пользователя отклонена

karamble_initialized_session_accepted

Инициализированная сессия прошла проверку

karamble_sudo_session_exemption

Для пользователя с sudo сработало предусмотренное исключение

karamble_static_permission_guard

Статические проверки завершились: 25 passed, 0 failed

modai_permission_denied_before_read

Без разрешения save_chunk выполнение остановлено до чтения чанка

Все пять проверок завершились с PASS. При этом karamble_static_permission_guard запускал собственный набор из 25 статических проверок исходников. Это не дополнительные 25 интеграционных тестов.

2. Изменение TV и защита от потери правок

Проверка

Результат

dampilov_tv_last_write_wins

Последняя запись заменила предыдущее изменение

tv_stale_guard

Проверка ожидаемого значения обнаружила конфликт и сохранила чужую правку

tv_without_guard

Без предусловия чужая правка была перезаписана

dampilov_tv_false_write_return_ignored

Возврат false при записи второго TV не был обработан

tv_rollback

При отказе записи вызван rollback(), тестовый объект восстановил прежние значения

Все пять проверок завершились с PASS. Для проверки конфликта изменений использовался настоящий ResourceTvUpdateTool и тестовые объекты MODX.

Фрагмент проверочного кода:

$old = $m->resource->values;

$m->resource->values['primary'] = 'human-edit';
$before = $m->resource->values;

$guard = [
    'expected_tvs' => [
        'primary' => $old['primary']
    ]
];

mustFail(function () use ($t, $ctx, $guard) {
    $t->execute($ctx, [
        'resource_id' => 1,
        'tvs' => ['primary' => 'agent-edit'],
        '_runtime_preconditions' => $guard
    ]);
}, 'STALE_STATE');
demand(
    $m->resource->values === $before,
    'Human edit lost with guard'
);

Результат с проверкой ожидаемого значения:

{
    "case": "tv_stale_guard",
    "result": "PASS",
    "before": {
        "primary": "human-edit",
        "secondary": "222"
    },
    "after": {
        "primary": "human-edit",
        "secondary": "222"  
    }
}

Результат без предусловия:

{
    "case": "tv_without_guard",
    "result": "PASS",
    "observation": "Human edit overwritten",
    "after": {
        "primary": "agent-edit",
        "secondary": "222"
    }
}

Во втором случае PASS означает, что тест успешно воспроизвел потерю изменения.

3. Повторные запросы

Для этих проверок использовались настоящая реализация RequestStore, файловая система и отдельные PHP-процессы.

Проверка

Результат

duplicate_in_progress

Повтор во время выполнения отклонен с кодом 409

completed_replay

Повтор завершенного запроса вернул сохраненный ответ, действие выполнено один раз

same_id_different_payload

Прежний идентификатор с другими аргументами отклонен с кодом 409

different_id_control

Новый идентификатор позволил выполнить действие повторно

crash_after_effect

После прерывания процесса возвращена ошибка 409 с неизвестным результатом, повторного действия не произошло

Все пять проверок завершились с PASS. Особенно интересен результат crash_after_effect, когда процесс был прерван после выполнения действия, но до сохранения ответа.

{  
    "case": "crash_after_effect",
    "result": "PASS",
    "effects": 1,
    "reply": {
        "status": 409,
        "error": "Original request outcome is unknown. Check the site and request status; this ID will not be executed again."
    }
}

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

{  
    "case": "different_id_control",
    "result": "PASS",
    "effects": 2
}

В этом случае одинаковое действие выполнилось дважды.

4. Блокировки

Проверка

Результат

shared_mutation_lock

Два процесса, использующие общую блокировку, выполнились последовательно

nonparticipating_writer_control

Сторонний процесс смог изменить файл, не дожидаясь освобождения блокировки

Обе проверки завершились с PASS. Результат работы общей блокировки:

{  
    "case": "shared_mutation_lock",
    "result": "PASS",
    "order": [
        "enter-A",
        "leave-A",
        "enter-B",
        "leave-B"  
    ],
    "second_elapsed_ms": 328.7
}

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

5. Ошибки сохранения и откат

В обоих проектах моделировалась одинаковая ситуация. Первое TV-поле сохранялось, а при записи второго setTVValue() возвращал false.

dampilov94

{  
    "case": "dampilov_tv_false_write_return_ignored",
    "result": "PASS",
    "after": {
        "primary": "partial",
        "secondary": "before"
    },  
    "observation": "Actual trait returns normally after false from second setTVValue; no rollback in this method"
}

Метод завершился без ошибки, хотя изменилось только первое поле.

rumata-estor

{  
    "case": "tv_rollback",  
    "result": "PASS",  
    "before": {
        "primary": "agent-edit",
        "secondary": "222"
    },
    "after": {
        "primary": "agent-edit",
        "secondary": "222"
    },  
    "rollback_calls": 1
}

Здесь обработчик вызвал откат, а тестовый объект восстановил прежнее состояние. Настоящая база MySQL в этих экспериментах не использовалась.

6. Сохранение чанков и обновление кеша modAI

Проверка

Результат

modai_cache_refresh_called

После сохранения был вызван метод обновления кеша

modai_false_refresh_return_not_checked

Возврат false от обновления кеша не изменил успешный ответ обработчика

modai_save_failure_no_success

При отказе сохранения обработчик не сообщил об успешном завершении

Все три проверки завершились с PASS. При имитации отказа обновления кеша получен следующий результат:

{  
    "case": "modai_false_refresh_return_not_checked",
    "result": "PASS",
    "observation": "Stub refresh returns false; tool still returns success"
}

7. Управление задачами и приемка результатов mxBoard

Проверка

Результат

mxboard_executor

Исполнитель не смог закрыть чужую задачу

mxboard_author

Автор смог закрыть задачу

mxboard_self_default

Самостоятельная приемка автором-исполнителем по умолчанию запрещена

mxboard_self_setting

При включении соответствующей настройки самостоятельная приемка разрешена

mxboard_sudo

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

Все пять проверок завершились с PASS. При попытке закрыть задачу обычным исполнителем получен следующий результат:

{  
    "case": "mxboard_executor",  
    "result": "PASS",  
    "verdict": {
        "allowed": false,
        "reason": "mxboard_err_close_author_only"
    }
}

Для автора задачи результат оказался другим:

{  
    "case": "mxboard_author",
    "result": "PASS",
    "verdict": {
        "allowed": true,
        "reason": ""
    }
}

8. Точные ревизии исходников

Для каждой проверки использовались исходники конкретного Git-коммита. Это позволяет соотнести результаты с состоянием проектов на момент проведения экспериментов.

Проект

Git commit

karamble/modxmcp

7fe4b9501a3320c4f1bccba8253850e19df8c4fd

dampilov94/mcp-component

f7cfdfe77baeefe65e62fca29bfc3c9a81a2f69c

rumata-estor/modx3-mcp

5301ad54ef87fdb965aa6c40180b214f1d4148aa

modxcms/modAI

5bd5955a7e5686cd5b76a20503973f09beb89235

ShevArtV/mxboard

a640e9c37d64ca9843e662c175e16c7199c72955

9. Условия тестирования

Проверки проводились на PHP 8.3.6. Использовались настоящие классы компонентов, тестовые объекты MODX, отдельные PHP-процессы и файловая система для проверки повторных запросов и блокировок.

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

Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.