Протокол модуля ↔ API
Границы и статус
SD Module REST 1.0 · Актуальный контракт для разработки · 1 октября 2026. Это первая версия REST-протокола модуля/API. Предыдущие документы Bridge RPC были исследовательскими черновиками и не накладывают требований совместимости или миграции. Описанные эндпоинты ещё нужно реализовать; открытые технические детали перечислены ниже.
Модуль — адаптер платформы, без LLM, harness или MCP-сервера. Agentfy обращается к MCP Store Deputy; Store Deputy проверяет полномочия, преобразует инструменты в запросы этого протокола и вызывает модуль. Модуль проверяет подключение и локальные права, применяет правила платформы и возвращает доказательства результата. Магазин остаётся источником истины.
Harness запускает агента. Модель предлагает бизнес-аргументы, а не ключи или права.
catalog.setBasePriceКвитанция возвращается в чат. Данные принадлежат магазину.
Жизненный цикл подключения
Авторизованный администратор создаёт одноразовый challenge. Браузер передаёт ссылку и CSRF state, не секреты магазина.
pendingПривязка и создание агента — разные этапы. Повтор создания не должен давать дубликат.
- Администратор с правом OpenCart
modifyзапускает Connect. Модуль создаёт одноразовый challenge со сроком действия; браузер передаёт только ссылку на привязку и CSRF state. - Вход/регистрация на сайте определяет команду и явно подтверждает магазин. Сервис проверяет членство, доказательство владения, точный endpoint магазина и разрешённый URL возврата. Секреты не передаются в query-параметрах редиректа.
POST /pairings— ресурс первоначального подключения на стороне модуля под REST-базой/v1. Доверенный модулю ключ бекенда аутентифицирует межсерверный обмен до появления ключей подключения. Модуль проверяет и атомарно погашает одноразовый challenge, привязывает подтверждённые connection/store и устанавливает ключи подключения. Отдельные ключи направлений выводятся через согласованный KDF; схемы bootstrap, векторы подписи/KDF и key ID нужно определить до запуска. Это исключение первоначального подключения не разрешает бизнес-операций.- API читает защищённый REST manifest, проверяет идентичность, версию и возможности, затем создаёт агента в Agentfy с центральным MCP endpoint. Успех привязки и создания агента — разные состояния. Повтор создания использует то же подключение, без дублирования агентов.
- Локальная Pause сразу блокирует бизнес-чтения и записи; подписанные manifest, статус операций и отзыв остаются доступны. Отзыв отклоняет старые ключи, отменяет отправку ожидающих команд и привязки покупателей. Уже выполненные записи не откатываются. Ротация имеет key ID и ограниченное окно перехода; отозванный ключ не принимается даже в этом окне.
Сохраняем pending → paired → ready, а также paused, revoked, incompatible, provisioningFailed. Неудачная проверка manifest не даёт ready. Ошибку создания агента можно исправить без повторной привязки.
Транспорт и аутентификация
Вне изолированных локальных fixtures обязателен HTTPS с проверкой сертификата. JSON — UTF-8, без сжатия в этом профиле; ответы имеют Cache-Control: no-store. Логические маршруты ниже относительны к зарегистрированному базовому URL модуля, например https://shop.example/storedeputy.php/v1. PATH_INFO/front-controller нужно проверить на хостингах; перехода к неограниченному admin router OpenCart нет. Редиректы отклоняются. Проверка DNS и адресов должна предотвращать SSRF, включая private/loopback/link-local, rebinding и перенаправления.
Предлагаемый профиль: HTTP Message Signatures RFC 9421, hmac-sha256, отдельные ключи направлений для каждого подключения; Content-Digest RFC 9530 с SHA-256 точных переданных байтов, включая пустое тело. Подпись охватывает @method, @target-uri, content-digest, x-sd-protocol, x-sd-connection, x-sd-store, x-request-id; для изменений также idempotency-key. Обязательные параметры keyid, created, expires, nonce; key ID определяет разрешённый алгоритм и направление, а не произвольный параметр отправителя.
Предлагаемые границы: expiry не позже 60 секунд после created, погрешность часов 30 секунд; подписи вне окна отклоняются. Nonce атомарно резервируется для ключа/направления до expiry + погрешность. Повтор имеет новую подпись/nonce/request ID, но тот же ключ операции и payload. Повторные заголовки, дубли query-параметров и неизвестные кодировки отклоняются. Proxy сохраняет зарегистрированный публичный URI; доверенные proxy настраиваются явно.
Ответы тоже подписаны: статус, digest, connection, store, request ID и исходные method/target через request-bound компоненты RFC 9421. До доверия квитанции проверяем корреляцию, срок, digest, подпись и JSON schema. Неверная/отсутствующая подпись или HTML шлюза означает неизвестный результат, не успех записи. Общие эталонные векторы PHP/Node — условие выпуска; существующий экспериментальный код этих правил не реализует.
Обнаружение и возможности
GET /manifest требует аутентификации. Возвращает connection/store, версии протокола/модуля/платформы, включённые возможности, лимиты, Pause, семантику валюты/цен и предупреждения совместимости. Пример описывает будущий модуль, не текущий manifest. Объявленная возможность — реализованное и проверенное обязательство, не roadmap.
Доступ — пересечение возможности модуля, локальных разрешений владельца, роли пользователя/команды, scope агента и тарифа. API может ограничить права, но не включить отсутствующую возможность. Manifest проверяется после обновления/переподключения; каждая запись всё равно проверяет актуальные локальные права. Данные OpenCart multi-store могут быть общими: базовая цена влияет на несколько витрин. Модуль возвращает все затронутые магазины и отклоняет запись без полномочий и согласования для всех.
{
"data": {
"protocol": "1.0", "connectionId": "conn_demo", "storeId": "0",
"moduleVersion": "1.0.0", "platform": { "name": "opencart", "version": "3.0.3.5" },
"paused": false,
"capabilities": ["catalog.products.read@1", "catalog.basePrice.write@1", "operations.read@1"],
"limits": { "maxPageSize": 100, "maxBodyBytes": 262144, "maxExecutionMs": 10000 },
"price": { "currency": "USD", "scale": 4, "taxIncluded": false, "scope": "sharedProduct" },
"warnings": []
},
"meta": { "requestId": "req_demo", "protocol": "1.0" }
}Ресурсы и соответствие MCP
Маршруты относительны к базе /v1 модуля. Магазин задаёт подписанный X-SD-Store, ограниченный подключением. GET не меняет бизнес-данные. Нет универсального исполнения SQL, PHP, admin-route или произвольных HTTP-запросов.
Контракт чтения этапа A: cursor (непрозрачный, необязательный), limit (1–100, по умолчанию 50), sku (необязательный точный фильтр), language (объявленная локаль). Стабильная сортировка по product ID; ответ {data: Product[], page:{nextCursor:null|string}, meta}. Cursor привязан к connection/store/filter, но не гарантирует snapshot. ID — непрозрачные строки; деньги — десятичные строки объявленной точности; валюта — ISO. Товар имеет id, sku, name, basePrice, currency, revision, affectedStoreIds и предупреждения о specials/options. SKU может дать несколько товаров: нельзя молча выбрать первый. Отсутствующий товар — 404. Сильная ревизия базовой цены учитывает внешние изменения администратора, а не только модуль; без доказательства этого записи выключены.
Создание товаров, остатки и ответы на отзывы — расширения. До публикации нужны собственные схемы, права, конкурентность и проверка результата. Первая реализация не означает доступность всех действий админки.
| MCP tool / purpose | REST resource | Capability / stage |
|---|---|---|
| Adapter discovery | GET /manifest | Required / A |
catalog.listProducts | GET /products | catalog.products.read@1 / A |
catalog.getProduct | GET /products/{id} | catalog.products.read@1 / A |
catalog.getBasePrice | GET /products/{id}/base-price | catalog.products.read@1 / A |
Approved catalog.setBasePrice | POST /operations (kind=product.basePrice.set) | catalog.basePrice.write@1 / controlled write |
operation.get | GET /operations/{operationId} | operations.read@1 / A |
cart.get | GET /shopper-bindings/{bindingId}/cart | cart.read@1 / B |
Authorized cart.setItemQuantity | POST /operations (kind=cart.item.quantity.set) | cart.write@1 / B, schema pending |
GET /products/{id}/base-price → 200, ETag: "price-r17":
{"data":{"productId":"42","amount":"19.9900","currency":"USD","revision":"price-r17","affectedStoreIds":["0"]},"meta":{"requestId":"req_demo","protocol":"1.0"}}Контролируемые записи
Владелец проверяет товар 42, ревизию, затронутые магазины и новую базовую цену. Другое изменение требует нового согласования.
19.9900 → 21.5000 USDОдно согласованное намерение → один operation ID, сохранённый при повторах.
API определяет connectionId, магазин, пользователя, run и согласование из аутентифицированного состояния вне prompt. Модель передаёт бизнес-аргументы, а не ключи, роль, личность покупателя или флаг согласования. Для защищённой записи владелец видит точные ID, значения до/после, затронутые магазины и предупреждения; неизменяемое согласование привязывается к действию и ревизии. Изменение данных требует нового согласования. Членство, тариф, отзыв и срок согласования проверяются ещё раз перед отправкой.
POST /operations создаёт одну операцию для одного ресурса, не скрытую batch-транзакцию. API создаёт UUID один раз для согласованного намерения и использует его как Idempotency-Key; operationId в теле должен совпадать. Подписанное тело содержит доверенного пользователя и решение согласования; модуль доверяет API аутентификацию сотрудника, но сам проверяет connection/store, локальные права/Pause, точный хеш действия и срок согласования. Это не защищает от скомпрометированного доверенного API; локальные права и Pause ограничивают это доверие.
actionHash — SHA-256 канонического JSON RFC 8785 для {connectionId, storeId, kind, target, expected, set, affectedStoreIds}. Ревизия и затронутые магазины тоже связаны. Модуль вычисляет хеш и сравнивает с grant согласования; более широкие wildcard-права недопустимы. Неизвестные поля изменения, некорректные суммы, валюта, просроченное согласование, несоответствие scope и изменения без эффекта отклоняются до записи. expected.revision — бизнес-предусловие цены, не If-Match коллекции операций; несовпадение даёт 409 REVISION_CONFLICT без записи.
Исполнение: атомарная запись в журнал → проверка ресурса/предусловия → условная запись с сохранением остальных полей → чтение назад → обязательная очистка кеша → квитанция. Hooks и внешние эффекты имеют отдельный статус; успех БД не доказывает их завершения. Не перезаписываем полную форму товара или specials/discounts/options. Автоотката нет: возврат — новая согласованная операция на текущей ревизии.
POST /operations · Idempotency-Key: 4faf6b8a-887e-4c97-93b0-a5476ae95a75
{
"operationId": "4faf6b8a-887e-4c97-93b0-a5476ae95a75",
"connectionId": "conn_demo", "storeId": "0",
"kind": "product.basePrice.set", "target": { "productId": "42" },
"expected": { "revision": "price-r17", "amount": "19.9900" },
"set": { "amount": "21.5000", "currency": "USD" },
"affectedStoreIds": ["0"],
"context": { "actorId": "user_demo", "actorKind": "employee", "runId": "run_demo" },
"approval": { "id": "apr_demo", "actionHash": "<sha256-hex>", "expiresAt": "2026-10-01T12:05:00Z" }
}201 Created · Location: https://shop.example/storedeputy.php/v1/operations/4faf6b8a-887e-4c97-93b0-a5476ae95a75
{
"data": {
"operationId": "4faf6b8a-887e-4c97-93b0-a5476ae95a75",
"state": "succeeded", "actionHash": "<sha256-hex>",
"target": { "productId": "42" },
"before": { "amount": "19.9900", "revision": "price-r17" },
"after": { "amount": "21.5000", "currency": "USD", "revision": "price-r18" },
"verifiedAt": "2026-10-01T12:00:01Z",
"effects": { "cache": "completed", "hooks": "notRequired" }, "warnings": []
},
"meta": { "requestId": "req_demo", "protocol": "1.0" }
}Повторы, конфликты и неизвестный результат
GET /operations/{id}Прочитайте журнал перед решениемЗавершённая запись доказывает успех, конфликт или отказ. Возвращаем настоящий результат без повторного исполнения.
succeeded / conflict / failedНе создавайте новый ключ и не повторяйте вслепую. Совпадение цены не доказывает нашу запись.
Журнал имеет атомарный уникальный ключ (connectionId, storeId, operationId) и неизменяемый хеш полного смыслового тела операции. Тот же ключ/тело возвращает сохранённый результат с новой транспортной подписью; другое тело — 409 IDEMPOTENCY_MISMATCH. Повтор активной операции даёт 202 с тем же URL и Retry-After, не запускает другого исполнителя. Повтор завершённой — 200. Даже кешированный результат требует действующих прав подключения; просроченное согласование не начинает и не возобновляет новые эффекты.
GET /operations/{id} возвращает {data: Operation, meta}, где state: pending, running, succeeded, conflict, failed, unknown. Успех содержит квитанцию; conflict/failed — стабильный code и доказательство отсутствия эффекта; unknown — известные частичные эффекты и причину сверки. Pause разрешает авторизованное чтение статуса. Отсутствующая запись — 404 OPERATION_NOT_FOUND, но это не доказательство отсутствия исторического эффекта.
После таймаута, 5xx шлюза, падения процесса или отсутствующей подписи сначала читаем статус. Без нового ключа и слепого повтора. Tombstone запрещает повторное исполнение после архивирования квитанции. ID/хеш операции храним бессрочно в REST 1.0; персональное содержимое очищаем/архивируем отдельно. Потеря журнала или восстановление backup нарушает полноту: записи останавливаются до сверки. Повтор неизвестного ключа конкурирует с исходным исполнителем через тот же атомарный claim.
currentPrice == requestedPrice не доказывает наше исполнение: администратор мог изменить цену так же. Устаревший running не разрешает другого писателя. Сверяем устойчивые доказательства операции; иначе оставляем unknown и требуется расследование. Не обещаем exactly-once между ценой, журналом и hooks без доказательства crash boundary. Для нетранзакционных таблиц — только чтение, пока не доказано безопасное восстановление. Несколько товаров — отдельные операции с собственными результатами, не атомарный batch.
Расширение покупателя и корзины
Widget вызывает модуль на origin магазина. Модуль определяет гостя или покупателя; сырые cookies остаются в магазине.
same-origin bootstrapВход, выход или слияние корзины требуют обновления или отзыва привязки. Расширение этапа B.
Этап B имеет отдельный профиль; учётных данных сотрудника недостаточно. Widget обращается к same-origin endpoint модуля с защитой от CSRF. Модуль определяет личность из действующей сессии витрины и передаёт аутентифицированное подтверждение Store Deputy. API возвращает непрозрачную привязку и краткосрочный доступ к конкретному разговору. Сырые cookies сессии и админ-ключи не передаются widget или Agentfy.
Серверная привязка ограничена connection, store, поколением сессии, покупателем/гостем, разговором, действиями корзины и сроком. API и модуль проверяют её каждый раз. Модель не выбирает binding или customer ID. Login/logout, expiry и слияние гостевой корзины требуют отзыва/перепривязки; изменение в другой вкладке делает ревизию неактуальной. Модуль использует настоящую корзину OpenCart и расчёты платформы, не отдельную API-корзину.
Лучше желаемое количество (установить 3), чем добавление при каждом повторе. Изменение корзины требует ключа операции, ожидаемой ревизии и явного намерения покупателя относительно точных товаров/опций; админ-согласование не заменяет владение корзиной. Возвращаются проверка позиций, подтверждённая ревизия и пересчитанные totals для обновления витрины. Схемы bootstrap/refresh, hooks смены сессии и квитанции нужно зафиксировать на этапе B до включения cart.*. Checkout, платежи и создание заказов исключены.
Ошибки и лимиты
Ошибки имеют application/problem+json по RFC 9457 и стабильные расширения code, requestId, operationId, если известен. Клиент использует status/code, не переведённый текст. Без SQL, stack traces, секретов или сессионных ID. Размер проверяется до разбора; неизвестные поля изменения и неправильные типы отклоняются. API и модуль ограничивают частоту на connection; manifest-лимиты — технические, не тарифные. Учитываем Retry-After; чтения повторяем с ограниченной задержкой, записи сначала сверяем. 202 — принято/исполняется, не завершено. 200 с неверной схемой не является успехом.
| HTTP | Stable code | Meaning / next step |
|---|---|---|
| 400 | INVALID_REQUEST | Malformed transport/JSON; fix request |
| 401 | AUTHENTICATION_FAILED | Invalid/expired/replayed signature; no effect |
| 403 | SCOPE_DENIED, CONNECTION_REVOKED, STORE_PAUSED, APPROVAL_REQUIRED | No new effect; restore authority explicitly |
| 404 | RESOURCE_NOT_FOUND, OPERATION_NOT_FOUND | Check identity; do not infer write outcome |
| 409 | REVISION_CONFLICT, IDEMPOTENCY_MISMATCH | No new write; inspect conflict |
| 413 | BODY_TOO_LARGE | Reduce request size |
| 422 | VALIDATION_FAILED, CAPABILITY_UNAVAILABLE | Fix data or disable tool |
| 429 | RATE_LIMITED | Respect Retry-After |
| 503 | TEMPORARILY_UNAVAILABLE | Read back operation before write retry |
{"type":"about:blank","title":"Conflict","status":409,"detail":"The approved price revision is no longer current.","code":"REVISION_CONFLICT","requestId":"req_demo","operationId":"4faf6b8a-887e-4c97-93b0-a5476ae95a75"}Совместимость и проверка реализации
До параллельной реализации обмена зафиксировать OpenAPI + JSON Schemas и общие fixtures в STOR-17. Страница определяет целевую семантику, не готовый SDK или набор тестов. Новые необязательные поля ответа добавляются совместимо; неизвестные возможности выключены. Неизвестное состояние операции — неразрешённое, не успех. Несовместимая семантика требует нового major; REST 1.0 не имеет режима совместимости со старым RPC.
Условия выпуска:
- Векторы подписи и action hash для PHP/Node; изменение method/path/query/body/status нарушает проверку; гонки nonce, время, ротация и отзыв.
- Fixtures OpenCart 3.0.3.5 и 3.0.5.1, переименованная admin-папка и маршрутизация хостингов; непроверенные версии выключены. OpenCart 4 — отдельный адаптер и матрица.
- Запрет чужой команды/магазина, общие товары, Pause, просроченное согласование, подставленная в prompt личность и вредоносный текст каталога.
- Пагинация/дубли SKU, точные суммы, конфликт ручной правки, включая изменение и возврат (ABA); сохранение связей товара и предупреждений о цене витрины.
- Конкурентные повторы имеют один claim; другой payload с тем же ключом отклоняется; остановка процесса до/после записи, read-back, кеша и квитанции. Доказательство восстановления unknown вместо выдуманного успеха.
- Логи связывают connection/run/request/operation без секретов; аудит API хранит согласование, хеш и квитанцию. События модуля не являются отдельным счётчиком биллинга Agentfy.
- Этап B отдельно доказывает принадлежность корзины гостю/покупателю, logout/merge/revocation, вкладки, повтор изменения количества и обновление витрины.
Ответственные: Artem Belikov — модуль/PHP; yevhen.kariakin — API, MCP-адаптер и общий контракт. Обе стороны используют одинаковые fixtures. До фиксации остаются точные signing/KDF vectors, публичная маршрутизация, безопасные ревизии/восстановление для нетранзакционных магазинов, политика hooks и схемы этапа B. Записи не объявлять до прохождения этих проверок.
Источники и связанные работы
- ADR 002.
- STOR-16 · STOR-17.
- RFC 9421 — HTTP Message Signatures, RFC 9530 — Content-Digest: signing and digest foundations; the profile and timing limits above are Store Deputy proposals.
- RFC 9457 — Problem Details, RFC 9110 — HTTP semantics: error shape and HTTP behavior; application codes/states are our contract.
- RFC 8785 — JSON Canonicalization Scheme: canonical JSON for action hashes; shared PHP/Node fixtures remain required.