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