@canonmsg/backend-contracts 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -22,3 +22,6 @@ __exportStar(require("./runtimeCardStorage.js"), exports);
22
22
  __exportStar(require("./turnProtocol.js"), exports);
23
23
  __exportStar(require("./agentBehaviorPolicy.js"), exports);
24
24
  __exportStar(require("./contactRequest.js"), exports);
25
+ __exportStar(require("./verbContract.js"), exports);
26
+ __exportStar(require("./verbSchemas.js"), exports);
27
+ __exportStar(require("./verbWire.js"), exports);
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ /**
3
+ * Canon agent verb contract — the canonical, runtime-agnostic vocabulary for
4
+ * the conversational/HITL actions an agent deliberately takes against Canon
5
+ * (v1 scope: messaging, HITL interactions, contact sharing, and the read
6
+ * verbs bindings need alongside them).
7
+ *
8
+ * One verb = one intent-level action. Each runtime binding (Hermes native
9
+ * tool, MCP server, codex dynamicTools, agent-sdk method, CLI) projects these
10
+ * verbs into its native tool surface; the names, argument shapes, limits, and
11
+ * result vocabularies defined here are the single source of truth. JSON
12
+ * Schemas for each verb live in `verbSchemas.ts` and are also emitted as a
13
+ * plain JSON artifact at build time (`dist/canon-verbs.schema.json`) so
14
+ * non-TypeScript consumers (the Python hermes plugin, external integrators)
15
+ * can consume the identical contract.
16
+ *
17
+ * Deliberately NOT verbs: replying in the active conversation, streaming
18
+ * partials, typing, and read receipts stay host-mediated — the model talks
19
+ * and the platform delivers. Still out of scope pending their own design
20
+ * pass: plan approval (the fourth runtime-interaction kind — coding-host
21
+ * concern today) and block/mute. Their REST surfaces remain directly
22
+ * callable.
23
+ *
24
+ * Scope note: this module DESCRIBES the contract (canonical shapes + the
25
+ * server-enforced limits, with enforcement sites cited). Enforcement itself
26
+ * stays where it runs today — functions/src. Server-side verb endpoints that
27
+ * validate against these schemas are a planned follow-up.
28
+ *
29
+ * Normativity: the JSON Schemas in `verbSchemas.ts` (and the emitted
30
+ * canon-verbs.schema.json) are the normative contract. The TypeScript types
31
+ * here are a convenience projection — corrections flow schema -> type, and
32
+ * the dual-witness fixtures in verbContract.test.ts (each fixture is both
33
+ * compile-checked against the type and validated against the schema) guard
34
+ * the two from drifting. Single-sourcing the types from the schemas is a
35
+ * planned follow-up.
36
+ *
37
+ * Byte-sensitive limits: JSON Schema `maxLength` counts UTF-16 code units,
38
+ * but several server limits count UTF-8 bytes or serialized-JSON length,
39
+ * which no standard keyword expresses. Bindings MUST run
40
+ * `findVerbByteLimitViolations()` (or equivalent checks from the emitted
41
+ * canon-verbs.limits.json) after schema validation.
42
+ */
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.CANON_VERB_LIMITS_ARTIFACT = exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = exports.CANON_VERB_NAMES = exports.SELF_CONTEXT_TYPE = exports.VERB_RATE_LIMITS = exports.VERB_LIMITS = exports.VERB_ID_PATTERNS = exports.CANON_CARD_SCHEMA_ID = exports.CANON_VERB_NAMESPACE = exports.CANON_VERBS_SCHEMA_ID = exports.CANON_VERBS_SCHEMA_VERSION = void 0;
45
+ exports.canonVerbToolName = canonVerbToolName;
46
+ exports.findVerbByteLimitViolations = findVerbByteLimitViolations;
47
+ /** Identifier for this contract document. */
48
+ exports.CANON_VERBS_SCHEMA_VERSION = 'canon.verbs.v1';
49
+ /** $id of the emitted JSON Schema bundle. */
50
+ exports.CANON_VERBS_SCHEMA_ID = 'https://canonmsg.com/schemas/canon.verbs.v1.json';
51
+ /** Namespace prefix bindings should use for flat tool names (e.g. `canon_send_to`). */
52
+ exports.CANON_VERB_NAMESPACE = 'canon';
53
+ /**
54
+ * $id of the canonical canon.card.v1 document schema
55
+ * (@canonmsg/rich-cards RUNTIME_CARD_JSON_SCHEMA_V1). The verbs bundle only
56
+ * validates the card ENVELOPE; compose the full document schema into card
57
+ * verbs via `getVerbInputSchema(verb, { cardSchema })`.
58
+ */
59
+ exports.CANON_CARD_SCHEMA_ID = 'https://canonmsg.com/schemas/canon.card.v1.json';
60
+ /**
61
+ * Identifier patterns, mirrored from the enforcing sites:
62
+ * - RUNTIME_ID_PATTERN: conversation/input/approval ids —
63
+ * functions/src/utils/runtimeRequestHelpers.ts (`/^[A-Za-z0-9_.:-]{1,160}$/`)
64
+ * - CARD_ID_PATTERN / ACTION_ID_PATTERN: functions/src/api/interactionCard.ts
65
+ * and @canonmsg/rich-cards RUNTIME_CARD_ACTION_ID_PATTERN (80 chars)
66
+ * - QUESTION_ID_PATTERN: functions/src/api/interactionInput.ts (120 chars)
67
+ * - SESSION_RULE_TOOL_PATTERN: functions/src/callable/respondToInteraction.ts
68
+ */
69
+ exports.VERB_ID_PATTERNS = {
70
+ runtimeId: '^[A-Za-z0-9_.:-]{1,160}$',
71
+ cardId: '^[A-Za-z0-9_.:-]{1,80}$',
72
+ actionId: '^[A-Za-z0-9_.:-]{1,80}$',
73
+ questionId: '^[A-Za-z0-9_.:-]{1,120}$',
74
+ sessionRuleToolPattern: '^[\\w.*:-]{1,128}$',
75
+ };
76
+ /**
77
+ * Server-enforced limits, single-sourced. Each value cites its enforcement
78
+ * site; keep the citation current when a limit moves.
79
+ */
80
+ exports.VERB_LIMITS = {
81
+ /** Message text — UTF-8 bytes (functions/src/api/sendMessage.ts MAX_MESSAGE_TEXT_BYTES). */
82
+ messageTextBytes: 4096,
83
+ /**
84
+ * Serialized metadata JSON length in UTF-16 code units — the server checks
85
+ * JSON.stringify(metadata).length, not bytes (sendMessage.ts:716-724 inline;
86
+ * parseBody.ts maxJsonBytes despite the name).
87
+ */
88
+ messageMetadataJsonChars: 4096,
89
+ /** Attachments per message (sendMessage.ts MAX_MESSAGE_ATTACHMENTS). */
90
+ messageAttachments: 10,
91
+ /** Client-supplied messageId — chars and UTF-8 bytes (sendMessage.ts). */
92
+ messageIdChars: 160,
93
+ messageIdBytes: 256,
94
+ /** Self-context note (functions/src/utils/selfContexts.ts SELF_CONTEXT_CONTEXT_LIMIT). */
95
+ selfContextChars: 1000,
96
+ /**
97
+ * Contact-request note. Values longer than this are truncated by senders
98
+ * (slice(0,497)+'...') — the truncation is currently triplicated in
99
+ * functions/src/api/sendContextualMessage.ts, packages/core/src/reach-out.ts
100
+ * and the hermes plugin; this constant is the canonical figure.
101
+ */
102
+ contactRequestNoteChars: 500,
103
+ /** Group membership cap incl. creator (functions/src/utils/conversations.ts MAX_GROUP_MEMBERS). */
104
+ groupMembers: 50,
105
+ /** Runtime input (functions/src/api/interactionInput.ts). */
106
+ inputTitleChars: 160,
107
+ inputPromptChars: 4000,
108
+ inputChoices: 12,
109
+ inputChoiceLabelChars: 120,
110
+ inputChoiceValueChars: 200,
111
+ inputChoiceDescriptionChars: 300,
112
+ inputQuestions: 12,
113
+ inputQuestionChars: 1000,
114
+ inputQuestionHeaderChars: 120,
115
+ inputSecretNameChars: 160,
116
+ inputAnswerChars: 8192,
117
+ /** Approval (functions/src/api/interactionApproval.ts). */
118
+ toolNameChars: 128,
119
+ toolSummaryChars: 1000,
120
+ approvalDetails: 8,
121
+ approvalDetailLabelChars: 80,
122
+ approvalDetailValueChars: 500,
123
+ diffFiles: 100,
124
+ diffPathChars: 1024,
125
+ diffFileBytes: 24 * 1024,
126
+ diffTotalBytes: 96 * 1024,
127
+ /** Card envelope acceptance (functions/src/api/interactionCard.ts — server caps;
128
+ * authoring caps in @canonmsg/rich-cards are stricter: title 120, fallback 500, blocks 24). */
129
+ cardEnvelopeBytes: 32 * 1024,
130
+ cardServerTitleChars: 200,
131
+ cardServerFallbackTextChars: 2000,
132
+ cardServerBlocks: 64,
133
+ /** Response-values caps enforced on submit (callable respondToInteraction.ts
134
+ * MAX_VALUES_BYTES/MAX_VALUES_DEPTH; interactionCard.ts re-checks bytes on
135
+ * consume via MAX_REPLY_BYTES). */
136
+ cardValuesBytes: 8 * 1024,
137
+ cardValuesDepth: 8,
138
+ /** Native correlation metadata (functions/src/api/interactionKinds.ts normalizeNative). */
139
+ nativeKeys: 24,
140
+ nativeValueChars: 256,
141
+ nativeHandles: 16,
142
+ /** Deadlines (functions/src/utils/runtimeRequestHelpers.ts). */
143
+ minTimeoutMs: 1000,
144
+ /** input/card/plan ceiling (30 minutes). */
145
+ maxTimeoutMs: 30 * 60 * 1000,
146
+ /** approval-only ceiling (72 hours — owner ruling 2026-07-10). */
147
+ maxApprovalTimeoutMs: 72 * 60 * 60 * 1000,
148
+ /** turnId / runtimeId fields on interaction creators. */
149
+ turnIdChars: 128,
150
+ /** Reaction key (functions/src/api/reactToMessage.ts normalizeReactionKey). */
151
+ reactionEmojiChars: 64,
152
+ /**
153
+ * Group name authoring cap (enforced on rename via updateNameServer; group
154
+ * CREATE does not length-check today — treat as the authoring contract).
155
+ */
156
+ groupNameChars: 100,
157
+ };
158
+ /**
159
+ * Sender-side rate limits enforced by POST /messages/send
160
+ * (functions/src/api/sendMessage.ts). Bindings should surface 429s with the
161
+ * retryAfter the server returns rather than re-deriving these.
162
+ */
163
+ exports.VERB_RATE_LIMITS = {
164
+ senderMessagesPer5Min: 300,
165
+ conversationMessagesPer5Min: 120,
166
+ agentPeerMessagesPerHour: 30,
167
+ };
168
+ /** The one self-context type the platform accepts today. */
169
+ exports.SELF_CONTEXT_TYPE = 'cross_session';
170
+ /** Canonical verb names. */
171
+ exports.CANON_VERB_NAMES = [
172
+ 'send_to',
173
+ 'request_input',
174
+ 'request_approval',
175
+ 'check_approval',
176
+ 'send_card',
177
+ 'request_card',
178
+ 'share_contact',
179
+ 'react',
180
+ 'forward',
181
+ 'create_group',
182
+ 'add_member',
183
+ 'remove_member',
184
+ 'leave_conversation',
185
+ 'list_contacts',
186
+ 'list_contact_requests',
187
+ 'list_conversations',
188
+ ];
189
+ /**
190
+ * Error code on the 400 a create_group receives when NO member is directly
191
+ * addable (approval-required members can only be invited to an existing
192
+ * group; a creator-only group is not created). The error detail carries the
193
+ * partition: pendingRequired[] and skipped[].
194
+ */
195
+ exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = 'CREATE_GROUP_NO_ADDABLE_MEMBERS';
196
+ /**
197
+ * Error code on the 403 a create_group receives when the CREATOR itself is
198
+ * a coding agent requiring explicit session setup: the group helper prepares
199
+ * session configs for every coding-agent member including the creator, and
200
+ * only the agent's human owner may provide them (v1 scoping — the owner
201
+ * creates the group from the app instead).
202
+ */
203
+ exports.CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = 'CREATE_GROUP_CREATOR_SETUP_REQUIRED';
204
+ /** Flat tool name for a verb on namespaced tool surfaces (e.g. `canon_send_to`). */
205
+ function canonVerbToolName(verb) {
206
+ return `${exports.CANON_VERB_NAMESPACE}_${verb}`;
207
+ }
208
+ const utf8Bytes = (value) => new TextEncoder().encode(value).length;
209
+ function checkTextBytes(path, value, into) {
210
+ if (typeof value === 'string' && utf8Bytes(value) > exports.VERB_LIMITS.messageTextBytes) {
211
+ into.push({
212
+ path,
213
+ message: `text exceeds ${exports.VERB_LIMITS.messageTextBytes} UTF-8 bytes (sendMessage.ts MAX_MESSAGE_TEXT_BYTES)`,
214
+ });
215
+ }
216
+ }
217
+ function checkMessageId(path, value, into) {
218
+ if (typeof value === 'string' && utf8Bytes(value) > exports.VERB_LIMITS.messageIdBytes) {
219
+ into.push({
220
+ path,
221
+ message: `messageId exceeds ${exports.VERB_LIMITS.messageIdBytes} UTF-8 bytes (sendMessage.ts MAX_MESSAGE_ID_BYTES)`,
222
+ });
223
+ }
224
+ }
225
+ /**
226
+ * The server limits JSON Schema cannot express: UTF-8 byte caps and
227
+ * serialized-JSON length caps. Bindings MUST run this after schema
228
+ * validation; the server enforces the same checks with 400s.
229
+ */
230
+ function findVerbByteLimitViolations(verb, input) {
231
+ const violations = [];
232
+ if (verb === 'send_to' || verb === 'share_contact' || verb === 'forward') {
233
+ checkTextBytes('text', input.text, violations);
234
+ }
235
+ if (verb === 'share_contact') {
236
+ checkMessageId('messageId', input.messageId, violations);
237
+ }
238
+ if (verb === 'send_to') {
239
+ const options = input.messageOptions;
240
+ if (options && typeof options === 'object' && !Array.isArray(options)) {
241
+ const record = options;
242
+ checkMessageId('messageOptions.messageId', record.messageId, violations);
243
+ if (record.metadata !== undefined) {
244
+ const serialized = JSON.stringify(record.metadata);
245
+ if (typeof serialized === 'string' && serialized.length > exports.VERB_LIMITS.messageMetadataJsonChars) {
246
+ violations.push({
247
+ path: 'messageOptions.metadata',
248
+ message: `metadata serializes to more than ${exports.VERB_LIMITS.messageMetadataJsonChars} JSON characters `
249
+ + '(sendMessage.ts checks JSON.stringify length)',
250
+ });
251
+ }
252
+ }
253
+ }
254
+ }
255
+ if (verb === 'send_card' || verb === 'request_card') {
256
+ if (input.card !== undefined) {
257
+ const serialized = JSON.stringify(input.card);
258
+ if (typeof serialized === 'string' && utf8Bytes(serialized) > exports.VERB_LIMITS.cardEnvelopeBytes) {
259
+ violations.push({
260
+ path: 'card',
261
+ message: `card serializes to more than ${exports.VERB_LIMITS.cardEnvelopeBytes} bytes (interactionCard.ts MAX_CARD_BYTES)`,
262
+ });
263
+ }
264
+ }
265
+ }
266
+ return violations;
267
+ }
268
+ /**
269
+ * Machine-readable limits companion to the schema bundle, emitted as
270
+ * dist/canon-verbs.limits.json so non-TypeScript bindings can apply the
271
+ * byte-sensitive checks JSON Schema cannot express.
272
+ */
273
+ exports.CANON_VERB_LIMITS_ARTIFACT = {
274
+ schemaVersion: exports.CANON_VERBS_SCHEMA_VERSION,
275
+ limits: exports.VERB_LIMITS,
276
+ rateLimits: exports.VERB_RATE_LIMITS,
277
+ idPatterns: exports.VERB_ID_PATTERNS,
278
+ byteSemantics: {
279
+ 'send_to.text': 'utf8_bytes<=messageTextBytes',
280
+ 'share_contact.text': 'utf8_bytes<=messageTextBytes',
281
+ 'forward.text': 'utf8_bytes<=messageTextBytes',
282
+ 'send_to.messageOptions.messageId': 'utf8_bytes<=messageIdBytes',
283
+ 'share_contact.messageId': 'utf8_bytes<=messageIdBytes',
284
+ 'send_to.messageOptions.metadata': 'json_stringify_chars<=messageMetadataJsonChars',
285
+ 'send_card.card': 'json_utf8_bytes<=cardEnvelopeBytes',
286
+ 'request_card.card': 'json_utf8_bytes<=cardEnvelopeBytes',
287
+ },
288
+ };