Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion asyncapi/action-events.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ HTTP-клиент -> openapi/ -> HTTP-сервис
- HTTP-граница описана в `openapi/`.
- Формат события описан в `asyncapi/` и связанных схемах.
- Состояние диалога описано в `openapi/conversation-api.yaml`.
- Общий виджет подключения описан в `schemas/connection-widget.schema.json`; OAuth-секретов в нём нет.
- Пример считается верным только после проверки тестом.

Внутренние Java- и Python-модели не переносятся сюда автоматически. Сначала проектируется стабильная
Expand Down
8 changes: 7 additions & 1 deletion docs/conversation-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,23 @@

## Ответ

Ответ содержит `messageId`, `conversationId` и один из двух вариантов:
Ответ содержит `messageId`, `conversationId` и один из трёх вариантов:

- `text` — безопасный текстовый вопрос, если данных недостаточно;
- `confirmation` — карточка уже сохранённого действия с `actionId` и `payloadHash`.
- `connection` — временная карточка подключения внешнего аккаунта.

Контекст сообщения переиспользует `schemas/message-context.schema.json`, поэтому Channel Gateway и
Conversation Service не образуют циклическую OpenAPI-ссылку. Карточка использует
`schemas/action-confirmation.schema.json`. Она не является командой выполнения.
Решение пользователя отправляется в Action API вместе с тем же `payloadHash`, поэтому изменённую или
устаревшую карточку нельзя подтвердить молча.

Карточка подключения использует `schemas/connection-widget.schema.json`. Поле `button.url` содержит
короткоживущую HTTPS-ссылку, полученную от Connection Service. Ссылка, OAuth `state`, коды и токены не
являются состоянием диалога и не должны сохраняться Conversation Service. При повторном запросе
сервис получает новую ссылку. После подключения MVP просит пользователя повторить исходную команду.

## Граница ответственности

Conversation Service вызывает Agent Runtime, а при полном предложении создаёт действие через Action
Expand Down
16 changes: 16 additions & 0 deletions docs/migrations/4.0.0.md
Original file line number Diff line number Diff line change
@@ -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` как подтверждение действия.
11 changes: 11 additions & 0 deletions examples/connection-widget.valid.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
2 changes: 1 addition & 1 deletion openapi/action-api.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion openapi/agent-runtime-api.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion openapi/channel-gateway-api.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion openapi/connection-api.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
15 changes: 13 additions & 2 deletions openapi/conversation-api.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -40,7 +40,7 @@
schema:
$ref: "#/components/schemas/Problem"
"502":
description: Agent Runtime или Action Service временно недоступен
description: Agent Runtime, Action Service или Connection Service временно недоступен
content:
application/json:
schema:
Expand Down Expand Up @@ -89,11 +89,13 @@
oneOf:
- $ref: "#/components/schemas/TextReply"
- $ref: "#/components/schemas/ConfirmationReply"
- $ref: "#/components/schemas/ConnectionReply"

Check failure on line 92 in openapi/conversation-api.yaml

View workflow job for this annotation

GitHub Actions / compatibility (channel-gateway-api)

response-property-one-of-added

in API POST /api/v1/conversations/messages added `#/components/schemas/ConnectionReply` to the `reply` response property `oneOf` list for the response status `200`

Check failure on line 92 in openapi/conversation-api.yaml

View workflow job for this annotation

GitHub Actions / compatibility (conversation-api)

response-property-one-of-added

in API POST /api/v1/messages added `#/components/schemas/ConnectionReply` to the `reply` response property `oneOf` list for the response status `200`
discriminator:
propertyName: type
mapping:
text: "#/components/schemas/TextReply"
confirmation: "#/components/schemas/ConfirmationReply"
connection: "#/components/schemas/ConnectionReply"
TextReply:
type: object
additionalProperties: false
Expand All @@ -114,6 +116,15 @@
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
Expand Down
2 changes: 1 addition & 1 deletion openapi/mcp-gateway-api.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@portable-agent/contracts",
"version": "3.0.0",
"version": "4.0.0",
"private": true,
"files": [
"openapi",
Expand Down
35 changes: 35 additions & 0 deletions schemas/connection-widget.schema.json
Original file line number Diff line number Diff line change
@@ -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://"
}
}
}
}
}
13 changes: 13 additions & 0 deletions test/conversation-service.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
32 changes: 31 additions & 1 deletion test/schemas.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
Expand Down
Loading