Перейти до вмісту

Контракт шлюзу 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, повноваження й відновлення.