Перейти к содержимому

Контракт шлюза v2 ​

ARCHIVE

Архивное исследование / черновик. Не является актуальным контрактом реализации. Смотрите REST 1.0 и актуальную концепцию.

Черновик на согласование · 2 октября 2026 · не реализовано. Эта страница предлагает точные контракты для решения 002. Ничего из описанного ещё не работает. Модуль по-прежнему говорит на протоколе моста v1, а Agentfy пока не умеет передавать контекст вызова, нужный этому контракту. PHP-часть нужно согласовать с Artem Belikov, часть Agentfy — с командой Agentfy, прежде чем начинать реализацию (STOR-17).

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

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

Машиночитаемый контракт лежит в папке contracts/ репозитория. bun run contracts:check проверяет, что файлы согласованы между собой и каждый пример соответствует своей схеме. Это доказывает согласованность документов, а не то, что что-то работает.

ФайлЧто фиксирует
contracts/module-rest/openapi.yamlREST API модуля, OpenAPI 3.1
contracts/module-rest/signature-vectors.jsonТестовые значения подписей для PHP и TypeScript
contracts/store-deputy/module-callbacks.openapi.yamlЧто модуль вызывает в Store Deputy (этап B)
contracts/mcp/tools.yamlИнструменты MCP Store Deputy, их схемы и примеры
contracts/context/call-context.yamlЗаголовки от Agentfy, гранты и разрешённый контекст
contracts/errors.yamlКаждый код ошибки, его HTTP-статус и что видит агент

Три границы полномочий ​

text
Runtime Agentfy ──(1) ключ агента + (2) грант разговора──▶ MCP Store Deputy
                                                            │ права, аудит
                                                            │
                 (3) подпись подключения, никогда (1) или (2) ┘
                                                            ▼
                                              REST API модуля OpenCart
ГраницаДоказательствоКто выдаёт, кто хранитДействует только вНикогда
Агент → Store DeputyAuthorization: Bearer sdak_…, один на созданного агентаStore Deputy; Agentfy хранит как секрет командыMCP Store DeputyНе пересылается в магазин, не показывается модели, не считается полномочием человека
Человек или покупательX-SD-Context: sdcg_…, один на разговорStore Deputy; Agentfy несёт его для каждого разговораMCP Store Deputy, для этого агента и тредаНе имеет собственных прав; не переживает отозванного членства
Store Deputy → магазинHMAC секретом подключения, в обе стороныStore Deputy и модуль, при подключенииМодуль одного этого магазинаНе покидает серверов Store Deputy или магазина

Storefront-сессия покупателя — четвёртый секрет, и она никогда не покидает магазин. Модуль проверяет её и передаёт Store Deputy только непрозрачную ссылку (см. покупатели).

MCP Store Deputy ​

Транспорт. Один адрес {STORE_DEPUTY_API}/mcp, Streamable HTTP, версия протокола 2025-06-18 (именно её сегодня отправляет клиент Agentfy). Сервер без состояния: Agentfy открывает новую MCP-сессию на каждый вызов, поэтому initialize должен быть дешёвым, и никакое состояние не может держаться на Mcp-Session-Id. tools/list возвращает все инструменты одной страницей, потому что Agentfy не идёт по nextCursor.

Инструменты Admin Agent (этап A). Все только читают; всем, кроме store_status, нужна возможность catalog.read.

ИнструментЧто делает
store_statusКакой магазин, доступен ли, на паузе ли, какие инструменты сейчас работают
catalog_find_productsПоиск по SKU, модели, EAN и другим идентификаторам; возвращает все совпадения и никогда не выбирает одно
catalog_get_productsАктуальные данные по ID товара; отсутствующие ID перечислены
catalog_list_productsПостраничный просмотр каталога, при желании — только недавние изменения

Инструменты Frontend Agent (этап B): cart_get, cart_add_item, cart_update_item, cart_remove_item. Изменение цен выключено, пока у STOR-5 не появится собственная проверка согласования.

Правила для каждого инструмента:

  • Ни один аргумент не называет магазин, команду, человека, покупателя, разговор, право на цену или разрешение. Каждая объектная схема запрещает лишние поля. Передача store_id отклоняется с invalid_arguments и ничего не выбирает. Скрипт проверки контролирует оба правила.
  • Имена — только буквы, цифры и подчёркивания. Провайдеры моделей за Agentfy отвергают точки, а Agentfy молча убирает имена, которые провайдер не принимает.
  • Инструмент появляется в списке только тогда, когда он есть в профиле агента, манифест магазина объявляет нужную возможность и подписка команды это разрешает. Вызов инструмента вне списка отклоняет сервер, а не только скрывает.
  • Результаты. Успех возвращает structuredContent и тот же JSON текстом. Ошибка устанавливает isError и возвращает { "error": { "code", "message", "retryable", "details" } }. Agentfy сегодня не читает isError, поэтому сообщение должно быть понятно модели само по себе.
  • Текст магазина — это данные. Названия товаров и опций возвращаются как поля и никогда не встраиваются в инструкции. Товар с названием «ignore previous instructions» ничего не меняет.
  • ID — текстом ("42"), чтобы схема была одинаковой на всех платформах; деньги — десятичной строкой с четырьмя знаками.

