Контракт шлюза 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.yaml | REST 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-статус и что видит агент |
Три границы полномочий
Runtime Agentfy ──(1) ключ агента + (2) грант разговора──▶ MCP Store Deputy
│ права, аудит
│
(3) подпись подключения, никогда (1) или (2) ┘
▼
REST API модуля OpenCart| Граница | Доказательство | Кто выдаёт, кто хранит | Действует только в | Никогда |
|---|---|---|---|---|
| Агент → Store Deputy | Authorization: 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-Id | Runtime Agentfy | Совпадает с агентом, которому выдан ключ |
X-SD-Context: sdcg_… | Runtime Agentfy, для каждого разговора | Существует, не просрочен и не отозван, выдан этому агенту и этому треду |
X-Agentfy-Thread-Id | Runtime Agentfy | Совпадает с тредом, для которого выдан грант |
X-Agentfy-Turn-Id | Runtime Agentfy | Только для аудита |
X-Agentfy-Tool-Call-Id | Runtime Agentfy | Становится стабильным ключом операции для изменения |
X-Agentfy-Initiated-By | Runtime Agentfy | human, 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:
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}/cart | B | нет |
POST /v2/shopper-bindings/{ref}/cart/items | B | нет |
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 его не передают.
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 вызова инструмента от модели, поэтому повтор того же вызова использует тот же ключ, а новый запрос человека получает новый.
- Модуль записывает ключ вместе с хешем запроса, прежде чем что-либо делать.
- Тот же ключ и тот же запрос возвращают сохранённый ответ с
X-SD-Replayed: true. Ничего не выполняется дважды. - Тот же ключ с другим запросом —
operation_key_reused, ничего не меняется. - После тайм-аута Store Deputy не отправляет запрос повторно. Он спрашивает
GET /v2/operations:unknownозначает, что запрос не дошёл, и его можно отправить снова с тем же ключом;completedдаёт сохранённый результат;startedозначает, что запрос оборвался посередине. - Каждое изменение корзины также несёт ожидаемую ревизию корзины (следующий раздел). Именно это делает повторную отправку
startedбезопасной: если первая попытка применилась, ревизия изменилась, и повтор возвращаетcart_conflictвместо того, чтобы добавить товар дважды.
Пока результат неизвестен, агент получает outcome_uncertain и указание не повторять запрос.
Покупатели и корзина (этап B)
- Виджет витрины обращается к модулю на домене самого магазина. Модуль определяет посетителя по настоящей сессии OpenCart (гость или вошедший покупатель), никогда — по
customer_id, присланному браузером. - Модуль создаёт случайный
binding_ref, хранит у себя его связь с сессией OpenCart и вызываетPOST /shopper-bindingsв Store Deputy, подписанный секретом подключения. Отправляет только: гость или покупатель, псевдоним покупателя (выведенный ключом, который остаётся в магазине), магазин, язык, валюта и срок действия. Без cookie, ID сессии, email или имени. - Store Deputy проверяет подписку на Frontend Agent, создаёт привязку и грант разговора и возвращает браузеру токен чата, действующий не дольше 15 минут и только в чат-шлюзе.
- Вызов инструмента корзины разрешает грант до привязки, и Store Deputy обращается к модулю с этим
binding_ref. Модуль ещё раз проверяет, что привязка активна и всё ещё соответствует состоянию входа посетителя, и работает с настоящей корзиной посетителя через собственный код корзины OpenCart. - Вход, выход или слияние гостевой корзины с корзиной покупателя создают новую привязку с
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_key | 401 | store_refused; оператор получает оповещение |
replayed | 401 | Ничего; Store Deputy один раз повторяет с новым nonce |
not_paired | 401 | store_disconnected |
paused | 423 | store_paused |
capability_unavailable | 501 | capability_unavailable |
operation_key_reused | 409 | internal: ошибка на нашей стороне |
operation_in_progress | 409 | outcome_uncertain |
binding_stale | 409 | shopper_session_changed |
binding_expired, binding_revoked | 410 | shopper_session_ended |
cart_conflict | 409 | cart_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, полномочия и восстановление.