diff --git a/README.md b/README.md index 9147f4c..43d5954 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ OpenAPI в pull request с `main` и блокирует нарушение эт ## Версии и release -Текущая версия — `3.0.0`. Переход на Google Calendar описан в -[migration guide](docs/migrations/3.0.0.md). +Текущая версия — `4.0.0`. Новый тип ответа подключения описан в +[migration guide](docs/migrations/4.0.0.md). Версия в `package.json`, OpenAPI и AsyncAPI должна совпадать. Тег `vX.Y.Z` запускает release workflow. Он проверяет репозиторий и прикладывает к GitHub Release bundle `portable-agent-contracts-X.Y.Z.tgz`. diff --git a/asyncapi/action-events.yaml b/asyncapi/action-events.yaml index 393d956..8cb26ed 100644 --- a/asyncapi/action-events.yaml +++ b/asyncapi/action-events.yaml @@ -1,7 +1,7 @@ asyncapi: 3.0.0 info: title: Portable Agent Action Events - version: 3.0.0 + version: 4.0.0 description: События жизненного цикла безопасных действий. defaultContentType: application/json servers: diff --git a/docs/architecture.md b/docs/architecture.md index 47215c4..552a464 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -13,6 +13,7 @@ HTTP-клиент -> openapi/ -> HTTP-сервис - HTTP-граница описана в `openapi/`. - Формат события описан в `asyncapi/` и связанных схемах. - Состояние диалога описано в `openapi/conversation-api.yaml`. +- Общий виджет подключения описан в `schemas/connection-widget.schema.json`; OAuth-секретов в нём нет. - Пример считается верным только после проверки тестом. Внутренние Java- и Python-модели не переносятся сюда автоматически. Сначала проектируется стабильная diff --git a/docs/conversation-service.md b/docs/conversation-service.md index 7b79c2e..b526166 100644 --- a/docs/conversation-service.md +++ b/docs/conversation-service.md @@ -13,10 +13,11 @@ ## Ответ -Ответ содержит `messageId`, `conversationId` и один из двух вариантов: +Ответ содержит `messageId`, `conversationId` и один из трёх вариантов: - `text` — безопасный текстовый вопрос, если данных недостаточно; - `confirmation` — карточка уже сохранённого действия с `actionId` и `payloadHash`. +- `connection` — временная карточка подключения внешнего аккаунта. Контекст сообщения переиспользует `schemas/message-context.schema.json`, поэтому Channel Gateway и Conversation Service не образуют циклическую OpenAPI-ссылку. Карточка использует @@ -24,6 +25,11 @@ Conversation Service не образуют циклическую OpenAPI-ссы Решение пользователя отправляется в Action API вместе с тем же `payloadHash`, поэтому изменённую или устаревшую карточку нельзя подтвердить молча. +Карточка подключения использует `schemas/connection-widget.schema.json`. Поле `button.url` содержит +короткоживущую HTTPS-ссылку, полученную от Connection Service. Ссылка, OAuth `state`, коды и токены не +являются состоянием диалога и не должны сохраняться Conversation Service. При повторном запросе +сервис получает новую ссылку. После подключения MVP просит пользователя повторить исходную команду. + ## Граница ответственности Conversation Service вызывает Agent Runtime, а при полном предложении создаёт действие через Action diff --git a/docs/migrations/4.0.0.md b/docs/migrations/4.0.0.md new file mode 100644 index 0000000..e3247b1 --- /dev/null +++ b/docs/migrations/4.0.0.md @@ -0,0 +1,16 @@ +# Переход на contracts 4.0.0 + +Версия `4.0.0` добавляет третий вариант `connection` в `MessageResponse.reply` Conversation API и +Channel Gateway API. Для строгих клиентов это breaking change: исчерпывающий `switch` должен уметь +обработать новый тип. + +## Что изменить + +1. Обновить bundle контрактов до `4.0.0`. +2. Добавить обработку `reply.type == connection`. +3. Проверять `card` по `schemas/connection-widget.schema.json`. +4. Показать `card.button` как обычную HTTPS-ссылку канала. +5. Не сохранять OAuth URL, не извлекать из неё `state` и не отправлять её в логи. + +Клиенты, которые пока не умеют показывать кнопку, должны вернуть безопасный текст об обязательном +обновлении. Они не должны трактовать `connection` как подтверждение действия. diff --git a/examples/connection-widget.valid.json b/examples/connection-widget.valid.json new file mode 100644 index 0000000..3427fc3 --- /dev/null +++ b/examples/connection-widget.valid.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "widget": "connection", + "provider": "google-calendar", + "title": "Подключить Google Calendar", + "text": "Подключите календарь и повторите команду.", + "button": { + "label": "Подключить", + "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=example" + } +} diff --git a/openapi/action-api.yaml b/openapi/action-api.yaml index 6ff7f1f..e2df9cc 100644 --- a/openapi/action-api.yaml +++ b/openapi/action-api.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Portable Agent Action API - version: 3.0.0 + version: 4.0.0 description: Создание, просмотр и подтверждение безопасных действий агента. license: name: Apache-2.0 diff --git a/openapi/agent-runtime-api.yaml b/openapi/agent-runtime-api.yaml index 44619f0..3b537c1 100644 --- a/openapi/agent-runtime-api.yaml +++ b/openapi/agent-runtime-api.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Portable Agent Runtime API - version: 3.0.0 + version: 4.0.0 description: Подготовка предложения действия из текста пользователя. license: name: Apache-2.0 diff --git a/openapi/channel-gateway-api.yaml b/openapi/channel-gateway-api.yaml index 10786c0..6935a90 100644 --- a/openapi/channel-gateway-api.yaml +++ b/openapi/channel-gateway-api.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Portable Agent Channel Gateway API - version: 3.0.0 + version: 4.0.0 description: Единый текстовый вход для независимых каналов пользователя. license: name: Apache-2.0 diff --git a/openapi/connection-api.yaml b/openapi/connection-api.yaml index 103c75d..1affbd1 100644 --- a/openapi/connection-api.yaml +++ b/openapi/connection-api.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Portable Agent Connection API - version: 3.0.0 + version: 4.0.0 description: Подключения внешних аккаунтов. Публичные операции используют владельца из JWT. license: name: Apache-2.0 diff --git a/openapi/conversation-api.yaml b/openapi/conversation-api.yaml index 0b2c53e..b79a12e 100644 --- a/openapi/conversation-api.yaml +++ b/openapi/conversation-api.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Portable Agent Conversation API - version: 3.0.0 + version: 4.0.0 description: Внутренний API состояния диалога и оркестрации предложения действия. license: name: Apache-2.0 @@ -40,7 +40,7 @@ paths: schema: $ref: "#/components/schemas/Problem" "502": - description: Agent Runtime или Action Service временно недоступен + description: Agent Runtime, Action Service или Connection Service временно недоступен content: application/json: schema: @@ -89,11 +89,13 @@ components: oneOf: - $ref: "#/components/schemas/TextReply" - $ref: "#/components/schemas/ConfirmationReply" + - $ref: "#/components/schemas/ConnectionReply" discriminator: propertyName: type mapping: text: "#/components/schemas/TextReply" confirmation: "#/components/schemas/ConfirmationReply" + connection: "#/components/schemas/ConnectionReply" TextReply: type: object additionalProperties: false @@ -114,6 +116,15 @@ components: const: confirmation card: $ref: "../schemas/action-confirmation.schema.json" + ConnectionReply: + type: object + additionalProperties: false + required: [type, card] + properties: + type: + const: connection + card: + $ref: "../schemas/connection-widget.schema.json" Problem: type: object additionalProperties: true diff --git a/openapi/mcp-gateway-api.yaml b/openapi/mcp-gateway-api.yaml index 93e1071..61763ec 100644 --- a/openapi/mcp-gateway-api.yaml +++ b/openapi/mcp-gateway-api.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Portable Agent MCP Gateway API - version: 3.0.0 + version: 4.0.0 description: Безопасный внутренний вызов разрешённых tools в настроенных MCP-сервисах. license: name: Apache-2.0 diff --git a/package.json b/package.json index 3709c85..da64427 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@portable-agent/contracts", - "version": "3.0.0", + "version": "4.0.0", "private": true, "files": [ "openapi", diff --git a/schemas/connection-widget.schema.json b/schemas/connection-widget.schema.json new file mode 100644 index 0000000..b65bc7e --- /dev/null +++ b/schemas/connection-widget.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://portable-agent.dev/contracts/widgets/connection-widget.schema.json", + "title": "External account connection widget", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "widget", + "provider", + "title", + "text", + "button" + ], + "properties": { + "schemaVersion": { "const": 1 }, + "widget": { "const": "connection" }, + "provider": { "enum": ["google-calendar"] }, + "title": { "type": "string", "minLength": 1, "maxLength": 120 }, + "text": { "type": "string", "minLength": 1, "maxLength": 1000 }, + "button": { + "type": "object", + "additionalProperties": false, + "required": ["label", "url"], + "properties": { + "label": { "type": "string", "minLength": 1, "maxLength": 80 }, + "url": { + "type": "string", + "format": "uri", + "pattern": "^https://" + } + } + } + } +} diff --git a/test/conversation-service.test.mjs b/test/conversation-service.test.mjs index a383ed2..d34e219 100644 --- a/test/conversation-service.test.mjs +++ b/test/conversation-service.test.mjs @@ -51,10 +51,23 @@ test("message response has ids and one typed reply", async () => { assert.deepEqual(reply.oneOf, [ { $ref: "#/components/schemas/TextReply" }, { $ref: "#/components/schemas/ConfirmationReply" }, + { $ref: "#/components/schemas/ConnectionReply" }, ]); assert.equal(reply.discriminator.propertyName, "type"); }); +test("connection reply reuses the public widget schema", async () => { + const api = await readApi(); + const reply = api.components.schemas.ConnectionReply; + + assert.deepEqual(reply.required, ["type", "card"]); + assert.equal(reply.properties.type.const, "connection"); + assert.equal( + reply.properties.card.$ref, + "../schemas/connection-widget.schema.json", + ); +}); + test("confirmation reply reuses the public widget schema", async () => { const api = await readApi(); const reply = api.components.schemas.ConfirmationReply; diff --git a/test/schemas.test.mjs b/test/schemas.test.mjs index 00156f3..7fab3c6 100644 --- a/test/schemas.test.mjs +++ b/test/schemas.test.mjs @@ -14,7 +14,7 @@ const readJson = async (path) => JSON.parse(await readFile(path, "utf8")); test("breaking policy reads the real OpenAPI version", async () => { const source = await readFile("openapi/action-api.yaml", "utf8"); - assert.equal(readVersion(source), "3.0.0"); + assert.equal(readVersion(source), "4.0.0"); }); test("compatibility workflow skips policy when oasdiff finds no breaking changes", async () => { @@ -199,6 +199,36 @@ test("action confirmation example matches its public schema", async () => { assert.equal(valid, true, JSON.stringify(ajv.errors)); }); +test("connection widget example matches its public schema", async () => { + const ajv = new Ajv2020({ allErrors: true }); + addFormats(ajv); + const schema = await readJson("schemas/connection-widget.schema.json"); + const example = await readJson("examples/connection-widget.valid.json"); + + const valid = ajv.validate(schema, example); + + assert.equal(valid, true, JSON.stringify(ajv.errors)); +}); + +test("connection widget accepts only an HTTPS authorization URL", async () => { + const ajv = new Ajv2020({ allErrors: true }); + addFormats(ajv); + const schema = await readJson("schemas/connection-widget.schema.json"); + const example = await readJson("examples/connection-widget.valid.json"); + + assert.equal( + ajv.validate(schema, { + ...example, + button: { ...example.button, url: "http://example.com/oauth" }, + }), + false, + ); + assert.equal( + ajv.validate(schema, { ...example, state: "must-not-leak" }), + false, + ); +}); + test("confirmation widget rejects an altered payload hash", async () => { const ajv = new Ajv2020({ allErrors: true }); addFormats(ajv);