Доверенный контекст ​

Что отправляет Agentfy ​

ЗаголовокКто устанавливаетЧто проверяет Store Deputy
Authorization: Bearer sdak_…Agentfy, из секрета команды storedeputy:Authorization (работает уже сегодня)Известен и не отозван; даёт команду, магазин и профиль
X-Agentfy-Agent-IdRuntime AgentfyСовпадает с агентом, которому выдан ключ
X-SD-Context: sdcg_…Runtime Agentfy, для каждого разговораСуществует, не просрочен и не отозван, выдан этому агенту и этому треду
X-Agentfy-Thread-IdRuntime AgentfyСовпадает с тредом, для которого выдан грант
X-Agentfy-Turn-IdRuntime AgentfyТолько для аудита
X-Agentfy-Tool-Call-IdRuntime AgentfyСтановится стабильным ключом операции для изменения
X-Agentfy-Initiated-ByRuntime Agentfyhuman, heartbeat или agent; грант человека могут нести только ходы human

Модель не устанавливает ни один из них. Заголовки идут тем же TLS-запросом, что и ключ агента, поэтому они ровно настолько надёжны, насколько надёжен сам Agentfy. Подписанный токен контекста не нужен, пока между Agentfy и Store Deputy не появится посредник.

Что выводит Store Deputy ​

На каждом вызове инструмента, до любого запроса в магазин, Store Deputy превращает заголовки в один разрешённый контекст: команда, магазин, профиль, актор и действующие права. Код инструмента видит только этот результат.

АкторКогдаЧто может
employeeЧеловек открыл разговор в app; грант называет пользователя Store DeputyПересечение гранта, его текущей роли в команде, профиля агента, подписки и возможностей магазина
agentХод по расписанию или от другого агента, без грантаЧтения, которые владелец разрешил агенту; никогда не изменение, требующее согласования человека
shopperПосетитель открыл чат на витрине; грант называет привязку покупателяТолько собственная корзина, пока модуль подтверждает привязку

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

Чего Agentfy ещё не делает ​

Это результат чтения ветки main Agentfy 2 октября 2026 года. Это требования к STOR-22 и связанным задачам, которые нужно согласовать с командой Agentfy, а не изменения, которые делаются здесь.

Сегодня в AgentfyНужно
Вызовы MCP несут только статические заголовки, общие для команды; по правилу никакого контекста агента, пользователя или разговора не отправляетсяПо явному включению для конкретного MCP-сервера — заголовки из таблицы выше, которые устанавливает runtime
Нет секрета на уровне разговораХранить грант Store Deputy для каждого треда вне prompt и отправлять его в вызовах этого треда
Ходы посетителей вообще не получают MCP-инструментовРазрешить их для сервера, помеченного как frontend (этап B)
isError не проверяетсяСчитать такой шаг инструмента неудачным (рекомендуется)
Треды кабинета привязаны к пользователю AgentfyОтдельный тред для каждого человека Store Deputy, чтобы их гранты никогда не пересекались
Нет сервисного ключа; создание агентов и MCP-серверов требует сессии пользователяСервисный способ создать агента (STOR-23)

Как Agentfy получает грант в начале разговора, зависит от того, кто открывает разговор, а это ещё открытый вопрос (см. открытые пункты). Предложение: разговор открывает Store Deputy, потому что именно в app человек вошёл в аккаунт.

REST API модуля ​

Транспорт ​

У магазина одна точка входа, которую модуль сообщает при подключении как endpoint_url (для OpenCart — storedeputy.php в корне магазина). Логический путь передаётся параметром запроса path, потому что многие хостинги не передают PHP остаток URL:

text
GET https://shop.example/storedeputy.php?path=/v2/catalog/products&limit=100

Только HTTPS, JSON в UTF-8, тело не больше 1 МБ, перенаправления не выполняются. Ответил ли модуль, решает подпись ответа, а не HTTP-статус: страница проверки CDN, страница обслуживания или фатальная ошибка PHP не имеют действительной подписи и считаются «магазин недоступен». Подписанный ответ не из 2xx — настоящий отказ с телом ошибки.

