@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,760 @@
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 declare const CANON_VERBS_SCHEMA_VERSION = "canon.verbs.v1";
44
+ /** $id of the emitted JSON Schema bundle. */
45
+ export declare 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 declare 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 declare 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 declare const VERB_ID_PATTERNS: {
65
+ readonly runtimeId: "^[A-Za-z0-9_.:-]{1,160}$";
66
+ readonly cardId: "^[A-Za-z0-9_.:-]{1,80}$";
67
+ readonly actionId: "^[A-Za-z0-9_.:-]{1,80}$";
68
+ readonly questionId: "^[A-Za-z0-9_.:-]{1,120}$";
69
+ readonly 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 declare const VERB_LIMITS: {
76
+ /** Message text — UTF-8 bytes (functions/src/api/sendMessage.ts MAX_MESSAGE_TEXT_BYTES). */
77
+ readonly 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
+ readonly messageMetadataJsonChars: 4096;
84
+ /** Attachments per message (sendMessage.ts MAX_MESSAGE_ATTACHMENTS). */
85
+ readonly messageAttachments: 10;
86
+ /** Client-supplied messageId — chars and UTF-8 bytes (sendMessage.ts). */
87
+ readonly messageIdChars: 160;
88
+ readonly messageIdBytes: 256;
89
+ /** Self-context note (functions/src/utils/selfContexts.ts SELF_CONTEXT_CONTEXT_LIMIT). */
90
+ readonly 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
+ readonly contactRequestNoteChars: 500;
98
+ /** Group membership cap incl. creator (functions/src/utils/conversations.ts MAX_GROUP_MEMBERS). */
99
+ readonly groupMembers: 50;
100
+ /** Runtime input (functions/src/api/interactionInput.ts). */
101
+ readonly inputTitleChars: 160;
102
+ readonly inputPromptChars: 4000;
103
+ readonly inputChoices: 12;
104
+ readonly inputChoiceLabelChars: 120;
105
+ readonly inputChoiceValueChars: 200;
106
+ readonly inputChoiceDescriptionChars: 300;
107
+ readonly inputQuestions: 12;
108
+ readonly inputQuestionChars: 1000;
109
+ readonly inputQuestionHeaderChars: 120;
110
+ readonly inputSecretNameChars: 160;
111
+ readonly inputAnswerChars: 8192;
112
+ /** Approval (functions/src/api/interactionApproval.ts). */
113
+ readonly toolNameChars: 128;
114
+ readonly toolSummaryChars: 1000;
115
+ readonly approvalDetails: 8;
116
+ readonly approvalDetailLabelChars: 80;
117
+ readonly approvalDetailValueChars: 500;
118
+ readonly diffFiles: 100;
119
+ readonly diffPathChars: 1024;
120
+ readonly diffFileBytes: number;
121
+ readonly diffTotalBytes: number;
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
+ readonly cardEnvelopeBytes: number;
125
+ readonly cardServerTitleChars: 200;
126
+ readonly cardServerFallbackTextChars: 2000;
127
+ readonly 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
+ readonly cardValuesBytes: number;
132
+ readonly cardValuesDepth: 8;
133
+ /** Native correlation metadata (functions/src/api/interactionKinds.ts normalizeNative). */
134
+ readonly nativeKeys: 24;
135
+ readonly nativeValueChars: 256;
136
+ readonly nativeHandles: 16;
137
+ /** Deadlines (functions/src/utils/runtimeRequestHelpers.ts). */
138
+ readonly minTimeoutMs: 1000;
139
+ /** input/card/plan ceiling (30 minutes). */
140
+ readonly maxTimeoutMs: number;
141
+ /** approval-only ceiling (72 hours — owner ruling 2026-07-10). */
142
+ readonly maxApprovalTimeoutMs: number;
143
+ /** turnId / runtimeId fields on interaction creators. */
144
+ readonly turnIdChars: 128;
145
+ /** Reaction key (functions/src/api/reactToMessage.ts normalizeReactionKey). */
146
+ readonly 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
+ readonly 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 declare const VERB_RATE_LIMITS: {
159
+ readonly senderMessagesPer5Min: 300;
160
+ readonly conversationMessagesPer5Min: 120;
161
+ readonly agentPeerMessagesPerHour: 30;
162
+ };
163
+ /** The one self-context type the platform accepts today. */
164
+ export declare const SELF_CONTEXT_TYPE = "cross_session";
165
+ /** Canonical verb names. */
166
+ export declare const CANON_VERB_NAMES: readonly ["send_to", "request_input", "request_approval", "check_approval", "send_card", "request_card", "share_contact", "react", "forward", "create_group", "add_member", "remove_member", "leave_conversation", "list_contacts", "list_contact_requests", "list_conversations"];
167
+ export type CanonVerbName = (typeof CANON_VERB_NAMES)[number];
168
+ /** Private note-to-self attached to a cross-conversation send. */
169
+ export interface VerbSelfContext {
170
+ type: typeof SELF_CONTEXT_TYPE;
171
+ /** <= VERB_LIMITS.selfContextChars. Visible only to the sending agent. */
172
+ context: string;
173
+ }
174
+ export type VerbSessionSelection = {
175
+ mode: 'new';
176
+ } | {
177
+ mode: 'continue_latest';
178
+ } | {
179
+ mode: 'continue_or_create';
180
+ } | {
181
+ mode: 'specific';
182
+ conversationId: string;
183
+ };
184
+ export interface VerbMediaAttachment {
185
+ kind: 'image' | 'audio' | 'video' | 'file';
186
+ /** Must come from /media/upload (Canon storage) or the GIF picker — server allowlisted. */
187
+ url: string;
188
+ mimeType?: string;
189
+ fileName?: string;
190
+ sizeBytes?: number;
191
+ width?: number;
192
+ height?: number;
193
+ durationMs?: number;
194
+ }
195
+ /**
196
+ * Well-known turn-protocol keys inside the free-form metadata envelope
197
+ * (see turnProtocol.ts TurnMetadata — the authoritative type).
198
+ */
199
+ export interface VerbTurnMetadata {
200
+ turnId?: string | null;
201
+ turnSemantics?: 'progress' | 'turn_complete' | 'control';
202
+ deliveryIntent?: 'queue' | 'interrupt' | 'interleave' | 'stop';
203
+ replyBehavior?: 'allow_auto_reply' | 'suppress_auto_reply';
204
+ [key: string]: unknown;
205
+ }
206
+ /** Message composition options shared by send_to (mirrors POST /messages/send). */
207
+ export interface VerbMessageOptions {
208
+ messageId?: string;
209
+ contentType?: 'text' | 'image' | 'audio' | 'video' | 'file';
210
+ attachments?: VerbMediaAttachment[];
211
+ mentions?: string[];
212
+ replyTo?: string;
213
+ replyToPosition?: number;
214
+ metadata?: VerbTurnMetadata;
215
+ }
216
+ /**
217
+ * Message another conversation or user (admission-aware reach-out), optionally
218
+ * carrying a private self-context so the agent recognizes its own transfer in
219
+ * both sessions. Merges today's reach_out / send_contextual_message /
220
+ * cross-conversation send into one verb.
221
+ */
222
+ export interface SendToInput {
223
+ /** Exactly one of targetConversationId / targetUserId / canonContactId. */
224
+ targetConversationId?: string;
225
+ targetUserId?: string;
226
+ /**
227
+ * Contact-card identity. NOT a wire field on any send endpoint: bindings
228
+ * resolve it to a targetUserId via POST /admission/resolve first (the
229
+ * two-step core reachOutToCanonContact runs).
230
+ */
231
+ canonContactId?: string;
232
+ /** Required unless messageOptions.attachments carries the content. */
233
+ text?: string;
234
+ /**
235
+ * The conversation this send originates from. Required when selfContext is
236
+ * present (the self-context links source -> target).
237
+ */
238
+ sourceConversationId?: string;
239
+ selfContext?: VerbSelfContext;
240
+ /** Contact-request note when admission requires approval; defaults to text. */
241
+ requestMessage?: string;
242
+ /** Only meaningful for user targets that are agents. Default: continue_or_create. */
243
+ sessionSelection?: VerbSessionSelection;
244
+ /** Coding-agent session setup; only applied when the target is an agent. */
245
+ sessionConfig?: Record<string, unknown> | null;
246
+ messageOptions?: VerbMessageOptions;
247
+ }
248
+ export type SendToResult = {
249
+ status: 'messaged';
250
+ conversationId: string;
251
+ messageId?: string;
252
+ selfContextId?: string;
253
+ created?: boolean;
254
+ reused?: boolean;
255
+ sessionSelection?: string;
256
+ } | {
257
+ status: 'requested';
258
+ requestId: string | null;
259
+ deferredIntentId?: string | null;
260
+ } | {
261
+ status: 'pending';
262
+ requestId: string | null;
263
+ deferredIntentId?: string | null;
264
+ } | {
265
+ status: 'setup_required';
266
+ reason: string;
267
+ } | {
268
+ status: 'no_session';
269
+ reason: string;
270
+ } | {
271
+ status: 'blocked';
272
+ reason: string;
273
+ } | {
274
+ status: 'unavailable';
275
+ reason: string;
276
+ };
277
+ export type VerbInputKind = 'clarify' | 'sudo' | 'secret';
278
+ export interface VerbInputChoice {
279
+ label: string;
280
+ value?: string;
281
+ description?: string;
282
+ }
283
+ export interface VerbInputQuestion {
284
+ id: string;
285
+ question: string;
286
+ header?: string;
287
+ choices?: VerbInputChoice[];
288
+ allowOther?: boolean;
289
+ isSecret?: boolean;
290
+ multiSelect?: boolean;
291
+ }
292
+ /**
293
+ * Ask a human a structured question mid-turn (HITL input card). `sudo`,
294
+ * `secret`, `sensitive:true`, and any `isSecret` question force owner-only
295
+ * routing server-side; bindings must not let the model redirect the responder
296
+ * for those.
297
+ */
298
+ export interface RequestInputInput {
299
+ /** Bindings default this to the active conversation. */
300
+ conversationId?: string;
301
+ /** Durable single-use id; generated by the binding when omitted. */
302
+ inputId?: string;
303
+ kind?: VerbInputKind;
304
+ title?: string;
305
+ prompt?: string;
306
+ choices?: VerbInputChoice[];
307
+ questions?: VerbInputQuestion[];
308
+ secretName?: string;
309
+ sensitive?: boolean;
310
+ responseUserId?: string;
311
+ native?: Record<string, unknown>;
312
+ turnId?: string;
313
+ /** Relative deadline; server ceiling is VERB_LIMITS.maxTimeoutMs (30 min). */
314
+ timeoutMs?: number;
315
+ /** Absolute epoch-ms deadline; wins over timeoutMs when both given. */
316
+ expiresAt?: number;
317
+ }
318
+ export type RequestInputResult = {
319
+ status: 'submitted';
320
+ inputId: string;
321
+ value: string;
322
+ answers?: Record<string, {
323
+ answers: string[];
324
+ }>;
325
+ } | {
326
+ status: 'cancelled';
327
+ inputId: string;
328
+ } | {
329
+ status: 'timeout';
330
+ inputId: string;
331
+ };
332
+ export type VerbApprovalRisk = 'low' | 'normal' | 'high' | 'destructive';
333
+ export type VerbApprovalCategory = 'command' | 'file' | 'network' | 'browser' | 'mcp' | 'plugin' | 'canon' | 'tool';
334
+ export interface VerbApprovalDetail {
335
+ label: string;
336
+ value: string;
337
+ monospace?: boolean;
338
+ }
339
+ export interface VerbUnifiedDiffFile {
340
+ path: string;
341
+ status: 'modified' | 'created' | 'deleted' | 'renamed';
342
+ oldPath?: string;
343
+ additions?: number;
344
+ deletions?: number;
345
+ diff?: string;
346
+ suppressed?: boolean;
347
+ }
348
+ export interface VerbUnifiedDiff {
349
+ files: VerbUnifiedDiffFile[];
350
+ truncated?: boolean;
351
+ }
352
+ export interface VerbSessionRule {
353
+ type: 'approve-all' | 'approve-tool' | 'deny-tool';
354
+ toolPattern?: string;
355
+ expiresAt?: string | null;
356
+ }
357
+ /**
358
+ * Ask a human to allow or deny an action. `mode: 'blocking'` waits for the
359
+ * decision inside the verb call (default timeout 5 min); `mode: 'detached'`
360
+ * returns `pending` immediately and the decision is fetched later with
361
+ * check_approval (ceiling 72h). Timeouts fail closed to deny.
362
+ *
363
+ * Binding-local options are deliberately not wire fields: session-rule caches
364
+ * and their bypass (agent-sdk/core `ignoreSessionRules`) live in the binding,
365
+ * never on the wire — the server has no such field.
366
+ */
367
+ export interface RequestApprovalInput {
368
+ conversationId?: string;
369
+ /** Server-generated when omitted. Durable single-use id. */
370
+ approvalId?: string;
371
+ toolName: string;
372
+ toolSummary: string;
373
+ mode?: 'blocking' | 'detached';
374
+ riskLevel?: 'normal' | 'destructive';
375
+ risk?: VerbApprovalRisk;
376
+ category?: VerbApprovalCategory;
377
+ details?: VerbApprovalDetail[];
378
+ diff?: VerbUnifiedDiff;
379
+ native?: Record<string, unknown>;
380
+ runtimeId?: string;
381
+ turnId?: string;
382
+ responseUserId?: string;
383
+ /** Server forces false when the responder is not the agent owner. */
384
+ allowSessionRule?: boolean;
385
+ timeoutMs?: number;
386
+ expiresAt?: number;
387
+ }
388
+ export type RequestApprovalResult = {
389
+ status: 'allow';
390
+ approvalId: string;
391
+ sessionRule?: VerbSessionRule;
392
+ respondedBy?: string;
393
+ } | {
394
+ status: 'deny';
395
+ approvalId: string;
396
+ sessionRule?: VerbSessionRule;
397
+ respondedBy?: string;
398
+ } | {
399
+ status: 'timeout';
400
+ approvalId: string;
401
+ } | {
402
+ status: 'pending';
403
+ approvalId: string;
404
+ conversationId?: string;
405
+ expiresAt: number;
406
+ responseUserId?: string;
407
+ };
408
+ export interface CheckApprovalInput {
409
+ approvalId: string;
410
+ /** Bindings must bind checks to the conversation that created the approval. */
411
+ conversationId?: string;
412
+ }
413
+ /**
414
+ * `unknown` is NOT a denial — it means the id is unrecognized (consumed
415
+ * tombstone expired, or foreign id). Callers must treat only `resolved` with
416
+ * `decision` as an answer.
417
+ */
418
+ export type CheckApprovalResult = {
419
+ status: 'resolved';
420
+ approvalId: string;
421
+ decision: 'allow' | 'deny';
422
+ respondedBy?: string;
423
+ conversationId?: string;
424
+ } | {
425
+ status: 'pending';
426
+ approvalId: string;
427
+ expiresAt?: number;
428
+ conversationId?: string;
429
+ } | {
430
+ status: 'expired';
431
+ approvalId: string;
432
+ conversationId?: string;
433
+ } | {
434
+ status: 'unknown';
435
+ approvalId: string;
436
+ };
437
+ /**
438
+ * A canon.card.v1 document. The full document contract is
439
+ * @canonmsg/rich-cards RUNTIME_CARD_JSON_SCHEMA_V1
440
+ * ($id https://canonmsg.com/schemas/canon.card.v1.json) — authoring caps
441
+ * title 120 / fallbackText 500 / blocks 24; the server envelope accepts up to
442
+ * 200 / 2000 / 64 and 32 KiB total (interactionCard.ts validateCardEnvelope).
443
+ */
444
+ export interface VerbRuntimeCard {
445
+ schema: 'canon.card.v1';
446
+ cardId?: string;
447
+ title: string;
448
+ fallbackText: string;
449
+ /** At least one block (schema enforces minItems: 1). */
450
+ blocks: unknown[];
451
+ [key: string]: unknown;
452
+ }
453
+ /** Display a card with no actions — fire-and-forget, no pending state. */
454
+ export interface SendCardInput {
455
+ conversationId?: string;
456
+ card: VerbRuntimeCard;
457
+ cardId?: string;
458
+ native?: Record<string, unknown>;
459
+ runtimeId?: string;
460
+ turnId?: string;
461
+ }
462
+ export interface SendCardResult {
463
+ status: 'displayed';
464
+ cardId: string;
465
+ conversationId?: string;
466
+ responseUserId?: string;
467
+ }
468
+ /** Show an interactive card (>=1 actions block) and wait for the response. */
469
+ export interface RequestCardInput extends SendCardInput {
470
+ responseUserId?: string;
471
+ timeoutMs?: number;
472
+ expiresAt?: number;
473
+ }
474
+ export type RequestCardResult = {
475
+ status: 'submitted';
476
+ cardId: string;
477
+ actionId?: string;
478
+ values?: Record<string, unknown>;
479
+ /** Canon-authenticated responder (server-verified against responseUserId). */
480
+ respondedBy?: string;
481
+ } | {
482
+ status: 'cancelled';
483
+ cardId: string;
484
+ respondedBy?: string;
485
+ } | {
486
+ status: 'timeout';
487
+ cardId: string;
488
+ };
489
+ /**
490
+ * Share a contact card into a conversation (POST /messages/send with
491
+ * contentType 'contact_card'). The shared user must be in the sending
492
+ * agent's contacts (403 otherwise); the owner shortcut in sendMessage.ts
493
+ * applies only to human senders sharing their own agents. The server
494
+ * snapshots the card.
495
+ */
496
+ export interface ShareContactInput {
497
+ conversationId: string;
498
+ contactUserId: string;
499
+ text?: string;
500
+ messageId?: string;
501
+ }
502
+ export interface ShareContactResult {
503
+ status: 'shared';
504
+ messageId: string;
505
+ }
506
+ /** Toggle an emoji reaction on a message (idempotent toggle server-side). */
507
+ export interface ReactInput {
508
+ conversationId: string;
509
+ messageId: string;
510
+ /** <= VERB_LIMITS.reactionEmojiChars after normalization. Content-adjacent
511
+ * (open decision D5: MLS messengers encrypt reactions) — rides the body. */
512
+ emoji: string;
513
+ }
514
+ export interface ReactResult {
515
+ status: 'reacted';
516
+ action: 'added' | 'removed';
517
+ reactions?: Record<string, unknown>;
518
+ }
519
+ /** Forward an existing message into another conversation. Under E2EE this
520
+ * becomes a client-side re-encrypt; the wire verb carries only routing ids
521
+ * plus an optional caption. */
522
+ export interface ForwardInput {
523
+ sourceConversationId: string;
524
+ targetConversationId: string;
525
+ messageId: string;
526
+ /** Optional caption; <= VERB_LIMITS.messageTextBytes UTF-8 bytes. */
527
+ text?: string;
528
+ }
529
+ export interface ForwardResult {
530
+ status: 'forwarded';
531
+ messageId: string;
532
+ targetConversationId: string;
533
+ forwardedFrom?: unknown;
534
+ }
535
+ export interface CreateGroupInput {
536
+ /** Group name (authoring cap VERB_LIMITS.groupNameChars). */
537
+ name: string;
538
+ /** Other members; the caller is added automatically. Cap
539
+ * VERB_LIMITS.groupMembers including the creator. Each target's
540
+ * groupJoinPolicy is enforced server-side. */
541
+ memberIds: string[];
542
+ }
543
+ export interface CreateGroupResult {
544
+ status: 'created';
545
+ conversationId: string;
546
+ /** approval-required members — group_invite requests awaiting their approver. */
547
+ pending?: Array<{
548
+ userId: string;
549
+ requestId: string;
550
+ }>;
551
+ /** members the staged create could not admit: policy denials (owner-only,
552
+ * blocked, not-found, …) and setup-required coding agents (v1 scoping —
553
+ * only their owner can provide session setup, from the app). */
554
+ skipped?: Array<{
555
+ userId: string;
556
+ reason: string;
557
+ }>;
558
+ }
559
+ /**
560
+ * Error code on the 400 a create_group receives when NO member is directly
561
+ * addable (approval-required members can only be invited to an existing
562
+ * group; a creator-only group is not created). The error detail carries the
563
+ * partition: pendingRequired[] and skipped[].
564
+ */
565
+ export declare const CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = "CREATE_GROUP_NO_ADDABLE_MEMBERS";
566
+ /**
567
+ * Error code on the 403 a create_group receives when the CREATOR itself is
568
+ * a coding agent requiring explicit session setup: the group helper prepares
569
+ * session configs for every coding-agent member including the creator, and
570
+ * only the agent's human owner may provide them (v1 scoping — the owner
571
+ * creates the group from the app instead).
572
+ */
573
+ export declare const CREATE_GROUP_CREATOR_SETUP_REQUIRED_CODE = "CREATE_GROUP_CREATOR_SETUP_REQUIRED";
574
+ export interface AddMemberInput {
575
+ conversationId: string;
576
+ userId: string;
577
+ }
578
+ /** approval-required targets yield a pending group_invite contact request. */
579
+ export type AddMemberResult = {
580
+ status: 'added';
581
+ } | {
582
+ status: 'pending';
583
+ requestId: string;
584
+ };
585
+ export interface RemoveMemberInput {
586
+ conversationId: string;
587
+ userId: string;
588
+ }
589
+ export interface RemoveMemberResult {
590
+ status: 'removed';
591
+ }
592
+ export interface LeaveConversationInput {
593
+ conversationId: string;
594
+ }
595
+ export interface LeaveConversationResult {
596
+ status: 'left';
597
+ }
598
+ export type ListContactsInput = Record<string, never>;
599
+ export interface VerbContact {
600
+ /** The contact's userId. */
601
+ id: string;
602
+ /** Vocabulary: direct_add | phone_book | contact_request | link | qr | group | open_inbound_message | unknown (server passes strings through). */
603
+ source: string;
604
+ addedAt: string | null;
605
+ displayNameOverride: string | null;
606
+ }
607
+ export interface ListContactsResult {
608
+ contacts: VerbContact[];
609
+ }
610
+ export type ListContactRequestsInput = Record<string, never>;
611
+ /** Items are SerializedContactRequest (contactRequest.ts) — pending inbound only, newest first, capped at 100. */
612
+ export interface ListContactRequestsResult {
613
+ requests: unknown[];
614
+ }
615
+ export interface ListConversationsInput {
616
+ /** Optional client-side cap; the REST endpoint returns all memberships. */
617
+ limit?: number;
618
+ }
619
+ export interface VerbConversationSummary {
620
+ id: string;
621
+ type: 'direct' | 'group';
622
+ /** Raw passthrough — may be absent entirely on direct chats. */
623
+ name?: string | null;
624
+ topic: string | null;
625
+ memberIds: string[];
626
+ isAgentChat: boolean;
627
+ hasUnread?: boolean;
628
+ lastMessage: {
629
+ text: string | null;
630
+ messageId?: string;
631
+ senderId: string;
632
+ senderType: string;
633
+ contentType?: string;
634
+ timestamp: string | null;
635
+ } | null;
636
+ createdAt: string | null;
637
+ }
638
+ export interface ListConversationsResult {
639
+ conversations: VerbConversationSummary[];
640
+ }
641
+ /** Flat tool name for a verb on namespaced tool surfaces (e.g. `canon_send_to`). */
642
+ export declare function canonVerbToolName(verb: CanonVerbName): string;
643
+ export interface VerbLimitViolation {
644
+ path: string;
645
+ message: string;
646
+ }
647
+ /**
648
+ * The server limits JSON Schema cannot express: UTF-8 byte caps and
649
+ * serialized-JSON length caps. Bindings MUST run this after schema
650
+ * validation; the server enforces the same checks with 400s.
651
+ */
652
+ export declare function findVerbByteLimitViolations(verb: CanonVerbName, input: Record<string, unknown>): VerbLimitViolation[];
653
+ /**
654
+ * Machine-readable limits companion to the schema bundle, emitted as
655
+ * dist/canon-verbs.limits.json so non-TypeScript bindings can apply the
656
+ * byte-sensitive checks JSON Schema cannot express.
657
+ */
658
+ export declare const CANON_VERB_LIMITS_ARTIFACT: {
659
+ readonly schemaVersion: "canon.verbs.v1";
660
+ readonly limits: {
661
+ /** Message text — UTF-8 bytes (functions/src/api/sendMessage.ts MAX_MESSAGE_TEXT_BYTES). */
662
+ readonly messageTextBytes: 4096;
663
+ /**
664
+ * Serialized metadata JSON length in UTF-16 code units — the server checks
665
+ * JSON.stringify(metadata).length, not bytes (sendMessage.ts:716-724 inline;
666
+ * parseBody.ts maxJsonBytes despite the name).
667
+ */
668
+ readonly messageMetadataJsonChars: 4096;
669
+ /** Attachments per message (sendMessage.ts MAX_MESSAGE_ATTACHMENTS). */
670
+ readonly messageAttachments: 10;
671
+ /** Client-supplied messageId — chars and UTF-8 bytes (sendMessage.ts). */
672
+ readonly messageIdChars: 160;
673
+ readonly messageIdBytes: 256;
674
+ /** Self-context note (functions/src/utils/selfContexts.ts SELF_CONTEXT_CONTEXT_LIMIT). */
675
+ readonly selfContextChars: 1000;
676
+ /**
677
+ * Contact-request note. Values longer than this are truncated by senders
678
+ * (slice(0,497)+'...') — the truncation is currently triplicated in
679
+ * functions/src/api/sendContextualMessage.ts, packages/core/src/reach-out.ts
680
+ * and the hermes plugin; this constant is the canonical figure.
681
+ */
682
+ readonly contactRequestNoteChars: 500;
683
+ /** Group membership cap incl. creator (functions/src/utils/conversations.ts MAX_GROUP_MEMBERS). */
684
+ readonly groupMembers: 50;
685
+ /** Runtime input (functions/src/api/interactionInput.ts). */
686
+ readonly inputTitleChars: 160;
687
+ readonly inputPromptChars: 4000;
688
+ readonly inputChoices: 12;
689
+ readonly inputChoiceLabelChars: 120;
690
+ readonly inputChoiceValueChars: 200;
691
+ readonly inputChoiceDescriptionChars: 300;
692
+ readonly inputQuestions: 12;
693
+ readonly inputQuestionChars: 1000;
694
+ readonly inputQuestionHeaderChars: 120;
695
+ readonly inputSecretNameChars: 160;
696
+ readonly inputAnswerChars: 8192;
697
+ /** Approval (functions/src/api/interactionApproval.ts). */
698
+ readonly toolNameChars: 128;
699
+ readonly toolSummaryChars: 1000;
700
+ readonly approvalDetails: 8;
701
+ readonly approvalDetailLabelChars: 80;
702
+ readonly approvalDetailValueChars: 500;
703
+ readonly diffFiles: 100;
704
+ readonly diffPathChars: 1024;
705
+ readonly diffFileBytes: number;
706
+ readonly diffTotalBytes: number;
707
+ /** Card envelope acceptance (functions/src/api/interactionCard.ts — server caps;
708
+ * authoring caps in @canonmsg/rich-cards are stricter: title 120, fallback 500, blocks 24). */
709
+ readonly cardEnvelopeBytes: number;
710
+ readonly cardServerTitleChars: 200;
711
+ readonly cardServerFallbackTextChars: 2000;
712
+ readonly cardServerBlocks: 64;
713
+ /** Response-values caps enforced on submit (callable respondToInteraction.ts
714
+ * MAX_VALUES_BYTES/MAX_VALUES_DEPTH; interactionCard.ts re-checks bytes on
715
+ * consume via MAX_REPLY_BYTES). */
716
+ readonly cardValuesBytes: number;
717
+ readonly cardValuesDepth: 8;
718
+ /** Native correlation metadata (functions/src/api/interactionKinds.ts normalizeNative). */
719
+ readonly nativeKeys: 24;
720
+ readonly nativeValueChars: 256;
721
+ readonly nativeHandles: 16;
722
+ /** Deadlines (functions/src/utils/runtimeRequestHelpers.ts). */
723
+ readonly minTimeoutMs: 1000;
724
+ /** input/card/plan ceiling (30 minutes). */
725
+ readonly maxTimeoutMs: number;
726
+ /** approval-only ceiling (72 hours — owner ruling 2026-07-10). */
727
+ readonly maxApprovalTimeoutMs: number;
728
+ /** turnId / runtimeId fields on interaction creators. */
729
+ readonly turnIdChars: 128;
730
+ /** Reaction key (functions/src/api/reactToMessage.ts normalizeReactionKey). */
731
+ readonly reactionEmojiChars: 64;
732
+ /**
733
+ * Group name authoring cap (enforced on rename via updateNameServer; group
734
+ * CREATE does not length-check today — treat as the authoring contract).
735
+ */
736
+ readonly groupNameChars: 100;
737
+ };
738
+ readonly rateLimits: {
739
+ readonly senderMessagesPer5Min: 300;
740
+ readonly conversationMessagesPer5Min: 120;
741
+ readonly agentPeerMessagesPerHour: 30;
742
+ };
743
+ readonly idPatterns: {
744
+ readonly runtimeId: "^[A-Za-z0-9_.:-]{1,160}$";
745
+ readonly cardId: "^[A-Za-z0-9_.:-]{1,80}$";
746
+ readonly actionId: "^[A-Za-z0-9_.:-]{1,80}$";
747
+ readonly questionId: "^[A-Za-z0-9_.:-]{1,120}$";
748
+ readonly sessionRuleToolPattern: "^[\\w.*:-]{1,128}$";
749
+ };
750
+ readonly byteSemantics: {
751
+ readonly 'send_to.text': "utf8_bytes<=messageTextBytes";
752
+ readonly 'share_contact.text': "utf8_bytes<=messageTextBytes";
753
+ readonly 'forward.text': "utf8_bytes<=messageTextBytes";
754
+ readonly 'send_to.messageOptions.messageId': "utf8_bytes<=messageIdBytes";
755
+ readonly 'share_contact.messageId': "utf8_bytes<=messageIdBytes";
756
+ readonly 'send_to.messageOptions.metadata': "json_stringify_chars<=messageMetadataJsonChars";
757
+ readonly 'send_card.card': "json_utf8_bytes<=cardEnvelopeBytes";
758
+ readonly 'request_card.card': "json_utf8_bytes<=cardEnvelopeBytes";
759
+ };
760
+ };