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