diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 26dc5efc5..b76c6f0f2 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -290,7 +290,7 @@ jobs: const fs = require('node:fs'); const path = require('node:path'); - const dirs = ['a2a', 'types', 'engine', 'sdk-typescript', 'cli', 'mcp', 'react', 'openclaw']; + const dirs = ['a2a', 'types', 'engine', 'sdk-typescript', 'cli', 'mcp', 'react', 'openclaw', 'communication-policy']; const artifactDir = path.resolve('release-artifacts'); const packages = []; for (const dir of dirs) { @@ -365,6 +365,7 @@ jobs: - mcp - react - openclaw + - communication-policy steps: - name: Checkout code @@ -488,6 +489,7 @@ jobs: "@relaycast/mcp" "@relaycast/react" "@relaycast/openclaw" + "@relaycast/communication-policy" ) { echo "## Publish reconciliation for v${NEW_VERSION}" @@ -842,6 +844,7 @@ jobs: | @relaycast/mcp | ${{ needs.build.outputs.new_version }} | | @relaycast/react | ${{ needs.build.outputs.new_version }} | | @relaycast/openclaw | ${{ needs.build.outputs.new_version }} | + | @relaycast/communication-policy | ${{ needs.build.outputs.new_version }} | generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..871dcd791 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ + +# Canonical owner-generated contract bundles. +packages/communication-policy/contracts/*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e80a00fb1..1efd41b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,11 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Packages without a separate changelog are covered by the cross-package notes below. -## [Unreleased - Patch] +## [Unreleased - Minor] + +### Added + +- Portable communication policy contracts and a storage-independent coordinator for scoped admission, conversation grants, and delivery receipts. ### Fixed diff --git a/README.md b/README.md index e2902465c..e590d38e9 100644 --- a/README.md +++ b/README.md @@ -1385,3 +1385,11 @@ targets fail closed with typed 410 responses; terminal intents clear their paylo The existing recovery sweep also cleans expired intents. After cleanup, a reused key is a fresh request, so stop automatic retries after the 24-hour window. See [capacity regression and composition](packages/engine/scripts/CAPACITY-REGRESSION.md). + +## Communication policy primitives + +[`@relaycast/communication-policy`](packages/communication-policy/README.md) owns +scoped admission, conversation grants, and delivery decisions. Its portable +coordinator accepts a storage adapter; installing it does not enable policy on +existing HTTP or realtime paths. Work-status interpretation belongs to the +Cloud/desktop product, and History remains an evidence provider. diff --git a/package-lock.json b/package-lock.json index b280d3ba2..74a86368a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12201,6 +12201,33 @@ "dependencies": { "zod": "^4.3.6" } + }, + "node_modules/@relaycast/communication-policy": { + "resolved": "packages/communication-policy", + "link": true + }, + "packages/communication-policy": { + "name": "@relaycast/communication-policy", + "version": "10.4.0", + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^22.19.3", + "typescript": "^5.9.3", + "vitest": "^4.1.0" + } + }, + "packages/communication-policy/node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } } } } diff --git a/packages/communication-policy/README.md b/packages/communication-policy/README.md new file mode 100644 index 000000000..1a8fcc588 --- /dev/null +++ b/packages/communication-policy/README.md @@ -0,0 +1,7 @@ +# Communication policy + +Relaycast owns Open, Knock, Allow list, Busy, and Offline interpretation; admission decisions; grants; receipts; and the delivery coordinator. No work states or PR completion rules live here. + +The root export contains pure schemas and decisions. `./service` provides the storage-independent coordinator. A storage adapter must serialize recipient changes with final injection and atomically commit each callback. Cloud supplies a PostgreSQL adapter; Relaycast engine adapters and direct transport coverage are required before enrollment can be enabled. + +Contract JSON is an internal cross-language artifact, not a new HTTP endpoint. Existing Relaycast HTTP APIs remain snake_case. `npm run contracts:check` verifies generated schemas and fixtures; `npm test` checks decisions. No transport is enrolled by installing this package. diff --git a/packages/communication-policy/contracts/communication-status.v1.json b/packages/communication-policy/contracts/communication-status.v1.json new file mode 100644 index 000000000..0bed20f2f --- /dev/null +++ b/packages/communication-policy/contracts/communication-status.v1.json @@ -0,0 +1,2435 @@ +{ + "schemaVersion": 1, + "owner": "@relaycast/communication-policy", + "types": { + "CommunicationLabel": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "enum": [ + "open", + "knock", + "allow_list", + "busy", + "offline" + ] + }, + "CommunicationIdentity": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "CommunicationPolicy": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "recipient": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "admission": { + "type": "string", + "enum": [ + "open", + "knock", + "allow_list" + ] + }, + "interruption": { + "type": "string", + "enum": [ + "available", + "busy" + ] + }, + "allowedAgentIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "schemaVersion", + "recipient", + "revision", + "admission", + "interruption", + "allowedAgentIds" + ], + "additionalProperties": false + }, + "CommunicationStatus": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "policy": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "recipient": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "admission": { + "type": "string", + "enum": [ + "open", + "knock", + "allow_list" + ] + }, + "interruption": { + "type": "string", + "enum": [ + "available", + "busy" + ] + }, + "allowedAgentIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "schemaVersion", + "recipient", + "revision", + "admission", + "interruption", + "allowedAgentIds" + ], + "additionalProperties": false + }, + "connection": { + "type": "string", + "enum": [ + "online", + "offline" + ] + }, + "queueSupported": { + "type": "boolean" + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "observedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "policy", + "connection", + "queueSupported", + "revision", + "observedAt" + ], + "additionalProperties": false + }, + "ConversationGrant": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "id": { + "type": "string", + "minLength": 1 + }, + "recipient": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "sender": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "conversationId": { + "type": "string", + "minLength": 1 + }, + "state": { + "type": "string", + "enum": [ + "pending", + "approved", + "rejected", + "revoked", + "expired" + ] + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "id", + "recipient", + "sender", + "conversationId", + "state", + "revision", + "expiresAt" + ], + "additionalProperties": false + }, + "CommunicationMessage": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "sender": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "recipient": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "conversationId": { + "type": "string", + "minLength": 1 + }, + "inReplyTo": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sender", + "recipient", + "conversationId", + "inReplyTo" + ], + "additionalProperties": false + }, + "AwaitedReply": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "sender": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "conversationId": { + "type": "string", + "minLength": 1 + }, + "requestId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "sender", + "conversationId", + "requestId" + ], + "additionalProperties": false + }, + "CommunicationDecision": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "deliver", + "request_approval", + "hold", + "queue", + "reject" + ] + }, + "reason": { + "type": "string", + "enum": [ + "allowed", + "awaited_reply", + "approval_required", + "grant_rejected", + "grant_revoked", + "grant_expired", + "not_allowed", + "wrong_scope", + "wrong_recipient", + "busy", + "offline", + "offline_not_queueable" + ] + }, + "policyRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "required": [ + "action", + "reason", + "policyRevision" + ], + "additionalProperties": false + }, + "CommunicationReceipt": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "messageId": { + "type": "string", + "minLength": 1 + }, + "state": { + "type": "string", + "enum": [ + "pending_approval", + "held", + "queued", + "delivered", + "rejected", + "expired" + ] + }, + "policyRevision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "observedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "messageId", + "state", + "policyRevision", + "observedAt" + ], + "additionalProperties": false + }, + "CommunicationDecisionInput": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "status": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "policy": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "recipient": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "admission": { + "type": "string", + "enum": [ + "open", + "knock", + "allow_list" + ] + }, + "interruption": { + "type": "string", + "enum": [ + "available", + "busy" + ] + }, + "allowedAgentIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": [ + "schemaVersion", + "recipient", + "revision", + "admission", + "interruption", + "allowedAgentIds" + ], + "additionalProperties": false + }, + "connection": { + "type": "string", + "enum": [ + "online", + "offline" + ] + }, + "queueSupported": { + "type": "boolean" + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "observedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "policy", + "connection", + "queueSupported", + "revision", + "observedAt" + ], + "additionalProperties": false + }, + "message": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "sender": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "recipient": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "conversationId": { + "type": "string", + "minLength": 1 + }, + "inReplyTo": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "sender", + "recipient", + "conversationId", + "inReplyTo" + ], + "additionalProperties": false + }, + "grants": { + "type": "array", + "items": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "number", + "const": 1 + }, + "id": { + "type": "string", + "minLength": 1 + }, + "recipient": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "sender": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "conversationId": { + "type": "string", + "minLength": 1 + }, + "state": { + "type": "string", + "enum": [ + "pending", + "approved", + "rejected", + "revoked", + "expired" + ] + }, + "revision": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "schemaVersion", + "id", + "recipient", + "sender", + "conversationId", + "state", + "revision", + "expiresAt" + ], + "additionalProperties": false + } + }, + "awaitedReplies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sender": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + }, + "workspaceId": { + "type": "string", + "minLength": 1 + }, + "agentId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "organizationId", + "workspaceId", + "agentId" + ], + "additionalProperties": false + }, + "conversationId": { + "type": "string", + "minLength": 1 + }, + "requestId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "sender", + "conversationId", + "requestId" + ], + "additionalProperties": false + } + }, + "now": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" + } + }, + "required": [ + "status", + "message", + "grants", + "awaitedReplies", + "now" + ], + "additionalProperties": false + } + }, + "fixtures": { + "schemaVersion": 1, + "cases": [ + { + "name": "open accepts an authenticated scoped sender", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "open", + "expected": { + "action": "deliver", + "reason": "allowed", + "policyRevision": 1 + } + }, + { + "name": "open does not cross workspace", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_b", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "open", + "expected": { + "action": "reject", + "reason": "wrong_scope", + "policyRevision": 1 + } + }, + { + "name": "knock requires approval", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "request_approval", + "reason": "approval_required", + "policyRevision": 1 + } + }, + { + "name": "approved knock admits that conversation", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "deliver", + "reason": "allowed", + "policyRevision": 1 + } + }, + { + "name": "approval does not grant another conversation", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_b", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "request_approval", + "reason": "approval_required", + "policyRevision": 1 + } + }, + { + "name": "rejected knock cannot deliver", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "rejected", + "revision": 2, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_rejected", + "policyRevision": 1 + } + }, + { + "name": "revoked knock cannot deliver", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "revoked", + "revision": 2, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_revoked", + "policyRevision": 1 + } + }, + { + "name": "expired knock cannot deliver", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "expired", + "revision": 2, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_expired", + "policyRevision": 1 + } + }, + { + "name": "expiry boundary denies admission", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-13T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_expired", + "policyRevision": 1 + } + }, + { + "name": "allow list matches stable identity", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "available", + "allowedAgentIds": [ + "agent_sender" + ] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "allow_list", + "expected": { + "action": "deliver", + "reason": "allowed", + "policyRevision": 1 + } + }, + { + "name": "unlisted sender is refused", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "available", + "allowedAgentIds": [ + "another_agent" + ] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "allow_list", + "expected": { + "action": "reject", + "reason": "not_allowed", + "policyRevision": 1 + } + }, + { + "name": "busy holds ordinary messages", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "hold", + "reason": "busy", + "policyRevision": 1 + } + }, + { + "name": "busy admits correlated awaited reply", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": "request_a" + }, + "grants": [], + "awaitedReplies": [ + { + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "requestId": "request_a" + } + ], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "deliver", + "reason": "awaited_reply", + "policyRevision": 1 + } + }, + { + "name": "busy rejects wrong reply correlation", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": "other_request" + }, + "grants": [], + "awaitedReplies": [ + { + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "requestId": "request_a" + } + ], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "hold", + "reason": "busy", + "policyRevision": 1 + } + }, + { + "name": "awaited reply cannot bypass allow list", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": "request_a" + }, + "grants": [], + "awaitedReplies": [ + { + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "requestId": "request_a" + } + ], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "reject", + "reason": "not_allowed", + "policyRevision": 1 + } + }, + { + "name": "offline wins over busy and retains policy", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "busy", + "allowedAgentIds": [ + "agent_sender" + ] + }, + "connection": "offline", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "offline", + "expected": { + "action": "queue", + "reason": "offline", + "policyRevision": 1 + } + }, + { + "name": "offline transport without queue refuses", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "offline", + "queueSupported": false, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "offline", + "expected": { + "action": "reject", + "reason": "offline_not_queueable", + "policyRevision": 1 + } + }, + { + "name": "offline still requires knock admission", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "offline", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "offline", + "expected": { + "action": "request_approval", + "reason": "approval_required", + "policyRevision": 1 + } + }, + { + "name": "conflicting grant revision cannot restore approval", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "revoked", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_revoked", + "policyRevision": 1 + } + } + ], + "reads": [ + { + "name": "read failure keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": null, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "unsupported schema keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 2, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "stale revision keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 0, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "unknown enum keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "future_policy", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "other workspace keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "another_workspace", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "unsupported policy keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 2, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "new supported revision is accepted", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 2, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 2, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": false + } + ] + } +} diff --git a/packages/communication-policy/fixtures/communication-status.v1.json b/packages/communication-policy/fixtures/communication-status.v1.json new file mode 100644 index 000000000..3a8a5d095 --- /dev/null +++ b/packages/communication-policy/fixtures/communication-status.v1.json @@ -0,0 +1,1563 @@ +{ + "schemaVersion": 1, + "cases": [ + { + "name": "open accepts an authenticated scoped sender", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "open", + "expected": { + "action": "deliver", + "reason": "allowed", + "policyRevision": 1 + } + }, + { + "name": "open does not cross workspace", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_b", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "open", + "expected": { + "action": "reject", + "reason": "wrong_scope", + "policyRevision": 1 + } + }, + { + "name": "knock requires approval", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "request_approval", + "reason": "approval_required", + "policyRevision": 1 + } + }, + { + "name": "approved knock admits that conversation", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "deliver", + "reason": "allowed", + "policyRevision": 1 + } + }, + { + "name": "approval does not grant another conversation", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_b", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "request_approval", + "reason": "approval_required", + "policyRevision": 1 + } + }, + { + "name": "rejected knock cannot deliver", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "rejected", + "revision": 2, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_rejected", + "policyRevision": 1 + } + }, + { + "name": "revoked knock cannot deliver", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "revoked", + "revision": 2, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_revoked", + "policyRevision": 1 + } + }, + { + "name": "expired knock cannot deliver", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "expired", + "revision": 2, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_expired", + "policyRevision": 1 + } + }, + { + "name": "expiry boundary denies admission", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-13T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_expired", + "policyRevision": 1 + } + }, + { + "name": "allow list matches stable identity", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "available", + "allowedAgentIds": ["agent_sender"] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "allow_list", + "expected": { + "action": "deliver", + "reason": "allowed", + "policyRevision": 1 + } + }, + { + "name": "unlisted sender is refused", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "available", + "allowedAgentIds": ["another_agent"] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "allow_list", + "expected": { + "action": "reject", + "reason": "not_allowed", + "policyRevision": 1 + } + }, + { + "name": "busy holds ordinary messages", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "hold", + "reason": "busy", + "policyRevision": 1 + } + }, + { + "name": "busy admits correlated awaited reply", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": "request_a" + }, + "grants": [], + "awaitedReplies": [ + { + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "requestId": "request_a" + } + ], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "deliver", + "reason": "awaited_reply", + "policyRevision": 1 + } + }, + { + "name": "busy rejects wrong reply correlation", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": "other_request" + }, + "grants": [], + "awaitedReplies": [ + { + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "requestId": "request_a" + } + ], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "hold", + "reason": "busy", + "policyRevision": 1 + } + }, + { + "name": "awaited reply cannot bypass allow list", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "busy", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": "request_a" + }, + "grants": [], + "awaitedReplies": [ + { + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "requestId": "request_a" + } + ], + "now": "2026-10-13T12:00:00Z" + }, + "label": "busy", + "expected": { + "action": "reject", + "reason": "not_allowed", + "policyRevision": 1 + } + }, + { + "name": "offline wins over busy and retains policy", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "allow_list", + "interruption": "busy", + "allowedAgentIds": ["agent_sender"] + }, + "connection": "offline", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "offline", + "expected": { + "action": "queue", + "reason": "offline", + "policyRevision": 1 + } + }, + { + "name": "offline transport without queue refuses", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "offline", + "queueSupported": false, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "offline", + "expected": { + "action": "reject", + "reason": "offline_not_queueable", + "policyRevision": 1 + } + }, + { + "name": "offline still requires knock admission", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "offline", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "offline", + "expected": { + "action": "request_approval", + "reason": "approval_required", + "policyRevision": 1 + } + }, + { + "name": "conflicting grant revision cannot restore approval", + "input": { + "status": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "knock", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "message": { + "id": "msg_a", + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "conversationId": "conv_a", + "inReplyTo": null + }, + "grants": [ + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "approved", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + }, + { + "schemaVersion": 1, + "id": "grant_a", + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "sender": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_sender" + }, + "conversationId": "conv_a", + "state": "revoked", + "revision": 1, + "expiresAt": "2026-10-14T12:00:00Z" + } + ], + "awaitedReplies": [], + "now": "2026-10-13T12:00:00Z" + }, + "label": "knock", + "expected": { + "action": "reject", + "reason": "grant_revoked", + "policyRevision": 1 + } + } + ], + "reads": [ + { + "name": "read failure keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": null, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "unsupported schema keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 2, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "stale revision keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 0, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "unknown enum keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "future_policy", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "other workspace keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "another_workspace", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "unsupported policy keeps last state", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 2, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": true + }, + { + "name": "new supported revision is accepted", + "previous": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 1, + "observedAt": "2026-10-13T12:00:00Z" + }, + "input": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 2, + "observedAt": "2026-10-13T12:00:00Z" + }, + "expected": { + "schemaVersion": 1, + "policy": { + "schemaVersion": 1, + "recipient": { + "organizationId": "org_a", + "workspaceId": "ws_a", + "agentId": "agent_recipient" + }, + "revision": 1, + "admission": "open", + "interruption": "available", + "allowedAgentIds": [] + }, + "connection": "online", + "queueSupported": true, + "revision": 2, + "observedAt": "2026-10-13T12:00:00Z" + }, + "stale": false + } + ] +} diff --git a/packages/communication-policy/package.json b/packages/communication-policy/package.json new file mode 100644 index 000000000..094ccde42 --- /dev/null +++ b/packages/communication-policy/package.json @@ -0,0 +1,56 @@ +{ + "name": "@relaycast/communication-policy", + "version": "10.4.0", + "description": "Relaycast communication admission and delivery policy", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./contract": { + "types": "./dist/contract.d.ts", + "import": "./dist/contract.js", + "default": "./dist/contract.js" + }, + "./service": { + "types": "./dist/service.d.ts", + "import": "./dist/service.js", + "default": "./dist/service.js" + }, + "./contracts/*": "./contracts/*" + }, + "files": [ + "dist", + "contracts", + "fixtures", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "test": "vitest run --config vitest.config.ts", + "contracts": "npm run build && node scripts/export-contracts.mjs", + "contracts:check": "npm run build && node scripts/export-contracts.mjs --check", + "prepack": "npm run contracts:check", + "lint": "eslint src/" + }, + "dependencies": { + "zod": "^4.4.3" + }, + "devDependencies": { + "typescript": "^5.9.3", + "vitest": "^4.1.0", + "@types/node": "^22.19.3" + }, + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/relaycast.git", + "directory": "packages/communication-policy" + }, + "private": false +} diff --git a/packages/communication-policy/scripts/export-contracts.mjs b/packages/communication-policy/scripts/export-contracts.mjs new file mode 100644 index 000000000..1a3a8014b --- /dev/null +++ b/packages/communication-policy/scripts/export-contracts.mjs @@ -0,0 +1,32 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { z } from "zod"; +import { communicationStatusSchemas } from "../dist/contract.js"; +const output = + JSON.stringify( + { + schemaVersion: 1, + owner: "@relaycast/communication-policy", + types: Object.fromEntries( + Object.entries(communicationStatusSchemas).map(([name, schema]) => [ + name, + z.toJSONSchema(schema), + ]), + ), + fixtures: JSON.parse( + readFileSync( + new URL("../fixtures/communication-status.v1.json", import.meta.url), + "utf8", + ), + ), + }, + null, + 2, + ) + "\n"; +const path = new URL( + "../contracts/communication-status.v1.json", + import.meta.url, +); +if (process.argv.includes("--check")) { + if (readFileSync(path, "utf8") !== output) + throw Error("Contract drift: communication-status"); +} else writeFileSync(path, output); diff --git a/packages/communication-policy/src/contract.ts b/packages/communication-policy/src/contract.ts new file mode 100644 index 000000000..c9a3b2c58 --- /dev/null +++ b/packages/communication-policy/src/contract.ts @@ -0,0 +1,220 @@ +/** Communication policy v1. Pure decisions; this module does not send or acknowledge messages. */ +import { z } from "zod"; + +const id = z.string().min(1); +const revision = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER); +const instant = z.iso.datetime({ offset: true }); +export const COMMUNICATION_STATUS_VERSION = 1 as const; +export const communicationLabel = z.enum([ + "open", + "knock", + "allow_list", + "busy", + "offline", +]); +export const communicationIdentity = z.strictObject({ + organizationId: id, + workspaceId: id, + agentId: id, +}); +export const communicationPolicy = z.strictObject({ + schemaVersion: z.literal(COMMUNICATION_STATUS_VERSION), + recipient: communicationIdentity, + revision, + admission: z.enum(["open", "knock", "allow_list"]), + interruption: z.enum(["available", "busy"]), + allowedAgentIds: z.array(id), +}); +export const communicationStatus = z.strictObject({ + schemaVersion: z.literal(COMMUNICATION_STATUS_VERSION), + policy: communicationPolicy, + connection: z.enum(["online", "offline"]), + queueSupported: z.boolean(), + /** Admission and connectivity can change independently. */ + revision, + observedAt: instant, +}); +export type CommunicationStatus = z.infer; +export type CommunicationPolicy = z.infer; +export const conversationGrant = z.strictObject({ + schemaVersion: z.literal(COMMUNICATION_STATUS_VERSION), + id, + recipient: communicationIdentity, + sender: communicationIdentity, + conversationId: id, + state: z.enum(["pending", "approved", "rejected", "revoked", "expired"]), + /** Monotonic for the recipient/sender/conversation tuple, including renewed knocks. */ + revision, + expiresAt: instant, +}); +export const communicationMessage = z.strictObject({ + id, + sender: communicationIdentity, + recipient: communicationIdentity, + conversationId: id, + inReplyTo: id.nullable(), +}); +export const awaitedReply = z.strictObject({ + sender: communicationIdentity, + conversationId: id, + requestId: id, +}); +export const communicationDecision = z.strictObject({ + action: z.enum(["deliver", "request_approval", "hold", "queue", "reject"]), + reason: z.enum([ + "allowed", + "awaited_reply", + "approval_required", + "grant_rejected", + "grant_revoked", + "grant_expired", + "not_allowed", + "wrong_scope", + "wrong_recipient", + "busy", + "offline", + "offline_not_queueable", + ]), + policyRevision: revision, +}); +export const communicationReceipt = z.strictObject({ + schemaVersion: z.literal(COMMUNICATION_STATUS_VERSION), + messageId: id, + state: z.enum([ + "pending_approval", + "held", + "queued", + "delivered", + "rejected", + "expired", + ]), + policyRevision: revision, + observedAt: instant, +}); +export const communicationDecisionInput = z.strictObject({ + status: communicationStatus, + message: communicationMessage, + grants: z.array(conversationGrant), + awaitedReplies: z.array(awaitedReply), + now: instant, +}); +export type CommunicationDecision = z.infer; + +/** Named schemas exported for generated desktop contracts. */ +export const communicationStatusSchemas = { + CommunicationLabel: communicationLabel, + CommunicationIdentity: communicationIdentity, + CommunicationPolicy: communicationPolicy, + CommunicationStatus: communicationStatus, + ConversationGrant: conversationGrant, + CommunicationMessage: communicationMessage, + AwaitedReply: awaitedReply, + CommunicationDecision: communicationDecision, + CommunicationReceipt: communicationReceipt, + CommunicationDecisionInput: communicationDecisionInput, +}; + +export function effectiveCommunicationStatus( + status: CommunicationStatus, +): z.infer { + return status.connection === "offline" + ? "offline" + : status.policy.interruption === "busy" + ? "busy" + : status.policy.admission; +} +const sameScope = ( + a: z.infer, + b: z.infer, +) => a.organizationId === b.organizationId && a.workspaceId === b.workspaceId; +const sameAgent = ( + a: z.infer, + b: z.infer, +) => sameScope(a, b) && a.agentId === b.agentId; + +/** Call only with authenticated identities and current grants from trusted storage. + * A deliver decision is permission to attempt delivery, never a delivered receipt. + * Re-evaluate before injection, including after approval/reconnect/policy changes. + */ +export function decideCommunication( + input: z.infer, +): CommunicationDecision { + const { status, message, grants, awaitedReplies, now } = + communicationDecisionInput.parse(input); + const p = status.policy; + const result = ( + action: CommunicationDecision["action"], + reason: CommunicationDecision["reason"], + ): CommunicationDecision => ({ action, reason, policyRevision: p.revision }); + if (!sameAgent(message.recipient, p.recipient)) + return result("reject", "wrong_recipient"); + if (!sameScope(message.sender, p.recipient)) + return result("reject", "wrong_scope"); + if ( + p.admission === "allow_list" && + !p.allowedAgentIds.includes(message.sender.agentId) + ) + return result("reject", "not_allowed"); + if (p.admission === "knock") { + // The latest revision wins, even if an older grant was approved. + const denialRank = (g: z.infer) => + g.state === "revoked" + ? 5 + : g.state === "rejected" + ? 4 + : g.state === "expired" || Date.parse(g.expiresAt) <= Date.parse(now) + ? 3 + : g.state === "pending" + ? 2 + : 1; + const matching = grants + .filter( + (g) => + sameAgent(g.sender, message.sender) && + sameAgent(g.recipient, p.recipient) && + g.conversationId === message.conversationId, + ) + .sort((a, b) => b.revision - a.revision || denialRank(b) - denialRank(a)); + const grant = matching[0]; + if (grant?.state === "rejected") return result("reject", "grant_rejected"); + if (grant?.state === "revoked") return result("reject", "grant_revoked"); + if ( + grant && + (grant.state === "expired" || + Date.parse(grant.expiresAt) <= Date.parse(now)) + ) + return result("reject", "grant_expired"); + if (!grant || grant.state !== "approved") + return result("request_approval", "approval_required"); + } + if (status.connection === "offline") + return result( + status.queueSupported ? "queue" : "reject", + status.queueSupported ? "offline" : "offline_not_queueable", + ); + const expectedReply = awaitedReplies.some( + (r) => + sameAgent(r.sender, message.sender) && + r.conversationId === message.conversationId && + r.requestId === message.inReplyTo, + ); + if (p.interruption === "busy" && !expectedReply) + return result("hold", "busy"); + return result("deliver", expectedReply ? "awaited_reply" : "allowed"); +} + +/** Unsupported versions, stale responses, and read failures never reset a policy to Open. */ +export function readCommunicationStatus( + input: unknown, + previous: CommunicationStatus, +) { + const parsed = communicationStatus.safeParse(input); + if ( + !parsed.success || + !sameAgent(parsed.data.policy.recipient, previous.policy.recipient) || + parsed.data.revision < previous.revision || + parsed.data.policy.revision < previous.policy.revision + ) + return { status: previous, stale: true }; + return { status: parsed.data, stale: false }; +} diff --git a/packages/communication-policy/src/index.ts b/packages/communication-policy/src/index.ts new file mode 100644 index 000000000..03247eb4f --- /dev/null +++ b/packages/communication-policy/src/index.ts @@ -0,0 +1 @@ +export * from "./contract.js"; diff --git a/packages/communication-policy/src/service.ts b/packages/communication-policy/src/service.ts new file mode 100644 index 000000000..7763de655 --- /dev/null +++ b/packages/communication-policy/src/service.ts @@ -0,0 +1,388 @@ +import { createHash } from "node:crypto"; +import { z } from "zod"; +import { + awaitedReply, + communicationMessage, + communicationPolicy, + communicationReceipt, + conversationGrant, + decideCommunication, + type CommunicationPolicy, + type CommunicationStatus, +} from "./contract.js"; + +export type StoredPolicy = { + id: string; + workspaceId: string; + ownerUserId: string; + agentName: string; + policy: Record; + grants: unknown[]; + awaitedReplies: unknown[]; +}; +export type StoredReceipt = { + fingerprint: string; + receipt: Record; + dispatching: boolean; +}; +export interface CommunicationTransaction { + context: Context; + record: StoredPolicy | null; + savePolicy( + patch: Partial>, + ): Promise; + readReceipt(deliveryKey: string): Promise; + saveReceipt(deliveryKey: string, receipt: StoredReceipt): Promise; +} +/** Storage must atomically commit/rollback each callback and serialize ALL recipient + * mutations and final injection across every process. A process-local mutex alone + * is insufficient for shared stores. Dispatch intent commits before external I/O; + * a failed final commit leaves the durable intent held, preventing blind reinjection. + */ +export interface CommunicationStore { + insert(record: StoredPolicy): Promise; + read(id: string): Promise; + binding(workspaceId: string, agentName: string): Promise; + withRecipient( + id: string, + fn: (tx: CommunicationTransaction) => Promise, + ): Promise; +} + +type Identity = CommunicationPolicy["recipient"]; +type Message = z.infer; +type Receipt = z.infer; +type Grant = z.infer; +/** Construct only from verified request auth, never a tool argument. */ +export type CommunicationActor = { + userId: string; + workspaceId: string; + organizationId: string; + kind: "human" | "agent"; + capabilities: readonly ("communication:manage" | "communication:approve")[]; +}; +const digest = (value: unknown) => + createHash("sha256").update(JSON.stringify(value)).digest("hex"); +export const recipientKey = (identity: Identity) => + digest([identity.organizationId, identity.workspaceId, identity.agentId]); +const equalIdentity = (a: Identity, b: Identity) => + recipientKey(a) === recipientKey(b); + +export function createCommunicationService(deps: { + store: CommunicationStore; + now(): Date; + /** Server-owned capability registry must attest ALL ingress/read paths, not a client-supplied flag. */ + verifyTransportCoverage(recipient: Identity): Promise; + verifyOwnership( + actor: CommunicationActor, + recipient: Identity, + agentName: string, + ): Promise; +}) { + const { store } = deps; + function authorize( + actor: CommunicationActor, + policy: CommunicationPolicy, + ownerUserId: string, + permission: "communication:manage" | "communication:approve", + ) { + if ( + actor.workspaceId !== policy.recipient.workspaceId || + actor.organizationId !== policy.recipient.organizationId || + actor.userId !== ownerUserId || + (actor.kind !== "human" && !actor.capabilities.includes(permission)) + ) + throw new Error("communication_forbidden"); + } + async function locked( + identity: Identity, + fn: ( + tx: CommunicationTransaction, + row: StoredPolicy, + policy: CommunicationPolicy, + ) => Promise, + ) { + return store.withRecipient(recipientKey(identity), async (tx) => { + const row = tx.record; + if (!row) throw new Error("communication_not_enrolled"); + const policy = communicationPolicy.parse(row.policy); + if (!equalIdentity(policy.recipient, identity)) + throw new Error("communication_wrong_recipient"); + return fn(tx, row, policy); + }); + } + function decision( + row: StoredPolicy, + policy: CommunicationPolicy, + message: Message, + connection: CommunicationStatus["connection"], + queueSupported: boolean, + ) { + return decideCommunication({ + status: { + schemaVersion: 1, + policy, + connection, + queueSupported, + revision: policy.revision, + observedAt: deps.now().toISOString(), + }, + message, + grants: z.array(conversationGrant).parse(row.grants), + awaitedReplies: z.array(awaitedReply).parse(row.awaitedReplies), + now: deps.now().toISOString(), + }); + } + const stateFor = ( + d: ReturnType, + ): Receipt["state"] => + d.reason === "grant_expired" + ? "expired" + : ( + { + deliver: "queued", + request_approval: "pending_approval", + hold: "held", + queue: "queued", + reject: "rejected", + } as const + )[d.action]; + function receipt( + message: Message, + policyRevision: number, + state: Receipt["state"], + ): Receipt { + return { + schemaVersion: 1, + messageId: message.id, + state, + policyRevision, + observedAt: deps.now().toISOString(), + }; + } + return { + async enroll( + actor: CommunicationActor, + agentName: string, + policyInput: CommunicationPolicy, + ) { + const policy = communicationPolicy.parse(policyInput); + authorize(actor, policy, actor.userId, "communication:manage"); + if ( + policy.revision !== 0 || + !agentName || + !(await deps.verifyOwnership(actor, policy.recipient, agentName)) || + !(await deps.verifyTransportCoverage(policy.recipient)) + ) + throw new Error("communication_transport_not_ready"); + const row = await store.insert({ + id: recipientKey(policy.recipient), + workspaceId: actor.workspaceId, + ownerUserId: actor.userId, + agentName, + policy, + grants: [], + awaitedReplies: [], + }); + if (!row) throw new Error("communication_already_enrolled"); + return policy; + }, + async updatePolicy(actor: CommunicationActor, input: CommunicationPolicy) { + const next = communicationPolicy.parse(input); + return locked(next.recipient, async (tx, row, previous) => { + authorize(actor, previous, row.ownerUserId, "communication:manage"); + if (next.revision !== previous.revision + 1) + throw new Error("communication_revision_conflict"); + await tx.savePolicy({ policy: next }); + return next; + }); + }, + async setGrant(actor: CommunicationActor, input: Grant) { + const grant = conversationGrant.parse(input); + return locked(grant.recipient, async (tx, row, policy) => { + authorize(actor, policy, row.ownerUserId, "communication:approve"); + if ( + grant.sender.workspaceId !== grant.recipient.workspaceId || + grant.sender.organizationId !== grant.recipient.organizationId + ) + throw new Error("communication_wrong_scope"); + const grants = z.array(conversationGrant).parse(row.grants); + const index = grants.findIndex( + (g) => + equalIdentity(g.sender, grant.sender) && + g.conversationId === grant.conversationId, + ); + const previous = grants[index]; + if ( + !previous || + previous.id !== grant.id || + grant.revision !== previous.revision + 1 + ) + throw new Error("communication_revision_conflict"); + if ( + grant.state === "approved" && + Date.parse(grant.expiresAt) <= deps.now().getTime() + ) + throw new Error("communication_expired_grant"); + grants[index] = grant; + await tx.savePolicy({ grants }); + return grant; + }); + }, + async setAwaitedReplies( + actor: CommunicationActor, + identity: Identity, + expectedRevision: number, + input: z.infer[], + ) { + const awaitedReplies = z.array(awaitedReply).max(100).parse(input); + return locked(identity, async (tx, row, policy) => { + authorize(actor, policy, row.ownerUserId, "communication:manage"); + if (expectedRevision !== policy.revision) + throw new Error("communication_revision_conflict"); + if ( + awaitedReplies.some( + (r) => + r.sender.workspaceId !== identity.workspaceId || + r.sender.organizationId !== identity.organizationId, + ) + ) + throw new Error("communication_wrong_scope"); + await tx.savePolicy({ + awaitedReplies, + policy: { ...policy, revision: policy.revision + 1 }, + }); + }); + }, + async read(identity: Identity) { + const row = await store.read(recipientKey(identity)); + if (!row) return null; + return { + policy: communicationPolicy.parse(row.policy), + grants: z.array(conversationGrant).parse(row.grants), + }; + }, + async binding(workspaceId: string, agentName: string) { + const row = await store.binding(workspaceId, agentName); + return row ? communicationPolicy.parse(row.policy) : null; + }, + /** Read-time admission. Caller has already authenticated message sender/recipient and scope. Never returns message text on denial. */ + async inspect(messageInput: Message) { + const message = communicationMessage.parse(messageInput); + return locked(message.recipient, async (_tx, row, policy) => + decision(row, policy, message, "online", true), + ); + }, + /** No body is persisted in Status. A knock exposes only sender/conversation identity until approval. */ + async deliver(input: { + message: Message; + deliveryKey: string; + contentHash: string; + connection: CommunicationStatus["connection"]; + queueSupported: boolean; + send(context: Context): Promise; + }): Promise { + const message = communicationMessage.parse(input.message); + if (!input.deliveryKey || !/^[a-f0-9]{64}$/.test(input.contentHash)) + throw new Error("invalid_delivery_key"); + const id = recipientKey(message.recipient); + const fingerprint = digest([message, input.contentHash]); + // Commit a dispatch intent before touching a transport. An ambiguous result is held, never re-injected automatically. + const prepared = await locked( + message.recipient, + async (tx, row, policy) => { + const existing = await tx.readReceipt(input.deliveryKey); + if (existing && existing.fingerprint !== fingerprint) + throw new Error("communication_message_conflict"); + if (existing) { + const prior = communicationReceipt.parse(existing.receipt); + if ( + existing.dispatching || + ["delivered", "rejected", "expired"].includes(prior.state) + ) + return { done: prior }; + } + const d = decision( + row, + policy, + message, + input.connection, + input.queueSupported, + ); + if (d.action === "request_approval") { + const grants = z.array(conversationGrant).parse(row.grants); + if ( + !grants.some( + (g) => + equalIdentity(g.sender, message.sender) && + g.conversationId === message.conversationId, + ) + ) { + if (grants.length >= 1000) + throw new Error("communication_knock_limit"); + grants.push({ + schemaVersion: 1, + id: digest([ + id, + message.sender.agentId, + message.conversationId, + ]), + recipient: message.recipient, + sender: message.sender, + conversationId: message.conversationId, + state: "pending", + revision: 0, + expiresAt: new Date( + deps.now().getTime() + 86_400_000, + ).toISOString(), + }); + await tx.savePolicy({ grants }); + } + } + const next = receipt( + message, + policy.revision, + d.action === "deliver" ? "held" : stateFor(d), + ); + await tx.saveReceipt(input.deliveryKey, { + fingerprint, + receipt: next, + dispatching: d.action === "deliver", + }); + return d.action === "deliver" ? { done: null } : { done: next }; + }, + ); + if (prepared.done) return prepared.done; + return locked(message.recipient, async (tx, row, policy) => { + // Lock orders revocation/policy changes with the final injection attempt. + const d = decision( + row, + policy, + message, + input.connection, + input.queueSupported, + ); + let state = stateFor(d); + let uncertain = false; + if (d.action === "deliver") { + try { + state = (await input.send(tx.context)) ? "delivered" : "queued"; + } catch { + state = "held"; + uncertain = true; + } + } + const next = receipt(message, policy.revision, state); + await tx.saveReceipt(input.deliveryKey, { + fingerprint, + receipt: next, + dispatching: uncertain, + }); + return next; + }); + }, + }; +} +export type CommunicationService = ReturnType< + typeof createCommunicationService +>; diff --git a/packages/communication-policy/test/contract.test.ts b/packages/communication-policy/test/contract.test.ts new file mode 100644 index 000000000..33284b87c --- /dev/null +++ b/packages/communication-policy/test/contract.test.ts @@ -0,0 +1,111 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { + communicationDecisionInput, + communicationReceipt, + communicationStatus, + decideCommunication, + effectiveCommunicationStatus, + readCommunicationStatus, + communicationStatusSchemas, +} from "../src/contract.js"; + +const fixtures = JSON.parse( + readFileSync( + new URL("../fixtures/communication-status.v1.json", import.meta.url), + "utf8", + ), +); +describe("communication status v1 contract", () => { + for (const fixture of fixtures.reads) { + it(fixture.name, () => { + expect(readCommunicationStatus(fixture.input, fixture.previous)).toEqual({ + status: fixture.expected, + stale: fixture.stale, + }); + }); + } + it("the exported bundle matches the owned schemas and decision fixtures", () => { + const bundle = JSON.parse( + readFileSync( + new URL("../contracts/communication-status.v1.json", import.meta.url), + "utf8", + ), + ); + expect(bundle.fixtures).toEqual(fixtures); + expect(bundle.types).toEqual( + Object.fromEntries( + Object.entries(communicationStatusSchemas).map(([name, schema]) => [ + name, + z.toJSONSchema(schema), + ]), + ), + ); + }); + for (const fixture of fixtures.cases) { + it(fixture.name, () => { + const input = communicationDecisionInput.parse(fixture.input); + expect(effectiveCommunicationStatus(input.status)).toBe(fixture.label); + expect(decideCommunication(input)).toEqual(fixture.expected); + }); + } + const status = communicationStatus.parse(fixtures.cases[0].input.status); + it("reconnecting and clearing Busy restores the saved admission policy", () => { + const saved = { + ...status, + policy: { + ...status.policy, + admission: "knock" as const, + interruption: "busy" as const, + }, + }; + expect( + effectiveCommunicationStatus({ ...saved, connection: "offline" }), + ).toBe("offline"); + expect(effectiveCommunicationStatus(saved)).toBe("busy"); + expect( + effectiveCommunicationStatus({ + ...saved, + policy: { ...saved.policy, interruption: "available" }, + }), + ).toBe("knock"); + }); + it.each([ + null, + {}, + { ...status, schemaVersion: 2 }, + { ...status, revision: 0 }, + { ...status, policy: { ...status.policy, admission: "future_policy" } }, + { ...status, policy: { ...status.policy, revision: 0 } }, + ])("failed or unsupported reads cannot reset admission to Open", (input) => { + expect(readCommunicationStatus(input, status)).toEqual({ + status, + stale: true, + }); + }); + it("a delivery decision is not a delivered receipt", () => { + const input = communicationDecisionInput.parse(fixtures.cases[0].input); + expect(decideCommunication(input).action).toBe("deliver"); + expect( + communicationReceipt.safeParse(decideCommunication(input)).success, + ).toBe(false); + }); + it("an approval for another sender or recipient grants no access", () => { + const input = communicationDecisionInput.parse( + fixtures.cases.find( + (f: { name: string }) => + f.name === "approved knock admits that conversation", + ).input, + ); + for (const side of ["sender", "recipient"] as const) { + const grants = input.grants.map((g) => ({ + ...g, + [side]: { ...g[side], agentId: "different_agent" }, + })); + expect(decideCommunication({ ...input, grants }).action).toBe( + "request_approval", + ); + } + }); +}); diff --git a/packages/communication-policy/test/service.test.ts b/packages/communication-policy/test/service.test.ts new file mode 100644 index 000000000..2a4dff8fa --- /dev/null +++ b/packages/communication-policy/test/service.test.ts @@ -0,0 +1,189 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { + createCommunicationService, + type CommunicationStore, + type StoredPolicy, + type StoredReceipt, +} from "../src/service.js"; +import type { CommunicationPolicy } from "../src/contract.js"; +const recipient = { + organizationId: "org", + workspaceId: "rw_prod", + agentId: "receiver", +}; +const sender = { ...recipient, agentId: "sender" }; +const actor = { + userId: "user", + workspaceId: recipient.workspaceId, + organizationId: recipient.organizationId, + kind: "human" as const, + capabilities: [], +}; +const policy: CommunicationPolicy = { + schemaVersion: 1, + recipient, + revision: 0, + admission: "open", + interruption: "available", + allowedAgentIds: [], +}; +const now = new Date("2026-10-14T00:00:00Z"); +const message = { + id: "message", + recipient, + sender, + conversationId: "conversation", + inReplyTo: null, +}; +let rows: Map, + receipts: Map, + tail: Promise; +let locks: number, + beforeLock: ((n: number) => Promise) | undefined, + failCommit: boolean; +let service: ReturnType>; +const send = vi.fn(async () => true); +const delivery = () => ({ + message, + deliveryKey: "delivery", + contentHash: "a".repeat(64), + connection: "online" as const, + queueSupported: true, + send, +}); +beforeEach(() => { + rows = new Map(); + receipts = new Map(); + tail = Promise.resolve(); + locks = 0; + beforeLock = undefined; + failCommit = false; + send.mockReset(); + send.mockResolvedValue(true); + const store: CommunicationStore = { + async insert(row) { + if (rows.has(row.id)) return false; + rows.set(row.id, structuredClone(row)); + return true; + }, + async read(id) { + return structuredClone(rows.get(id) ?? null); + }, + async binding(workspaceId, agentName) { + return structuredClone( + [...rows.values()].find( + (r) => r.workspaceId === workspaceId && r.agentName === agentName, + ) ?? null, + ); + }, + async withRecipient(id, fn) { + await beforeLock?.(++locks); + const operation = tail.then(async () => { + const nextRows = structuredClone(rows), + nextReceipts = structuredClone(receipts); + const key = (deliveryKey: string) => JSON.stringify([id, deliveryKey]); + const result = await fn({ + context: undefined, + record: nextRows.get(id) ?? null, + async savePolicy(patch) { + Object.assign(nextRows.get(id)!, patch); + }, + async readReceipt(k) { + return nextReceipts.get(key(k)) ?? null; + }, + async saveReceipt(k, value) { + nextReceipts.set(key(k), value); + }, + }); + if (failCommit) { + failCommit = false; + throw Error("commit failed"); + } + rows = nextRows; + receipts = nextReceipts; + return result; + }); + tail = operation.catch(() => undefined); + return operation; + }, + }; + service = createCommunicationService({ + store, + now: () => now, + verifyOwnership: async () => true, + verifyTransportCoverage: async () => true, + }); +}); +it("serializes concurrent delivery and exact retries without reinjection", async () => { + await service.enroll(actor, "receiver", policy); + const result = await Promise.all([ + service.deliver(delivery()), + service.deliver(delivery()), + ]); + expect(result.some((r) => r.state === "delivered")).toBe(true); + expect(send).toHaveBeenCalledTimes(1); + expect((await service.deliver(delivery())).state).toBe("delivered"); + expect(send).toHaveBeenCalledTimes(1); + await expect( + service.deliver({ ...delivery(), contentHash: "b".repeat(64) }), + ).rejects.toThrow("message_conflict"); +}); +it("owns knock creation, approval, conversation scope, and revocation", async () => { + await service.enroll(actor, "receiver", { ...policy, admission: "knock" }); + expect((await service.deliver(delivery())).state).toBe("pending_approval"); + await service.deliver(delivery()); + const [grant] = (await service.read(recipient))!.grants; + expect((await service.read(recipient))!.grants).toHaveLength(1); + expect(send).not.toHaveBeenCalled(); + await service.setGrant(actor, { ...grant!, revision: 1, state: "approved" }); + expect((await service.inspect(message)).action).toBe("deliver"); + expect( + (await service.inspect({ ...message, conversationId: "other" })).action, + ).toBe("request_approval"); + await service.setGrant(actor, { ...grant!, revision: 2, state: "revoked" }); + expect((await service.inspect(message)).reason).toBe("grant_revoked"); +}); +it("rechecks policy between durable intent and final injection", async () => { + await service.enroll(actor, "receiver", policy); + beforeLock = async (n) => { + if (n === 2) + await service.updatePolicy(actor, { + ...policy, + revision: 1, + admission: "allow_list", + }); + }; + expect((await service.deliver(delivery())).state).toBe("rejected"); + expect(send).not.toHaveBeenCalled(); +}); +it("retains the committed intent if the final receipt commit fails", async () => { + await service.enroll(actor, "receiver", policy); + send.mockImplementationOnce(async () => { + failCommit = true; + return true; + }); + await expect(service.deliver(delivery())).rejects.toThrow("commit failed"); + expect((await service.deliver(delivery())).state).toBe("held"); + expect(send).toHaveBeenCalledTimes(1); +}); +it("does not dispatch if the intent cannot commit", async () => { + await service.enroll(actor, "receiver", policy); + failCommit = true; + await expect(service.deliver(delivery())).rejects.toThrow("commit failed"); + expect(send).not.toHaveBeenCalled(); +}); +it("requires owner authority for policy, grants and awaited replies", async () => { + await service.enroll(actor, "receiver", policy); + await expect( + service.updatePolicy( + { ...actor, userId: "other" }, + { ...policy, revision: 1 }, + ), + ).rejects.toThrow("forbidden"); + await expect( + service.setAwaitedReplies({ ...actor, kind: "agent" }, recipient, 0, []), + ).rejects.toThrow("forbidden"); + await expect( + service.updatePolicy(actor, { ...policy, revision: 3 }), + ).rejects.toThrow("revision_conflict"); +}); diff --git a/packages/communication-policy/tsconfig.json b/packages/communication-policy/tsconfig.json new file mode 100644 index 000000000..5c5d393b0 --- /dev/null +++ b/packages/communication-policy/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/communication-policy/vitest.config.ts b/packages/communication-policy/vitest.config.ts new file mode 100644 index 000000000..5102c6457 --- /dev/null +++ b/packages/communication-policy/vitest.config.ts @@ -0,0 +1 @@ +export default { test: { include: ['test/**/*.test.ts'] } }; diff --git a/scripts/release-contract.mjs b/scripts/release-contract.mjs index 8a81eb135..9d31afee0 100644 --- a/scripts/release-contract.mjs +++ b/scripts/release-contract.mjs @@ -10,6 +10,7 @@ export const PUBLISHED_PACKAGE_DIRS = [ "mcp", "react", "openclaw", + "communication-policy", ]; const LEVEL_RANK = { Patch: 0, Minor: 1, Major: 2 }; diff --git a/scripts/release-workflow-contract.test.mjs b/scripts/release-workflow-contract.test.mjs index b0d900089..1758c179e 100644 --- a/scripts/release-workflow-contract.test.mjs +++ b/scripts/release-workflow-contract.test.mjs @@ -382,7 +382,7 @@ describe("publish workflow safety contract", () => { assert.match(packageInput, /options:\n\s+- all\n/); assert.doesNotMatch( packageInput, - /- (?:a2a|types|engine|sdk-typescript|cli|mcp|react|openclaw)/, + /- (?:a2a|types|engine|sdk-typescript|cli|mcp|react|openclaw|communication-policy)/, ); assert.doesNotMatch(workflow, /^ publish-single:/m); assert.doesNotMatch(workflow, /needs\.publish-single/); @@ -481,6 +481,7 @@ describe("publish workflow safety contract", () => { "@relaycast/mcp", "@relaycast/react", "@relaycast/openclaw", + "@relaycast/communication-policy", ]) { assert.match(verify, new RegExp(`"${pkg.replace(/[/]/g, "\\/")}"`)); }