@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/canon-verb-wire.schema.json +367 -0
- package/dist/canon-verbs.limits.json +70 -0
- package/dist/canon-verbs.schema.json +1744 -0
- package/dist/cjs/index.js +3 -0
- package/dist/cjs/verbContract.js +288 -0
- package/dist/cjs/verbSchemas.js +1144 -0
- package/dist/cjs/verbWire.js +634 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/verbContract.d.ts +760 -0
- package/dist/verbContract.js +283 -0
- package/dist/verbSchemas.d.ts +1460 -0
- package/dist/verbSchemas.js +1139 -0
- package/dist/verbWire.d.ts +405 -0
- package/dist/verbWire.js +628 -0
- package/package.json +8 -3
|
@@ -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
|
+
};
|