ОперацияЭтапРаботает на паузе
GET /v2/health — доступность, часыAда
GET /v2/manifest — платформа, лимиты, возможности, предупрежденияAда
POST /v2/connection — подключениеAда
DELETE /v2/connection — отключениеAда
POST /v2/connection/secrets — замена секретаAда
GET /v2/catalog/products — постраничный просмотр товаровAнет
POST /v2/catalog/product-lookups — по ID и по идентификаторуAнет
GET /v2/operations?keys=… — что случилось с изменениемAнет
GET /v2/shopper-bindings/{ref}/cartBнет
POST /v2/shopper-bindings/{ref}/cart/itemsBнет
PATCH, DELETE /v2/shopper-bindings/{ref}/cart/items/{id}Bнет

Подписи ​

Каждый запрос и каждый ответ несут X-SD-Connection, X-SD-Key-Id, X-SD-Timestamp, X-SD-Nonce, X-SD-Request-Id и X-SD-Signature: sd2=<hex HMAC-SHA256>. Authorization не используется, потому что распространённые конфигурации PHP его не передают.

text
SD2-REQ                       SD2-RES
{connection_id}               {connection_id}
{key_id}                      {key_id}
{timestamp}                   {timestamp}
{nonce}                       {nonce}
{request_id}                  {request_id}
{METHOD}                      {nonce запроса}
{логический путь}             {status}
{канонический query}          {sha256 hex тела}
{sha256 hex тела}
  • Канонический query — все параметры, кроме path; имена и значения закодированы по RFC 3986, отсортированы по имени, соединены через &. Повторённый параметр отклоняется.
  • Ключ HMAC — текст секрета как байты UTF-8, как в v1.
  • Ответ привязан к nonce запроса, поэтому ответ нельзя подсунуть другому запросу. В v1 этого не было.
  • Модуль отклоняет метку времени, отличающуюся от его часов больше чем на 300 секунд, и nonce, который он видел за последние 10 минут. Хранилище nonce в v2 обязательно; в v1 его так и не сделали.
  • Собственные вызовы модуля в Store Deputy (этап B) используют SD2-MREQ и SD2-MRES, поэтому подпись из одного направления никогда не сработает в другом.

Подключение использует SD2-PAIR и ключ подключения Store Deputy (ECDSA P-256) вместо секрета подключения, а модуль отвечает манифестом, подписанным предложенным секретом, как в v1. signature-vectors.json содержит фиксированные входы и выходы, в том числе UTF-8 и зарезервированные символы; проверка для PHP 7.3 — в contracts/scripts/vectors.php.

Отзыв, замена ключа и пауза ​

  • Отключение удаляет секреты и все привязки покупателей в магазине; каждый следующий запрос получает not_paired. Работает и на паузе, потому что только уменьшает доступ.
  • Замена секрета отправляет следующий секрет под новым ID ключа sec_N, подписанный текущим. Модуль отвечает уже новым секретом и принимает оба не дольше 10 минут. Тот же ID ключа с другим значением — secret_conflict.
  • Pause — выключатель владельца в OpenCart. Модуль отклоняет всё, кроме health, манифеста и операций подключения, с paused (HTTP 423). Это не зависит от того, правильно ли ведёт себя Store Deputy.
  • В Store Deputy отзыв ключа агента, гранта или членства в команде действует со следующего вызова.

Возможности и версии ​

Манифест перечисляет возможности с собственными версиями: catalog.read (этап A), cart.read и cart.write (этап B). price.base.write зарезервирована для STOR-5 и не предлагается. Возможность, которую манифест не объявляет, недоступна, что бы ни содержал код модуля.

Путь несёт основную версию (/v2), манифест перечисляет поддерживаемые версии major.minor. Минорная версия только добавляет необязательные поля, операции и возможности, а обе стороны игнорируют неизвестные поля. Store Deputy поддерживает текущую и предыдущую основную версию.

Изменения без дублей ​

Каждое изменение несёт X-SD-Operation-Key, стабильный для намерения, а не для попытки. Store Deputy выводит его из разговора и ID вызова инструмента от модели, поэтому повтор того же вызова использует тот же ключ, а новый запрос человека получает новый.

  1. Модуль записывает ключ вместе с хешем запроса, прежде чем что-либо делать.
  2. Тот же ключ и тот же запрос возвращают сохранённый ответ с X-SD-Replayed: true. Ничего не выполняется дважды.
  3. Тот же ключ с другим запросом — operation_key_reused, ничего не меняется.
  4. После тайм-аута Store Deputy не отправляет запрос повторно. Он спрашивает GET /v2/operations: unknown означает, что запрос не дошёл, и его можно отправить снова с тем же ключом; completed даёт сохранённый результат; started означает, что запрос оборвался посередине.
  5. Каждое изменение корзины также несёт ожидаемую ревизию корзины (следующий раздел). Именно это делает повторную отправку started безопасной: если первая попытка применилась, ревизия изменилась, и повтор возвращает cart_conflict вместо того, чтобы добавить товар дважды.

