@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.
@@ -0,0 +1,1744 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://canonmsg.com/schemas/canon.verbs.v1.json",
4
+ "title": "Canon Agent Verbs v1",
5
+ "description": "Canonical contract for every deliberate agent action against Canon. Each verb has a <verb>_input and <verb>_result definition. Inputs are strict; results are open for forward compatibility. Replies, streaming, typing, and read receipts are deliberately NOT verbs — they stay host-mediated.",
6
+ "$defs": {
7
+ "messageId": {
8
+ "type": "string",
9
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
10
+ "not": {
11
+ "pattern": "^(\\.{1,2}|__.*__)$",
12
+ "description": "Reserved forms the server 400s: '.', '..', and __x__ ids."
13
+ },
14
+ "description": "Client idempotency key (<= 160 chars / 256 UTF-8 bytes; no slashes, control chars, '.', '..', __x__). Same id + identical payload replays idempotently; different payload -> 409 MESSAGE_ID_EXISTS."
15
+ },
16
+ "selfContext": {
17
+ "type": "object",
18
+ "description": "Private note-to-self attached to a cross-conversation send. Visible only to the sending agent (surfaced back as provenance.activeSelfContext); never shown to recipients. Enforced by functions/src/utils/selfContexts.ts.",
19
+ "required": [
20
+ "type",
21
+ "context"
22
+ ],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "type": {
26
+ "const": "cross_session"
27
+ },
28
+ "context": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "maxLength": 1000,
32
+ "description": "What future-you needs to know about this transfer."
33
+ }
34
+ }
35
+ },
36
+ "sessionSelection": {
37
+ "description": "How to pick the conversation when targeting a user whose chats are session-shaped (agent targets). Canon default for user-targeted sends: continue_or_create.",
38
+ "oneOf": [
39
+ {
40
+ "type": "object",
41
+ "required": [
42
+ "mode"
43
+ ],
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "mode": {
47
+ "enum": [
48
+ "new",
49
+ "continue_latest",
50
+ "continue_or_create"
51
+ ]
52
+ }
53
+ }
54
+ },
55
+ {
56
+ "type": "object",
57
+ "required": [
58
+ "mode",
59
+ "conversationId"
60
+ ],
61
+ "additionalProperties": false,
62
+ "properties": {
63
+ "mode": {
64
+ "const": "specific"
65
+ },
66
+ "conversationId": {
67
+ "type": "string",
68
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
69
+ }
70
+ }
71
+ }
72
+ ]
73
+ },
74
+ "mediaAttachment": {
75
+ "type": "object",
76
+ "required": [
77
+ "kind",
78
+ "url"
79
+ ],
80
+ "additionalProperties": false,
81
+ "properties": {
82
+ "kind": {
83
+ "enum": [
84
+ "image",
85
+ "audio",
86
+ "video",
87
+ "file"
88
+ ]
89
+ },
90
+ "url": {
91
+ "type": "string",
92
+ "minLength": 1,
93
+ "description": "Must come from /media/upload (Canon storage) or the GIF picker — server allowlist, fail-closed (functions/src/utils/mediaUploadSafety.ts)."
94
+ },
95
+ "mimeType": {
96
+ "type": "string"
97
+ },
98
+ "fileName": {
99
+ "type": "string"
100
+ },
101
+ "sizeBytes": {
102
+ "type": "number",
103
+ "minimum": 0
104
+ },
105
+ "width": {
106
+ "type": "number",
107
+ "minimum": 0
108
+ },
109
+ "height": {
110
+ "type": "number",
111
+ "minimum": 0
112
+ },
113
+ "durationMs": {
114
+ "type": "number",
115
+ "minimum": 0
116
+ }
117
+ }
118
+ },
119
+ "turnMetadata": {
120
+ "type": "object",
121
+ "description": "Free-form metadata envelope; JSON.stringify length <= 4096 UTF-16 code units (functions/src/api/sendMessage.ts checks string length, not bytes). Well-known turn-protocol keys below (authoritative type: turnProtocol.ts TurnMetadata, which also carries requestedTurnMode); receipt metadata.type values are server-owned and rejected.",
122
+ "additionalProperties": true,
123
+ "properties": {
124
+ "turnId": {
125
+ "type": [
126
+ "string",
127
+ "null"
128
+ ]
129
+ },
130
+ "turnSemantics": {
131
+ "enum": [
132
+ "progress",
133
+ "turn_complete",
134
+ "control"
135
+ ]
136
+ },
137
+ "deliveryIntent": {
138
+ "enum": [
139
+ "queue",
140
+ "interrupt",
141
+ "interleave",
142
+ "stop"
143
+ ],
144
+ "description": "What the recipient runtime should do if the message lands mid-turn. Default queue."
145
+ },
146
+ "replyBehavior": {
147
+ "enum": [
148
+ "allow_auto_reply",
149
+ "suppress_auto_reply"
150
+ ]
151
+ }
152
+ }
153
+ },
154
+ "messageOptions": {
155
+ "type": "object",
156
+ "description": "Message composition options (mirrors POST /messages/send). Deliberately absent: contact_card contentType (use share_contact) and forwarded/forwardedFrom (forwarding stays host/REST-mediated; /messages/forward is not a v1 verb).",
157
+ "additionalProperties": false,
158
+ "properties": {
159
+ "messageId": {
160
+ "$ref": "#/$defs/messageId"
161
+ },
162
+ "contentType": {
163
+ "enum": [
164
+ "text",
165
+ "image",
166
+ "audio",
167
+ "video",
168
+ "file"
169
+ ],
170
+ "description": "Derived from attachments when omitted. contact_card is deliberately excluded here — use the share_contact verb."
171
+ },
172
+ "attachments": {
173
+ "type": "array",
174
+ "maxItems": 10,
175
+ "items": {
176
+ "$ref": "#/$defs/mediaAttachment"
177
+ }
178
+ },
179
+ "mentions": {
180
+ "type": "array",
181
+ "items": {
182
+ "type": "string"
183
+ },
184
+ "description": "Member userIds; every id must be a conversation member (400 otherwise)."
185
+ },
186
+ "replyTo": {
187
+ "type": "string",
188
+ "description": "Message id being replied to (not existence-validated)."
189
+ },
190
+ "replyToPosition": {
191
+ "type": "integer"
192
+ },
193
+ "metadata": {
194
+ "$ref": "#/$defs/turnMetadata"
195
+ }
196
+ }
197
+ },
198
+ "inputChoice": {
199
+ "type": "object",
200
+ "required": [
201
+ "label"
202
+ ],
203
+ "additionalProperties": false,
204
+ "properties": {
205
+ "label": {
206
+ "type": "string",
207
+ "minLength": 1,
208
+ "maxLength": 120
209
+ },
210
+ "value": {
211
+ "type": "string",
212
+ "maxLength": 200
213
+ },
214
+ "description": {
215
+ "type": "string",
216
+ "maxLength": 300
217
+ }
218
+ }
219
+ },
220
+ "inputQuestion": {
221
+ "type": "object",
222
+ "required": [
223
+ "id",
224
+ "question"
225
+ ],
226
+ "additionalProperties": false,
227
+ "properties": {
228
+ "id": {
229
+ "type": "string",
230
+ "pattern": "^[A-Za-z0-9_.:-]{1,120}$"
231
+ },
232
+ "question": {
233
+ "type": "string",
234
+ "minLength": 1,
235
+ "maxLength": 1000
236
+ },
237
+ "header": {
238
+ "type": "string",
239
+ "maxLength": 120
240
+ },
241
+ "choices": {
242
+ "type": "array",
243
+ "maxItems": 12,
244
+ "items": {
245
+ "$ref": "#/$defs/inputChoice"
246
+ }
247
+ },
248
+ "allowOther": {
249
+ "type": "boolean"
250
+ },
251
+ "isSecret": {
252
+ "type": "boolean",
253
+ "description": "Forces owner-only routing for the whole request."
254
+ },
255
+ "multiSelect": {
256
+ "type": "boolean"
257
+ }
258
+ }
259
+ },
260
+ "approvalDetail": {
261
+ "type": "object",
262
+ "required": [
263
+ "label",
264
+ "value"
265
+ ],
266
+ "additionalProperties": false,
267
+ "properties": {
268
+ "label": {
269
+ "type": "string",
270
+ "minLength": 1,
271
+ "maxLength": 80
272
+ },
273
+ "value": {
274
+ "type": "string",
275
+ "minLength": 1,
276
+ "maxLength": 500
277
+ },
278
+ "monospace": {
279
+ "type": "boolean"
280
+ }
281
+ }
282
+ },
283
+ "unifiedDiff": {
284
+ "type": "object",
285
+ "description": "Unified diff for file-change approvals. Server sanitizes: sensitive paths suppressed, secret tokens redacted, per-file clip 24576 bytes, total 98304 bytes (functions/src/api/interactionApproval.ts; primitives in backend-contracts diffRedaction.ts).",
286
+ "required": [
287
+ "files"
288
+ ],
289
+ "additionalProperties": false,
290
+ "properties": {
291
+ "files": {
292
+ "type": "array",
293
+ "maxItems": 100,
294
+ "items": {
295
+ "type": "object",
296
+ "required": [
297
+ "path",
298
+ "status"
299
+ ],
300
+ "additionalProperties": false,
301
+ "properties": {
302
+ "path": {
303
+ "type": "string",
304
+ "minLength": 1,
305
+ "maxLength": 1024
306
+ },
307
+ "status": {
308
+ "enum": [
309
+ "modified",
310
+ "created",
311
+ "deleted",
312
+ "renamed"
313
+ ]
314
+ },
315
+ "oldPath": {
316
+ "type": "string",
317
+ "maxLength": 1024
318
+ },
319
+ "additions": {
320
+ "type": "integer",
321
+ "minimum": 0
322
+ },
323
+ "deletions": {
324
+ "type": "integer",
325
+ "minimum": 0
326
+ },
327
+ "diff": {
328
+ "type": "string"
329
+ },
330
+ "suppressed": {
331
+ "type": "boolean"
332
+ }
333
+ }
334
+ }
335
+ },
336
+ "truncated": {
337
+ "type": "boolean"
338
+ }
339
+ }
340
+ },
341
+ "sessionRule": {
342
+ "type": "object",
343
+ "description": "Standing approval rule. Owner-only: the server forces allowSessionRule=false for non-owner responders.",
344
+ "required": [
345
+ "type"
346
+ ],
347
+ "additionalProperties": false,
348
+ "properties": {
349
+ "type": {
350
+ "enum": [
351
+ "approve-all",
352
+ "approve-tool",
353
+ "deny-tool"
354
+ ]
355
+ },
356
+ "toolPattern": {
357
+ "type": "string",
358
+ "pattern": "^[\\w.*:-]{1,128}$"
359
+ },
360
+ "expiresAt": {
361
+ "type": [
362
+ "string",
363
+ "null"
364
+ ],
365
+ "description": "ISO timestamp or null."
366
+ }
367
+ },
368
+ "if": {
369
+ "properties": {
370
+ "type": {
371
+ "enum": [
372
+ "approve-tool",
373
+ "deny-tool"
374
+ ]
375
+ }
376
+ },
377
+ "required": [
378
+ "type"
379
+ ]
380
+ },
381
+ "then": {
382
+ "required": [
383
+ "toolPattern"
384
+ ],
385
+ "properties": {
386
+ "toolPattern": true
387
+ }
388
+ }
389
+ },
390
+ "runtimeCard": {
391
+ "type": "object",
392
+ "description": "ENVELOPE-ONLY validation of a canon.card.v1 document: block structures are NOT checked here. Tool bindings MUST compose the full document schema via getVerbInputSchema(verb, { cardSchema: RUNTIME_CARD_JSON_SCHEMA_V1 }) so models cannot produce envelope-valid cards the platform rejects. Full document contract: @canonmsg/rich-cards RUNTIME_CARD_JSON_SCHEMA_V1 ($id https://canonmsg.com/schemas/canon.card.v1.json) — authoring caps title 120 / fallbackText 500 / blocks 24. Server envelope acceptance (interactionCard.ts): <= 32768 bytes serialized, title <= 200, fallbackText <= 2000, blocks <= 64; no secret-bearing top-level keys.",
393
+ "required": [
394
+ "schema",
395
+ "title",
396
+ "fallbackText",
397
+ "blocks"
398
+ ],
399
+ "additionalProperties": true,
400
+ "properties": {
401
+ "schema": {
402
+ "const": "canon.card.v1"
403
+ },
404
+ "cardId": {
405
+ "type": "string",
406
+ "pattern": "^[A-Za-z0-9_.:-]{1,80}$"
407
+ },
408
+ "title": {
409
+ "type": "string",
410
+ "minLength": 1,
411
+ "maxLength": 200
412
+ },
413
+ "fallbackText": {
414
+ "type": "string",
415
+ "minLength": 1,
416
+ "maxLength": 2000
417
+ },
418
+ "blocks": {
419
+ "type": "array",
420
+ "minItems": 1,
421
+ "maxItems": 64
422
+ }
423
+ }
424
+ },
425
+ "native": {
426
+ "type": "object",
427
+ "description": "Runtime correlation handles (<= 24 keys, string values <= 256 chars or a nested handles map <= 16; functions/src/api/interactionKinds.ts normalizeNative).",
428
+ "additionalProperties": true
429
+ },
430
+ "send_to_input": {
431
+ "type": "object",
432
+ "description": "Message another conversation or user (admission-aware), optionally carrying a private self-context. Exactly one of targetConversationId / targetUserId / canonContactId. canonContactId is NOT a wire field: bindings resolve it via POST /admission/resolve to a targetUserId first. Projections: selfContext sends -> POST /messages/send-contextual (which requires sourceConversationId + selfContext); known-conversation sends without selfContext -> POST /messages/send; plain user sends -> admission resolve + create + send (the core reachOut composite). User targets resolve admission first: open targets get the message, approval-required targets get a contact request (send deferred and auto-fulfilled on approval), owner-only targets surface as status 'unavailable' with reason owner-only.",
433
+ "additionalProperties": false,
434
+ "properties": {
435
+ "targetConversationId": {
436
+ "type": "string",
437
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
438
+ },
439
+ "targetUserId": {
440
+ "type": "string",
441
+ "minLength": 1
442
+ },
443
+ "canonContactId": {
444
+ "type": "string",
445
+ "minLength": 1
446
+ },
447
+ "text": {
448
+ "type": "string",
449
+ "maxLength": 4096,
450
+ "description": "<= 4096 UTF-8 bytes. Required unless messageOptions.attachments carries the content."
451
+ },
452
+ "sourceConversationId": {
453
+ "type": "string",
454
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
455
+ "description": "Required when selfContext is present; the agent must be a member."
456
+ },
457
+ "selfContext": {
458
+ "$ref": "#/$defs/selfContext"
459
+ },
460
+ "requestMessage": {
461
+ "type": "string",
462
+ "maxLength": 500,
463
+ "description": "Contact-request note when admission requires approval; defaults to text; senders truncate to 500 chars."
464
+ },
465
+ "sessionSelection": {
466
+ "$ref": "#/$defs/sessionSelection"
467
+ },
468
+ "sessionConfig": {
469
+ "type": [
470
+ "object",
471
+ "null"
472
+ ],
473
+ "description": "Coding-agent session setup (model/permissionMode/effort/workspaceId/executionMode); only applied to agent targets, stripped for humans."
474
+ },
475
+ "messageOptions": {
476
+ "$ref": "#/$defs/messageOptions"
477
+ }
478
+ },
479
+ "oneOf": [
480
+ {
481
+ "required": [
482
+ "targetConversationId"
483
+ ],
484
+ "properties": {
485
+ "targetConversationId": true
486
+ }
487
+ },
488
+ {
489
+ "required": [
490
+ "targetUserId"
491
+ ],
492
+ "properties": {
493
+ "targetUserId": true
494
+ }
495
+ },
496
+ {
497
+ "required": [
498
+ "canonContactId"
499
+ ],
500
+ "properties": {
501
+ "canonContactId": true
502
+ }
503
+ }
504
+ ],
505
+ "dependentRequired": {
506
+ "selfContext": [
507
+ "sourceConversationId"
508
+ ]
509
+ }
510
+ },
511
+ "send_to_result": {
512
+ "description": "Canonical outcome vocabulary. Legacy binding drift documented in CANON_VERB_BINDINGS (hermes 'opened' -> messaged without messageId; hermes 'denied' -> blocked).",
513
+ "oneOf": [
514
+ {
515
+ "type": "object",
516
+ "required": [
517
+ "status",
518
+ "conversationId"
519
+ ],
520
+ "additionalProperties": true,
521
+ "properties": {
522
+ "status": {
523
+ "const": "messaged"
524
+ },
525
+ "conversationId": {
526
+ "type": "string"
527
+ },
528
+ "messageId": {
529
+ "type": "string"
530
+ },
531
+ "selfContextId": {
532
+ "type": "string"
533
+ },
534
+ "created": {
535
+ "type": "boolean"
536
+ },
537
+ "reused": {
538
+ "type": "boolean"
539
+ },
540
+ "sessionSelection": {
541
+ "type": "string"
542
+ }
543
+ }
544
+ },
545
+ {
546
+ "type": "object",
547
+ "required": [
548
+ "status",
549
+ "requestId"
550
+ ],
551
+ "additionalProperties": true,
552
+ "properties": {
553
+ "status": {
554
+ "enum": [
555
+ "requested",
556
+ "pending"
557
+ ]
558
+ },
559
+ "requestId": {
560
+ "type": [
561
+ "string",
562
+ "null"
563
+ ]
564
+ },
565
+ "deferredIntentId": {
566
+ "type": [
567
+ "string",
568
+ "null"
569
+ ],
570
+ "description": "Present when the send is deferred until the contact request resolves."
571
+ }
572
+ }
573
+ },
574
+ {
575
+ "type": "object",
576
+ "required": [
577
+ "status",
578
+ "reason"
579
+ ],
580
+ "additionalProperties": true,
581
+ "properties": {
582
+ "status": {
583
+ "enum": [
584
+ "setup_required",
585
+ "no_session",
586
+ "blocked",
587
+ "unavailable"
588
+ ]
589
+ },
590
+ "reason": {
591
+ "type": "string"
592
+ }
593
+ }
594
+ }
595
+ ]
596
+ },
597
+ "request_input_input": {
598
+ "type": "object",
599
+ "description": "Ask a human a structured question mid-turn (HITL input card). sudo/secret kinds, sensitive:true, or any isSecret question force owner-only routing server-side; bindings must not let the model redirect the responder for those. Ceiling 30 minutes.",
600
+ "additionalProperties": false,
601
+ "properties": {
602
+ "conversationId": {
603
+ "type": "string",
604
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
605
+ "description": "Bindings default to the active conversation."
606
+ },
607
+ "inputId": {
608
+ "type": "string",
609
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
610
+ "description": "Durable single-use id (reuse -> 409); generated when omitted."
611
+ },
612
+ "kind": {
613
+ "enum": [
614
+ "clarify",
615
+ "sudo",
616
+ "secret"
617
+ ],
618
+ "default": "clarify"
619
+ },
620
+ "title": {
621
+ "type": "string",
622
+ "maxLength": 160
623
+ },
624
+ "prompt": {
625
+ "type": "string",
626
+ "maxLength": 4000
627
+ },
628
+ "choices": {
629
+ "type": "array",
630
+ "maxItems": 12,
631
+ "items": {
632
+ "$ref": "#/$defs/inputChoice"
633
+ }
634
+ },
635
+ "questions": {
636
+ "type": "array",
637
+ "maxItems": 12,
638
+ "items": {
639
+ "$ref": "#/$defs/inputQuestion"
640
+ }
641
+ },
642
+ "secretName": {
643
+ "type": "string",
644
+ "maxLength": 160
645
+ },
646
+ "sensitive": {
647
+ "type": "boolean"
648
+ },
649
+ "responseUserId": {
650
+ "type": "string",
651
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
652
+ "description": "Must be a human conversation member. Trust-critical bindings (hermes) derive this from turn provenance and ignore model-supplied values."
653
+ },
654
+ "native": {
655
+ "$ref": "#/$defs/native"
656
+ },
657
+ "turnId": {
658
+ "type": "string",
659
+ "maxLength": 128
660
+ },
661
+ "timeoutMs": {
662
+ "type": "integer",
663
+ "minimum": 1000,
664
+ "maximum": 1800000,
665
+ "description": "Relative deadline in ms (server ceiling 30 minutes). expiresAt wins when both are given."
666
+ },
667
+ "expiresAt": {
668
+ "type": "integer",
669
+ "minimum": 0,
670
+ "description": "Absolute epoch-ms deadline. The REST interaction creators REQUIRE a deadline (and /runtime-input/request also requires kind): bindings must fill defaults and convert timeoutMs into expiresAt before calling the endpoint."
671
+ }
672
+ }
673
+ },
674
+ "request_input_result": {
675
+ "oneOf": [
676
+ {
677
+ "type": "object",
678
+ "required": [
679
+ "status",
680
+ "inputId",
681
+ "value"
682
+ ],
683
+ "additionalProperties": true,
684
+ "properties": {
685
+ "status": {
686
+ "const": "submitted"
687
+ },
688
+ "inputId": {
689
+ "type": "string"
690
+ },
691
+ "value": {
692
+ "type": "string",
693
+ "description": "Free-text answer ('' when structured answers were used)."
694
+ },
695
+ "answers": {
696
+ "type": "object",
697
+ "description": "Structured answers keyed by question id.",
698
+ "additionalProperties": {
699
+ "type": "object",
700
+ "required": [
701
+ "answers"
702
+ ],
703
+ "properties": {
704
+ "answers": {
705
+ "type": "array",
706
+ "items": {
707
+ "type": "string"
708
+ }
709
+ }
710
+ }
711
+ }
712
+ }
713
+ }
714
+ },
715
+ {
716
+ "type": "object",
717
+ "required": [
718
+ "status",
719
+ "inputId"
720
+ ],
721
+ "additionalProperties": true,
722
+ "properties": {
723
+ "status": {
724
+ "enum": [
725
+ "cancelled",
726
+ "timeout"
727
+ ]
728
+ },
729
+ "inputId": {
730
+ "type": "string"
731
+ }
732
+ }
733
+ }
734
+ ]
735
+ },
736
+ "request_approval_input": {
737
+ "type": "object",
738
+ "description": "Ask a human to allow/deny an action. mode 'blocking' (default) waits inside the verb call; mode 'detached' returns pending immediately — fetch the decision later with check_approval. Approval deadline ceiling is 72 hours (other kinds: 30 minutes). Timeouts fail closed to deny.",
739
+ "required": [
740
+ "toolName",
741
+ "toolSummary"
742
+ ],
743
+ "additionalProperties": false,
744
+ "properties": {
745
+ "conversationId": {
746
+ "type": "string",
747
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
748
+ },
749
+ "approvalId": {
750
+ "type": "string",
751
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
752
+ "description": "Durable single-use id; server-generated when omitted."
753
+ },
754
+ "toolName": {
755
+ "type": "string",
756
+ "minLength": 1,
757
+ "maxLength": 128
758
+ },
759
+ "toolSummary": {
760
+ "type": "string",
761
+ "minLength": 1,
762
+ "maxLength": 1000,
763
+ "description": "Pre-redact secrets — this renders to humans."
764
+ },
765
+ "mode": {
766
+ "enum": [
767
+ "blocking",
768
+ "detached"
769
+ ],
770
+ "default": "blocking"
771
+ },
772
+ "riskLevel": {
773
+ "enum": [
774
+ "normal",
775
+ "destructive"
776
+ ],
777
+ "description": "Server accepts only these values; others are silently dropped, not rejected."
778
+ },
779
+ "risk": {
780
+ "enum": [
781
+ "low",
782
+ "normal",
783
+ "high",
784
+ "destructive"
785
+ ],
786
+ "description": "Advisory; server stores any string <= 64 chars today."
787
+ },
788
+ "category": {
789
+ "enum": [
790
+ "command",
791
+ "file",
792
+ "network",
793
+ "browser",
794
+ "mcp",
795
+ "plugin",
796
+ "canon",
797
+ "tool"
798
+ ],
799
+ "description": "Advisory; server stores any string <= 64 chars today."
800
+ },
801
+ "details": {
802
+ "type": "array",
803
+ "maxItems": 8,
804
+ "items": {
805
+ "$ref": "#/$defs/approvalDetail"
806
+ }
807
+ },
808
+ "diff": {
809
+ "$ref": "#/$defs/unifiedDiff"
810
+ },
811
+ "native": {
812
+ "$ref": "#/$defs/native"
813
+ },
814
+ "runtimeId": {
815
+ "type": "string",
816
+ "maxLength": 128
817
+ },
818
+ "turnId": {
819
+ "type": "string",
820
+ "maxLength": 128
821
+ },
822
+ "responseUserId": {
823
+ "type": "string",
824
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
825
+ },
826
+ "allowSessionRule": {
827
+ "type": "boolean",
828
+ "description": "Server forces false when the responder is not the agent owner."
829
+ },
830
+ "timeoutMs": {
831
+ "type": "integer",
832
+ "minimum": 1000,
833
+ "maximum": 259200000,
834
+ "description": "Relative deadline in ms (server ceiling 72 hours). expiresAt wins when both are given."
835
+ },
836
+ "expiresAt": {
837
+ "type": "integer",
838
+ "minimum": 0,
839
+ "description": "Absolute epoch-ms deadline. The REST interaction creators REQUIRE a deadline (and /runtime-input/request also requires kind): bindings must fill defaults and convert timeoutMs into expiresAt before calling the endpoint."
840
+ }
841
+ }
842
+ },
843
+ "request_approval_result": {
844
+ "oneOf": [
845
+ {
846
+ "type": "object",
847
+ "required": [
848
+ "status",
849
+ "approvalId"
850
+ ],
851
+ "additionalProperties": true,
852
+ "properties": {
853
+ "status": {
854
+ "const": "allow"
855
+ },
856
+ "approvalId": {
857
+ "type": "string"
858
+ },
859
+ "sessionRule": {
860
+ "$ref": "#/$defs/sessionRule"
861
+ },
862
+ "respondedBy": {
863
+ "type": "string",
864
+ "description": "Canon-authenticated responder userId."
865
+ }
866
+ }
867
+ },
868
+ {
869
+ "type": "object",
870
+ "required": [
871
+ "status",
872
+ "approvalId"
873
+ ],
874
+ "additionalProperties": true,
875
+ "properties": {
876
+ "status": {
877
+ "const": "deny"
878
+ },
879
+ "approvalId": {
880
+ "type": "string"
881
+ },
882
+ "sessionRule": {
883
+ "$ref": "#/$defs/sessionRule"
884
+ },
885
+ "respondedBy": {
886
+ "type": "string"
887
+ }
888
+ }
889
+ },
890
+ {
891
+ "type": "object",
892
+ "required": [
893
+ "status",
894
+ "approvalId"
895
+ ],
896
+ "additionalProperties": true,
897
+ "properties": {
898
+ "status": {
899
+ "const": "timeout"
900
+ },
901
+ "approvalId": {
902
+ "type": "string"
903
+ }
904
+ }
905
+ },
906
+ {
907
+ "type": "object",
908
+ "description": "Detached acceptance — the decision arrives via check_approval.",
909
+ "required": [
910
+ "status",
911
+ "approvalId",
912
+ "expiresAt"
913
+ ],
914
+ "additionalProperties": true,
915
+ "properties": {
916
+ "status": {
917
+ "const": "pending"
918
+ },
919
+ "approvalId": {
920
+ "type": "string"
921
+ },
922
+ "conversationId": {
923
+ "type": "string"
924
+ },
925
+ "expiresAt": {
926
+ "type": "integer"
927
+ },
928
+ "responseUserId": {
929
+ "type": "string"
930
+ }
931
+ }
932
+ }
933
+ ]
934
+ },
935
+ "check_approval_input": {
936
+ "type": "object",
937
+ "description": "Fetch the decision of a detached approval. Consume is idempotent within the replay window: a resolved decision replays for up to 72h after first consume, then the id becomes unknown. Bindings must bind the check to the conversation that created the approval.",
938
+ "required": [
939
+ "approvalId"
940
+ ],
941
+ "additionalProperties": false,
942
+ "properties": {
943
+ "approvalId": {
944
+ "type": "string",
945
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
946
+ },
947
+ "conversationId": {
948
+ "type": "string",
949
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
950
+ }
951
+ }
952
+ },
953
+ "check_approval_result": {
954
+ "description": "'unknown' is NOT a denial — only 'resolved' carries a decision. Canonical statuses map from the /runtime-approval/consume wire as: allow/deny -> resolved.decision, timeout -> expired, pending -> pending, HTTP 404 RUNTIME_APPROVAL_NOT_FOUND -> unknown.",
955
+ "oneOf": [
956
+ {
957
+ "type": "object",
958
+ "required": [
959
+ "status",
960
+ "approvalId",
961
+ "decision"
962
+ ],
963
+ "additionalProperties": true,
964
+ "properties": {
965
+ "status": {
966
+ "const": "resolved"
967
+ },
968
+ "approvalId": {
969
+ "type": "string"
970
+ },
971
+ "decision": {
972
+ "enum": [
973
+ "allow",
974
+ "deny"
975
+ ]
976
+ },
977
+ "respondedBy": {
978
+ "type": "string"
979
+ },
980
+ "conversationId": {
981
+ "type": "string"
982
+ }
983
+ }
984
+ },
985
+ {
986
+ "type": "object",
987
+ "required": [
988
+ "status",
989
+ "approvalId"
990
+ ],
991
+ "additionalProperties": true,
992
+ "properties": {
993
+ "status": {
994
+ "enum": [
995
+ "pending",
996
+ "expired",
997
+ "unknown"
998
+ ]
999
+ },
1000
+ "approvalId": {
1001
+ "type": "string"
1002
+ },
1003
+ "expiresAt": {
1004
+ "type": "integer"
1005
+ },
1006
+ "conversationId": {
1007
+ "type": "string"
1008
+ }
1009
+ }
1010
+ }
1011
+ ]
1012
+ },
1013
+ "send_card_input": {
1014
+ "type": "object",
1015
+ "description": "Display a card with no actions — fire-and-forget, no pending state, no deadline.",
1016
+ "required": [
1017
+ "card"
1018
+ ],
1019
+ "additionalProperties": false,
1020
+ "properties": {
1021
+ "conversationId": {
1022
+ "type": "string",
1023
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1024
+ },
1025
+ "card": {
1026
+ "$ref": "#/$defs/runtimeCard"
1027
+ },
1028
+ "cardId": {
1029
+ "type": "string",
1030
+ "pattern": "^[A-Za-z0-9_.:-]{1,80}$"
1031
+ },
1032
+ "native": {
1033
+ "$ref": "#/$defs/native"
1034
+ },
1035
+ "runtimeId": {
1036
+ "type": "string",
1037
+ "maxLength": 128
1038
+ },
1039
+ "turnId": {
1040
+ "type": "string",
1041
+ "maxLength": 128
1042
+ }
1043
+ }
1044
+ },
1045
+ "send_card_result": {
1046
+ "type": "object",
1047
+ "required": [
1048
+ "status",
1049
+ "cardId"
1050
+ ],
1051
+ "additionalProperties": true,
1052
+ "properties": {
1053
+ "status": {
1054
+ "const": "displayed"
1055
+ },
1056
+ "cardId": {
1057
+ "type": "string"
1058
+ },
1059
+ "conversationId": {
1060
+ "type": "string"
1061
+ },
1062
+ "responseUserId": {
1063
+ "type": "string"
1064
+ }
1065
+ }
1066
+ },
1067
+ "request_card_input": {
1068
+ "type": "object",
1069
+ "description": "Show an interactive card (>= 1 actions block required) and wait for the response. Deadline ceiling 30 minutes.",
1070
+ "required": [
1071
+ "card"
1072
+ ],
1073
+ "additionalProperties": false,
1074
+ "properties": {
1075
+ "conversationId": {
1076
+ "type": "string",
1077
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1078
+ },
1079
+ "card": {
1080
+ "$ref": "#/$defs/runtimeCard"
1081
+ },
1082
+ "cardId": {
1083
+ "type": "string",
1084
+ "pattern": "^[A-Za-z0-9_.:-]{1,80}$"
1085
+ },
1086
+ "responseUserId": {
1087
+ "type": "string",
1088
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
1089
+ "description": "Must be a human conversation member; defaults to the turn-triggering human (trust-critical bindings ignore model-supplied values)."
1090
+ },
1091
+ "native": {
1092
+ "$ref": "#/$defs/native"
1093
+ },
1094
+ "runtimeId": {
1095
+ "type": "string",
1096
+ "maxLength": 128
1097
+ },
1098
+ "turnId": {
1099
+ "type": "string",
1100
+ "maxLength": 128
1101
+ },
1102
+ "timeoutMs": {
1103
+ "type": "integer",
1104
+ "minimum": 1000,
1105
+ "maximum": 1800000,
1106
+ "description": "Relative deadline in ms (server ceiling 30 minutes). expiresAt wins when both are given."
1107
+ },
1108
+ "expiresAt": {
1109
+ "type": "integer",
1110
+ "minimum": 0,
1111
+ "description": "Absolute epoch-ms deadline. The REST interaction creators REQUIRE a deadline (and /runtime-input/request also requires kind): bindings must fill defaults and convert timeoutMs into expiresAt before calling the endpoint."
1112
+ }
1113
+ }
1114
+ },
1115
+ "request_card_result": {
1116
+ "oneOf": [
1117
+ {
1118
+ "type": "object",
1119
+ "required": [
1120
+ "status",
1121
+ "cardId"
1122
+ ],
1123
+ "additionalProperties": true,
1124
+ "properties": {
1125
+ "status": {
1126
+ "const": "submitted"
1127
+ },
1128
+ "cardId": {
1129
+ "type": "string"
1130
+ },
1131
+ "actionId": {
1132
+ "type": "string"
1133
+ },
1134
+ "values": {
1135
+ "type": "object",
1136
+ "description": "Validated server-side against the card's actionFields (validateRuntimeCardFieldValues); <= 8192 bytes, depth <= 8."
1137
+ },
1138
+ "respondedBy": {
1139
+ "type": "string",
1140
+ "description": "Canon-authenticated responder (server-verified vs responseUserId)."
1141
+ }
1142
+ }
1143
+ },
1144
+ {
1145
+ "type": "object",
1146
+ "required": [
1147
+ "status",
1148
+ "cardId"
1149
+ ],
1150
+ "additionalProperties": true,
1151
+ "properties": {
1152
+ "status": {
1153
+ "const": "cancelled"
1154
+ },
1155
+ "cardId": {
1156
+ "type": "string"
1157
+ },
1158
+ "respondedBy": {
1159
+ "type": "string"
1160
+ }
1161
+ }
1162
+ },
1163
+ {
1164
+ "type": "object",
1165
+ "required": [
1166
+ "status",
1167
+ "cardId"
1168
+ ],
1169
+ "additionalProperties": true,
1170
+ "properties": {
1171
+ "status": {
1172
+ "const": "timeout"
1173
+ },
1174
+ "cardId": {
1175
+ "type": "string"
1176
+ }
1177
+ }
1178
+ }
1179
+ ]
1180
+ },
1181
+ "share_contact_input": {
1182
+ "type": "object",
1183
+ "description": "Share a contact card into a conversation (POST /messages/send, contentType 'contact_card'). The shared user must be in the sending agent's contacts (403 otherwise; the owner shortcut applies only to human senders sharing their own agents). The server snapshots the card.",
1184
+ "required": [
1185
+ "conversationId",
1186
+ "contactUserId"
1187
+ ],
1188
+ "additionalProperties": false,
1189
+ "properties": {
1190
+ "conversationId": {
1191
+ "type": "string",
1192
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1193
+ },
1194
+ "contactUserId": {
1195
+ "type": "string",
1196
+ "minLength": 1
1197
+ },
1198
+ "text": {
1199
+ "type": "string",
1200
+ "maxLength": 4096
1201
+ },
1202
+ "messageId": {
1203
+ "$ref": "#/$defs/messageId"
1204
+ }
1205
+ }
1206
+ },
1207
+ "share_contact_result": {
1208
+ "type": "object",
1209
+ "required": [
1210
+ "status",
1211
+ "messageId"
1212
+ ],
1213
+ "additionalProperties": true,
1214
+ "properties": {
1215
+ "status": {
1216
+ "const": "shared"
1217
+ },
1218
+ "messageId": {
1219
+ "type": "string"
1220
+ }
1221
+ }
1222
+ },
1223
+ "react_input": {
1224
+ "type": "object",
1225
+ "description": "Toggle an emoji reaction on a message (server-side atomic toggle; functions/src/api/reactToMessage.ts).",
1226
+ "required": [
1227
+ "conversationId",
1228
+ "messageId",
1229
+ "emoji"
1230
+ ],
1231
+ "additionalProperties": false,
1232
+ "properties": {
1233
+ "conversationId": {
1234
+ "type": "string",
1235
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1236
+ },
1237
+ "messageId": {
1238
+ "type": "string",
1239
+ "minLength": 1
1240
+ },
1241
+ "emoji": {
1242
+ "type": "string",
1243
+ "minLength": 1,
1244
+ "maxLength": 64,
1245
+ "description": "Reaction key. Content-adjacent (D5) — rides the wire body."
1246
+ }
1247
+ }
1248
+ },
1249
+ "react_result": {
1250
+ "type": "object",
1251
+ "required": [
1252
+ "status",
1253
+ "action"
1254
+ ],
1255
+ "additionalProperties": true,
1256
+ "properties": {
1257
+ "status": {
1258
+ "const": "reacted"
1259
+ },
1260
+ "action": {
1261
+ "enum": [
1262
+ "added",
1263
+ "removed"
1264
+ ]
1265
+ },
1266
+ "reactions": {
1267
+ "type": "object"
1268
+ }
1269
+ }
1270
+ },
1271
+ "forward_input": {
1272
+ "type": "object",
1273
+ "description": "Forward an existing message into another conversation. Under E2EE this becomes client-side re-encrypt; only routing ids + an optional caption cross the wire.",
1274
+ "required": [
1275
+ "sourceConversationId",
1276
+ "targetConversationId",
1277
+ "messageId"
1278
+ ],
1279
+ "additionalProperties": false,
1280
+ "properties": {
1281
+ "sourceConversationId": {
1282
+ "type": "string",
1283
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1284
+ },
1285
+ "targetConversationId": {
1286
+ "type": "string",
1287
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1288
+ },
1289
+ "messageId": {
1290
+ "type": "string",
1291
+ "minLength": 1
1292
+ },
1293
+ "text": {
1294
+ "type": "string",
1295
+ "maxLength": 4096,
1296
+ "description": "Optional caption; <= 4096 UTF-8 bytes."
1297
+ }
1298
+ }
1299
+ },
1300
+ "forward_result": {
1301
+ "type": "object",
1302
+ "required": [
1303
+ "status",
1304
+ "messageId",
1305
+ "targetConversationId"
1306
+ ],
1307
+ "additionalProperties": true,
1308
+ "properties": {
1309
+ "status": {
1310
+ "const": "forwarded"
1311
+ },
1312
+ "messageId": {
1313
+ "type": "string"
1314
+ },
1315
+ "targetConversationId": {
1316
+ "type": "string"
1317
+ }
1318
+ }
1319
+ },
1320
+ "create_group_input": {
1321
+ "type": "object",
1322
+ "description": "Create a group conversation. Each target's groupJoinPolicy is enforced server-side with staged admission: directly-addable members join at creation, approval-required members become pending group_invite requests, policy-denied members are skipped (see the result). At least one member must be directly addable — a creator-only group is rejected with error code CREATE_GROUP_NO_ADDABLE_MEMBERS. v1 scoping: coding-agent members that require explicit session setup are skipped (reason setup-required), and a creator that itself requires setup cannot create groups (403 CREATE_GROUP_CREATOR_SETUP_REQUIRED); only the owner can set such agents up, from the app. Under MLS, membership changes are Add/Remove proposals + Commit — a group operation is a cryptographic state change, not a codec swap.",
1323
+ "required": [
1324
+ "name",
1325
+ "memberIds"
1326
+ ],
1327
+ "additionalProperties": false,
1328
+ "properties": {
1329
+ "name": {
1330
+ "type": "string",
1331
+ "minLength": 1,
1332
+ "maxLength": 100
1333
+ },
1334
+ "memberIds": {
1335
+ "type": "array",
1336
+ "minItems": 1,
1337
+ "maxItems": 49,
1338
+ "items": {
1339
+ "type": "string",
1340
+ "minLength": 1
1341
+ },
1342
+ "description": "Other members; the caller is added automatically."
1343
+ }
1344
+ }
1345
+ },
1346
+ "create_group_result": {
1347
+ "type": "object",
1348
+ "description": "Staged admission: the group exists with the directly-addable members; `pending` lists approval-required members whose group_invite awaits their approver; `skipped` lists members whose policy denied the add (reasons include owner-only, blocked, not-found, setup-required).",
1349
+ "required": [
1350
+ "status",
1351
+ "conversationId"
1352
+ ],
1353
+ "additionalProperties": true,
1354
+ "properties": {
1355
+ "status": {
1356
+ "const": "created"
1357
+ },
1358
+ "conversationId": {
1359
+ "type": "string"
1360
+ },
1361
+ "pending": {
1362
+ "type": "array",
1363
+ "items": {
1364
+ "type": "object",
1365
+ "required": [
1366
+ "userId",
1367
+ "requestId"
1368
+ ],
1369
+ "additionalProperties": true,
1370
+ "properties": {
1371
+ "userId": {
1372
+ "type": "string"
1373
+ },
1374
+ "requestId": {
1375
+ "type": "string"
1376
+ }
1377
+ }
1378
+ }
1379
+ },
1380
+ "skipped": {
1381
+ "type": "array",
1382
+ "items": {
1383
+ "type": "object",
1384
+ "required": [
1385
+ "userId",
1386
+ "reason"
1387
+ ],
1388
+ "additionalProperties": true,
1389
+ "properties": {
1390
+ "userId": {
1391
+ "type": "string"
1392
+ },
1393
+ "reason": {
1394
+ "type": "string"
1395
+ }
1396
+ }
1397
+ }
1398
+ }
1399
+ }
1400
+ },
1401
+ "add_member_input": {
1402
+ "type": "object",
1403
+ "description": "v1 scoping: a coding-agent target that requires explicit session setup is rejected (only its owner can set it up, from the app). Under MLS an add is an Add proposal + Commit (new epoch), not a server-side membership write — the verb semantics are stable, the mechanism is not.",
1404
+ "required": [
1405
+ "conversationId",
1406
+ "userId"
1407
+ ],
1408
+ "additionalProperties": false,
1409
+ "properties": {
1410
+ "conversationId": {
1411
+ "type": "string",
1412
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1413
+ },
1414
+ "userId": {
1415
+ "type": "string",
1416
+ "minLength": 1
1417
+ }
1418
+ }
1419
+ },
1420
+ "add_member_result": {
1421
+ "description": "approval-required targets yield a pending group_invite contact request routed to their approver.",
1422
+ "oneOf": [
1423
+ {
1424
+ "type": "object",
1425
+ "required": [
1426
+ "status"
1427
+ ],
1428
+ "additionalProperties": true,
1429
+ "properties": {
1430
+ "status": {
1431
+ "const": "added"
1432
+ }
1433
+ }
1434
+ },
1435
+ {
1436
+ "type": "object",
1437
+ "required": [
1438
+ "status",
1439
+ "requestId"
1440
+ ],
1441
+ "additionalProperties": true,
1442
+ "properties": {
1443
+ "status": {
1444
+ "const": "pending"
1445
+ },
1446
+ "requestId": {
1447
+ "type": "string"
1448
+ }
1449
+ }
1450
+ }
1451
+ ]
1452
+ },
1453
+ "remove_member_input": {
1454
+ "type": "object",
1455
+ "description": "Requester must be a group owner/admin (server-enforced). Under MLS a removal is a Remove proposal + Commit that rotates the group secret away from the removed leaf.",
1456
+ "required": [
1457
+ "conversationId",
1458
+ "userId"
1459
+ ],
1460
+ "additionalProperties": false,
1461
+ "properties": {
1462
+ "conversationId": {
1463
+ "type": "string",
1464
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1465
+ },
1466
+ "userId": {
1467
+ "type": "string",
1468
+ "minLength": 1
1469
+ }
1470
+ }
1471
+ },
1472
+ "remove_member_result": {
1473
+ "type": "object",
1474
+ "required": [
1475
+ "status"
1476
+ ],
1477
+ "additionalProperties": true,
1478
+ "properties": {
1479
+ "status": {
1480
+ "const": "removed"
1481
+ }
1482
+ }
1483
+ },
1484
+ "leave_conversation_input": {
1485
+ "type": "object",
1486
+ "required": [
1487
+ "conversationId"
1488
+ ],
1489
+ "additionalProperties": false,
1490
+ "properties": {
1491
+ "conversationId": {
1492
+ "type": "string",
1493
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1494
+ }
1495
+ }
1496
+ },
1497
+ "leave_conversation_result": {
1498
+ "type": "object",
1499
+ "required": [
1500
+ "status"
1501
+ ],
1502
+ "additionalProperties": true,
1503
+ "properties": {
1504
+ "status": {
1505
+ "const": "left"
1506
+ }
1507
+ }
1508
+ },
1509
+ "list_contacts_input": {
1510
+ "type": "object",
1511
+ "additionalProperties": false,
1512
+ "properties": {}
1513
+ },
1514
+ "list_contacts_result": {
1515
+ "type": "object",
1516
+ "required": [
1517
+ "contacts"
1518
+ ],
1519
+ "additionalProperties": true,
1520
+ "properties": {
1521
+ "contacts": {
1522
+ "type": "array",
1523
+ "items": {
1524
+ "type": "object",
1525
+ "required": [
1526
+ "id",
1527
+ "source",
1528
+ "addedAt",
1529
+ "displayNameOverride"
1530
+ ],
1531
+ "additionalProperties": true,
1532
+ "properties": {
1533
+ "id": {
1534
+ "type": "string",
1535
+ "description": "The contact's userId."
1536
+ },
1537
+ "source": {
1538
+ "type": "string",
1539
+ "description": "Vocabulary: direct_add | phone_book | contact_request | link | qr | group | open_inbound_message | unknown (server passes strings through)."
1540
+ },
1541
+ "addedAt": {
1542
+ "type": [
1543
+ "string",
1544
+ "null"
1545
+ ]
1546
+ },
1547
+ "displayNameOverride": {
1548
+ "type": [
1549
+ "string",
1550
+ "null"
1551
+ ]
1552
+ }
1553
+ }
1554
+ }
1555
+ }
1556
+ }
1557
+ },
1558
+ "list_contact_requests_input": {
1559
+ "type": "object",
1560
+ "additionalProperties": false,
1561
+ "properties": {}
1562
+ },
1563
+ "list_contact_requests_result": {
1564
+ "type": "object",
1565
+ "description": "Pending inbound requests, newest first, capped at 100. Items are SerializedContactRequest (backend-contracts contactRequest.ts). Read-only awareness: approval routes to the owner; agents cannot approve/reject.",
1566
+ "required": [
1567
+ "requests"
1568
+ ],
1569
+ "additionalProperties": true,
1570
+ "properties": {
1571
+ "requests": {
1572
+ "type": "array",
1573
+ "items": {
1574
+ "type": "object",
1575
+ "required": [
1576
+ "id",
1577
+ "requesterId",
1578
+ "targetId",
1579
+ "status",
1580
+ "kind"
1581
+ ],
1582
+ "additionalProperties": true,
1583
+ "properties": {
1584
+ "id": {
1585
+ "type": "string"
1586
+ },
1587
+ "requesterId": {
1588
+ "type": "string"
1589
+ },
1590
+ "requesterName": {
1591
+ "type": "string"
1592
+ },
1593
+ "targetId": {
1594
+ "type": "string"
1595
+ },
1596
+ "status": {
1597
+ "enum": [
1598
+ "pending",
1599
+ "approved",
1600
+ "rejected",
1601
+ "expired"
1602
+ ]
1603
+ },
1604
+ "kind": {
1605
+ "enum": [
1606
+ "dm",
1607
+ "group_invite"
1608
+ ]
1609
+ },
1610
+ "message": {
1611
+ "type": [
1612
+ "string",
1613
+ "null"
1614
+ ]
1615
+ },
1616
+ "createdAt": {
1617
+ "type": [
1618
+ "string",
1619
+ "null"
1620
+ ]
1621
+ },
1622
+ "expiresAt": {
1623
+ "type": [
1624
+ "string",
1625
+ "null"
1626
+ ]
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+ }
1632
+ },
1633
+ "list_conversations_input": {
1634
+ "type": "object",
1635
+ "additionalProperties": false,
1636
+ "properties": {
1637
+ "limit": {
1638
+ "type": "integer",
1639
+ "minimum": 1,
1640
+ "description": "Optional client-side cap applied by the binding; the REST endpoint has no pagination and returns all visible memberships."
1641
+ }
1642
+ }
1643
+ },
1644
+ "list_conversations_result": {
1645
+ "type": "object",
1646
+ "required": [
1647
+ "conversations"
1648
+ ],
1649
+ "additionalProperties": true,
1650
+ "properties": {
1651
+ "conversations": {
1652
+ "type": "array",
1653
+ "items": {
1654
+ "type": "object",
1655
+ "required": [
1656
+ "id",
1657
+ "type",
1658
+ "memberIds",
1659
+ "isAgentChat",
1660
+ "topic",
1661
+ "lastMessage",
1662
+ "createdAt"
1663
+ ],
1664
+ "additionalProperties": true,
1665
+ "properties": {
1666
+ "id": {
1667
+ "type": "string"
1668
+ },
1669
+ "type": {
1670
+ "enum": [
1671
+ "direct",
1672
+ "group"
1673
+ ]
1674
+ },
1675
+ "name": {
1676
+ "type": [
1677
+ "string",
1678
+ "null"
1679
+ ]
1680
+ },
1681
+ "topic": {
1682
+ "type": [
1683
+ "string",
1684
+ "null"
1685
+ ]
1686
+ },
1687
+ "memberIds": {
1688
+ "type": "array",
1689
+ "items": {
1690
+ "type": "string"
1691
+ }
1692
+ },
1693
+ "isAgentChat": {
1694
+ "type": "boolean"
1695
+ },
1696
+ "hasUnread": {
1697
+ "type": "boolean"
1698
+ },
1699
+ "lastMessage": {
1700
+ "type": [
1701
+ "object",
1702
+ "null"
1703
+ ],
1704
+ "additionalProperties": true,
1705
+ "properties": {
1706
+ "text": {
1707
+ "type": [
1708
+ "string",
1709
+ "null"
1710
+ ]
1711
+ },
1712
+ "messageId": {
1713
+ "type": "string"
1714
+ },
1715
+ "senderId": {
1716
+ "type": "string"
1717
+ },
1718
+ "senderType": {
1719
+ "type": "string"
1720
+ },
1721
+ "contentType": {
1722
+ "type": "string"
1723
+ },
1724
+ "timestamp": {
1725
+ "type": [
1726
+ "string",
1727
+ "null"
1728
+ ]
1729
+ }
1730
+ }
1731
+ },
1732
+ "createdAt": {
1733
+ "type": [
1734
+ "string",
1735
+ "null"
1736
+ ]
1737
+ }
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ }
1743
+ }
1744
+ }