diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01f209c..239354c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: if: steps.base-file.outputs.exists == 'true' && steps.compatibility.outputs.breaking != 'No breaking changes' env: BREAKING_APPROVED: ${{ contains(github.event.pull_request.labels.*.name, 'breaking-change-approved') }} - run: node scripts/check-breaking-policy.mjs "origin/${{ github.base_ref }}" + run: node scripts/check-breaking-policy.mjs "origin/${{ github.base_ref }}" "${{ matrix.contract }}" contracts: uses: portable-agent/.github/.github/workflows/reusable-node.yml@ed34f26071893063cf70bcd5c784d15d6c4fb5b7 with: diff --git a/README.md b/README.md index c527a72..9147f4c 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ OpenAPI в pull request с `main` и блокирует нарушение эт ## Версии и release -Текущая версия — `2.7.0`. Переход с `1.x` описан в -[migration guide](docs/migrations/2.0.0.md). +Текущая версия — `3.0.0`. Переход на Google Calendar описан в +[migration guide](docs/migrations/3.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 6f7da8f..393d956 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: 2.7.0 + version: 3.0.0 description: События жизненного цикла безопасных действий. defaultContentType: application/json servers: diff --git a/docs/calendar-event.md b/docs/calendar-event.md index a9a2dd7..faa59d8 100644 --- a/docs/calendar-event.md +++ b/docs/calendar-event.md @@ -1,7 +1,8 @@ # Создание встречи -Первое разрешённое действие MVP — `calendar.create_event`. Пока его выполняет `fake-calendar`. -Настоящий провайдер позже использует тот же payload. +Первое разрешённое действие MVP — `calendar.create_event`. Для детерминированных тестов его выполняет +`fake-calendar`, а для настоящего календаря — `google-calendar`. Оба connector используют один payload, +поэтому выбор провайдера не меняет форму действия или виджета подтверждения. ## Поля diff --git a/docs/migrations/3.0.0.md b/docs/migrations/3.0.0.md new file mode 100644 index 0000000..019f0c2 --- /dev/null +++ b/docs/migrations/3.0.0.md @@ -0,0 +1,15 @@ +# Переход на contracts 3.0.0 + +Версия `3.0.0` добавляет connector `google-calendar` в ответы Agent Runtime и Channel Gateway. +Это major-изменение: клиент, который считал `fake-calendar` единственным возможным значением, должен +научиться обрабатывать новый connector до обновления контракта. + +## Что изменить + +1. Обновить bundle контрактов до `3.0.0`. +2. Добавить обработку `google-calendar` рядом с `fake-calendar`. +3. Не выбирать provider по сетевому адресу. Использовать только имя connector из контракта. +4. Если приложение пока не поддерживает Google Calendar, показывать понятное сообщение вместо падения. + +Формат payload встречи не изменился. `fake-calendar` остаётся доступным для локальной разработки и +детерминированных end-to-end тестов. diff --git a/openapi/action-api.yaml b/openapi/action-api.yaml index 114c248..6ff7f1f 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: 2.7.0 + version: 3.0.0 description: Создание, просмотр и подтверждение безопасных действий агента. license: name: Apache-2.0 @@ -103,7 +103,7 @@ components: enum: [calendar.create_event] connector: type: string - enum: [fake-calendar] + enum: [fake-calendar, google-calendar] payload: $ref: "#/components/schemas/CalendarCreateEventPayload" requestKey: diff --git a/openapi/agent-runtime-api.yaml b/openapi/agent-runtime-api.yaml index d4c5a4f..44619f0 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: 2.7.0 + version: 3.0.0 description: Подготовка предложения действия из текста пользователя. license: name: Apache-2.0 @@ -77,7 +77,7 @@ components: uniqueItems: true items: type: string - enum: [fake-calendar] + enum: [fake-calendar, google-calendar] default: [] ProposalResponse: type: object @@ -112,7 +112,7 @@ components: enum: [calendar.create_event] connector: type: string - enum: [fake-calendar] + enum: [fake-calendar, google-calendar] payload: $ref: "#/components/schemas/CalendarCreateEventPayload" explanation: diff --git a/openapi/channel-gateway-api.yaml b/openapi/channel-gateway-api.yaml index c213950..10786c0 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: 2.7.0 + version: 3.0.0 description: Единый текстовый вход для независимых каналов пользователя. license: name: Apache-2.0 diff --git a/openapi/connection-api.yaml b/openapi/connection-api.yaml index 7100469..103c75d 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: 2.7.0 + version: 3.0.0 description: Подключения внешних аккаунтов. Публичные операции используют владельца из JWT. license: name: Apache-2.0 diff --git a/openapi/conversation-api.yaml b/openapi/conversation-api.yaml index 8abf043..0b2c53e 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: 2.7.0 + version: 3.0.0 description: Внутренний API состояния диалога и оркестрации предложения действия. license: name: Apache-2.0 diff --git a/openapi/mcp-gateway-api.yaml b/openapi/mcp-gateway-api.yaml index 527d437..93e1071 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: 2.7.0 + version: 3.0.0 description: Безопасный внутренний вызов разрешённых tools в настроенных MCP-сервисах. license: name: Apache-2.0 diff --git a/package.json b/package.json index ac7688c..3709c85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@portable-agent/contracts", - "version": "2.7.0", + "version": "3.0.0", "private": true, "files": [ "openapi", diff --git a/scripts/check-breaking-policy.mjs b/scripts/check-breaking-policy.mjs index 0673922..3b619ce 100644 --- a/scripts/check-breaking-policy.mjs +++ b/scripts/check-breaking-policy.mjs @@ -42,22 +42,24 @@ export const checkBreakingPolicy = ({ } }; -export const run = (baseRef, approved) => { - const base = spawnSync( - "git", - ["show", `${baseRef}:openapi/action-api.yaml`], - { - encoding: "utf8", - }, - ); +export const contractPath = (contractName) => { + if (!/^[a-z0-9-]+$/.test(contractName)) { + throw new Error("Некорректное имя OpenAPI-контракта."); + } + return `openapi/${contractName}.yaml`; +}; + +export const run = (baseRef, approved, contractName = "action-api") => { + const path = contractPath(contractName); + const base = spawnSync("git", ["show", `${baseRef}:${path}`], { + encoding: "utf8", + }); if (base.status !== 0) { throw new Error(base.stderr || "Не удалось прочитать базовый OpenAPI."); } const baseVersion = readVersion(base.stdout); - const nextVersion = readVersion( - readFileSync("openapi/action-api.yaml", "utf8"), - ); + const nextVersion = readVersion(readFileSync(path, "utf8")); checkBreakingPolicy({ baseVersion, nextVersion, @@ -67,5 +69,9 @@ export const run = (baseRef, approved) => { }; if (process.argv[1]?.endsWith("check-breaking-policy.mjs")) { - run(process.argv[2], process.env.BREAKING_APPROVED === "true"); + run( + process.argv[2], + process.env.BREAKING_APPROVED === "true", + process.argv[3], + ); } diff --git a/test/schemas.test.mjs b/test/schemas.test.mjs index 445ee7c..00156f3 100644 --- a/test/schemas.test.mjs +++ b/test/schemas.test.mjs @@ -6,6 +6,7 @@ import addFormats from "ajv-formats"; import YAML from "yaml"; import { checkBreakingPolicy, + contractPath, readVersion, } from "../scripts/check-breaking-policy.mjs"; @@ -13,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), "2.7.0"); + assert.equal(readVersion(source), "3.0.0"); }); test("compatibility workflow skips policy when oasdiff finds no breaking changes", async () => { @@ -25,6 +26,23 @@ test("compatibility workflow skips policy when oasdiff finds no breaking changes ); }); +test("compatibility workflow checks the contract from the matrix", async () => { + const workflow = await readFile(".github/workflows/ci.yml", "utf8"); + + assert.match( + workflow, + /check-breaking-policy\.mjs "origin\/\$\{\{ github\.base_ref \}\}" "\$\{\{ matrix\.contract \}\}"/, + ); +}); + +test("breaking policy accepts only a safe contract name", () => { + assert.equal( + contractPath("agent-runtime-api"), + "openapi/agent-runtime-api.yaml", + ); + assert.throws(() => contractPath("../package"), /Некорректное имя/); +}); + test("breaking change needs approval, new major and migration guide", () => { assert.throws( () => @@ -132,7 +150,10 @@ test("action request uses the service request key", async () => { assert.equal(request.properties.requestKey.type, "string"); assert.equal(request.properties.idempotencyKey, undefined); assert.deepEqual(request.properties.kind.enum, ["calendar.create_event"]); - assert.deepEqual(request.properties.connector.enum, ["fake-calendar"]); + assert.deepEqual(request.properties.connector.enum, [ + "fake-calendar", + "google-calendar", + ]); assert.equal( request.properties.payload.$ref, "#/components/schemas/CalendarCreateEventPayload", @@ -140,6 +161,20 @@ test("action request uses the service request key", async () => { assert.ok(api.paths["/api/v1/actions"].post.responses["409"]); }); +test("agent can choose fake or Google calendar", async () => { + const source = await readFile("openapi/agent-runtime-api.yaml", "utf8"); + const api = YAML.parse(source); + const requestConnectors = + api.components.schemas.UserContext.properties.availableConnectors.items; + const planConnector = api.components.schemas.ActionPlan.properties.connector; + + assert.deepEqual(requestConnectors.enum, [ + "fake-calendar", + "google-calendar", + ]); + assert.deepEqual(planConnector.enum, ["fake-calendar", "google-calendar"]); +}); + test("action API uses the shared calendar payload shape", async () => { const source = await readFile("openapi/action-api.yaml", "utf8"); const api = YAML.parse(source);