Пока результат неизвестен, агент получает outcome_uncertain и указание не повторять запрос.

Покупатели и корзина (этап B) ​

  1. Виджет витрины обращается к модулю на домене самого магазина. Модуль определяет посетителя по настоящей сессии OpenCart (гость или вошедший покупатель), никогда — по customer_id, присланному браузером.
  2. Модуль создаёт случайный binding_ref, хранит у себя его связь с сессией OpenCart и вызывает POST /shopper-bindings в Store Deputy, подписанный секретом подключения. Отправляет только: гость или покупатель, псевдоним покупателя (выведенный ключом, который остаётся в магазине), магазин, язык, валюта и срок действия. Без cookie, ID сессии, email или имени.
  3. Store Deputy проверяет подписку на Frontend Agent, создаёт привязку и грант разговора и возвращает браузеру токен чата, действующий не дольше 15 минут и только в чат-шлюзе.
  4. Вызов инструмента корзины разрешает грант до привязки, и Store Deputy обращается к модулю с этим binding_ref. Модуль ещё раз проверяет, что привязка активна и всё ещё соответствует состоянию входа посетителя, и работает с настоящей корзиной посетителя через собственный код корзины OpenCart.
  5. Вход, выход или слияние гостевой корзины с корзиной покупателя создают новую привязку с previous_binding_ref; старая отзывается тем же шагом. Разговор может продолжаться, но никогда с полномочиями предыдущего покупателя. Устаревшая привязка даёт shopper_session_changed.

Ревизия корзины — непрозрачное значение, которое меняется каждый раз, когда меняется что-либо, влияющее на строки или итоги корзины, в том числе правки в другой вкладке. Каждое изменение должно назвать ожидаемую ревизию; расхождение возвращает cart_conflict с текущей корзиной, а не тихую перезапись. Цены, опции, остатки, скидки, налоги и итоги вычисляет OpenCart; инструменты не принимают цену. Оформление, оплата и заказы — вне рамок.

Ошибки ​

Модуль отвечает на отказ настоящим HTTP-статусом и { "error": { "code", "message", "retryable", "details" } }. Сообщения никогда не содержат секретов, SQL, путей к файлам или персональных данных. Полная таблица с тем, что видит агент для каждого кода, — contracts/errors.yaml. Те, что определяют поведение:

КодHTTPЧто видит агент
signature_invalid, timestamp_skew, unknown_key401store_refused; оператор получает оповещение
replayed401Ничего; Store Deputy один раз повторяет с новым nonce
not_paired401store_disconnected
paused423store_paused
capability_unavailable501capability_unavailable
operation_key_reused409internal: ошибка на нашей стороне
operation_in_progress409outcome_uncertain
binding_stale409shopper_session_changed
binding_expired, binding_revoked410shopper_session_ended
cart_conflict409cart_conflict с текущей ревизией
Нет действительной подписи, тайм-аут на чтении—store_unreachable
Тайм-аут на изменении—outcome_uncertain; результат выясняют, прежде чем что-то повторить

Что зафиксировано здесь и что ещё согласовать ​

Этот черновик предлагает: REST-пути с логическим путём в параметре запроса; подписи SD2, привязанные к методу, пути, query и nonce запроса; обязательное хранилище nonce; замену ключа с 10-минутным перекрытием; ключи операций для намерения с проверкой в журнале; ревизии корзины на каждом изменении; имена и аргументы инструментов и правило, что ни один из них не выбирает идентичность; заголовки от Agentfy и модель грантов.

Нужно согласовать до реализации:

  • Artem Belikov (модуль): транспорт ?path= на реальных хостингах; таблицы nonce и операций на MyISAM; можно ли загрузить корзину OpenCart для сохранённой сессии без cookie посетителя (решение 002 требует доказать это на 3.0.3.5 и 3.0.5.1); как вычисляется ревизия корзины; PHP-проверку векторов подписей, которую не запускали, потому что там, где писался черновик, не было PHP и Docker.
  • Команда Agentfy: каждая строка раздела чего Agentfy ещё не делает, прежде всего исключение из правила «никакого контекста MCP-серверам».
  • Основатель: кто открывает разговор в app и, значит, передаёт грант Agentfy (предложение — Store Deputy); какие чтения разрешены актору agent без человека; приемлем ли псевдоним покупателя с учётом правил приватности, нужных для пилота.
  • Позже: OAuth 2.1 client credentials вместо статического ключа агента, если Agentfy это добавит; поиск товаров на витрине для Frontend Agent, который пока не покрывает ни одна задача.

Связанное: решение 002, протокол моста v1, полномочия и восстановление.