Читать 7 мин

Ослабляют ли откаты возможностей MCP контроль над агентами?

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

Ослабляют ли откаты возможностей MCP контроль над агентами?

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

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

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

Откат меняет маршрут, но не полномочия

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

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

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

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

Сначала сформулируйте инвариант безопасности, а уже потом составляйте матрицу:

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

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

Входные данные инициализации нужно проверять в небезопасных сценариях

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

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

Базовый запрос может выглядеть так:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "CURRENT_TEST_VERSION",
    "capabilities": {
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": { "name": "compat-fixture", "version": "1.0" }
  }
}

Затем заготовка должна создавать варианты, отличающиеся только одним существенным параметром:

  • самая старая поддерживаемая версия протокола;
  • текущая версия с {} в качестве capabilities;
  • текущая версия без поля capabilities, если ваш разбор это допускает;
  • объект возможностей без известных необязательных записей;
  • неподдерживаемая старая версия, которую сервер должен отклонить.

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

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

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32001,
    "message": "Session authorization required"
  }
}

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

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

Старые форматы сообщений не должны создавать старую модель безопасности

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

Обычно ответ должен быть простым: все поведение сохраняется. Авторизация сессии хранится локально. Проверка каждого вызова хранится локально. Запись аудита хранится локально. Для этого клиенту не нужно передавать эквивалентное поле в старом запросе.

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

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

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

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

Подтверждение сессии должно следовать за запросившим процессом

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

Запустите два независимых процесса-заготовки. Дайте им одно и то же отображаемое имя клиента, одну версию протокола и одно объявление возможностей. Процесс A инициализируется и запрашивает защищенное действие. Подтвердите его сессию. Затем процесс B инициализируется и запрашивает то же действие.

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

Затем проверьте граничные случаи жизненного цикла, с которыми сталкиваются рабочие агенты:

  1. Подтвердите процесс A и дождитесь его обычного завершения. Запустите B с теми же метаданными. Для B должно потребоваться подтверждение.
  2. Подтвердите A, затем завершите его без корректного закрытия. Запустите B. Для B должно потребоваться подтверждение.
  3. Подтвердите A, отзовите A из журнала сессии, пока процесс еще работает, и выполните из A еще один вызов. Вызов должен остановиться до выполнения.
  4. Отзовите A, пока его первое действие ждет внешнего ответа. После отзыва ожидающее действие не должно создавать второе одобренное действие.

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

Не используйте идентификатор сессии, переданный вызывающей стороной, как ключ подтверждения. Дефект легко выявить тестом: пусть B повторно использует метку сессии A, а шлюз все равно запросит подтверждение. Имя клиента нужно для отображения и диагностики, но оно не доказывает, что повторно обращается тот же исполняемый файл.

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

Проверяйте необычную активность клиентов
Проверяйте связанную хешами цепочку аудита офлайн с помощью sp audit verify, не открывая хранилище.

Настройка ключа «проверять каждый вызов» означает, что шлюз запрашивает согласие при каждом использовании этих учетных данных. Она не означает «один раз для каждого имени инструмента», «один раз для каждого открытого соединения» или «один раз, если клиент не старый». Проверяется реальный запрос, который передаст секрет или использует SSH-ключ.

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

Минимальная ожидаемая последовательность событий выглядит так:

session_authorized process=fixture-A
call_review_requested action=41 credential=deploy-token
call_completed action=41 result=success
call_review_requested action=42 credential=deploy-token
call_completed action=42 result=success

Важно не нумерование. Важно, что действие 42 не может использовать подтверждение, выданное для действия 41. Сначала отправьте второй запрос после завершения первого, а затем создайте случай, когда оба запроса приходят почти одновременно. Конкурентность выявляет реализации, которые хранят в сессии один временный флаг «проверка пройдена», вместо того чтобы связывать его с конкретным действием.

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

Добавьте жесткое утверждение об утечке секретов. Ответ MCP должен содержать результат действия, ошибку или состояние «требуется проверка». В нем не должно быть API-ключа, закрытого SSH-ключа, замаскированной подстановки вместо секрета или параметра инструмента, позволяющего агенту восстановить секрет. Откат - удобное место для адаптера совместимости, который случайно сериализует дополнительный контекст. Проверяйте необработанную расшифровку обмена, а не только структурированный объект теста.

Запись аудита должна находиться ниже согласования протокола

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

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

Sallyport формирует журналы Sessions и Activity из одного зашифрованного журнала аудита, связанного хешами. Для тестирования откатов это важно, потому что возможности клиента не должны решать, появится ли отдельный журнал. Один и тот же путь записи должен обрабатывать текущего клиента, неполного клиента и запрещенный вызов.

После каждого сквозного сценария добавляйте офлайн-проверку цепочки как отдельное утверждение:

$ sp audit verify
verified: 18 records
chain: intact

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

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

Стройте матрицу совместимости вокруг результатов безопасности

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

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

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

Ожидаемый результат нужно сформулировать простыми словами до запуска теста:

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

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

Для каждой строки собирайте три независимых наблюдения: необработанный ответ MCP, наблюдение на поддельном HTTP-сервисе или тестовом SSH-хосте и результат аудита. Одного ответа с ошибкой недостаточно, чтобы доказать, что действие не ушло наружу. Тестовая конечная точка сама по себе не доказывает, что шлюз записал отказ. Нужны все три наблюдения.

В стенограмме ошибки нужно указывать нарушенную границу

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

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

Client B initialized with oldest accepted version and no optional capabilities.
Client A had already received session approval and was still running.
Client B sent an HTTP action using the same displayed clientInfo name.
Gateway injected the credential and the test endpoint received the request.
No approval card appeared for B.
Activity journal recorded completion, but Sessions journal contained only A.

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

Второй сценарий ошибки тоньше:

Client initialized without the capability used for approval status updates.
Credential required per-call review.
First action displayed local review and completed.
Second action completed immediately.
Audit log recorded two completed actions and one review event.

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

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

Блокировка и отзыв требуют отдельных тестов

Не передавайте учетные данные в MCP
Агенты используют встроенный модуль sp mcp, а Sallyport подставляет учетные данные, не раскрывая их.

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

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

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

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

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

Условия выпуска должны пресекать незаметное выполнение

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

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

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

Вопросы и ответы

Сколько старых версий протокола MCP нужно тестировать?

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

Может ли возможность клиента MCP убрать запрос на подтверждение?

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

Что должно происходить, если клиент MCP отправляет возможности с неверным форматом?

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

Как проверить, что подтверждение сессии нельзя повторно использовать?

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

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

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

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

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

Что доказывает офлайн-проверка цепочки аудита?

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

Нужно ли тестам отката MCP покрытие от начала до конца?

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

Всегда ли отсутствие возможности MCP означает проблему безопасности?

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

Какие ошибки в тестах отката должны блокировать релиз?

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

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov