@canonmsg/backend-contracts 5.2.0 → 5.4.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.
@@ -13,6 +13,23 @@
13
13
  },
14
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
15
  },
16
+ "groupInviteRequirements": {
17
+ "type": "object",
18
+ "description": "Independent gates on a pending group invite. policyApproval is social consent; ownerSessionSetup means the agent owner must choose the runtime session configuration before membership is activated.",
19
+ "required": [
20
+ "policyApproval",
21
+ "ownerSessionSetup"
22
+ ],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "policyApproval": {
26
+ "type": "boolean"
27
+ },
28
+ "ownerSessionSetup": {
29
+ "type": "boolean"
30
+ }
31
+ }
32
+ },
16
33
  "selfContext": {
17
34
  "type": "object",
18
35
  "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.",
@@ -1471,7 +1488,7 @@
1471
1488
  },
1472
1489
  "create_group_input": {
1473
1490
  "type": "object",
1474
- "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.",
1491
+ "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; agents needing explicit group-session setup become pending owner requests; hard-denied members are skipped (see the result). A creator-only group is valid when at least one requested member has a pending invite. A creator that itself requires setup cannot create groups (403 CREATE_GROUP_CREATOR_SETUP_REQUIRED). Under MLS, membership changes are Add/Remove proposals + Commit — a group operation is a cryptographic state change, not a codec swap.",
1475
1492
  "required": [
1476
1493
  "name",
1477
1494
  "memberIds"
@@ -1497,10 +1514,13 @@
1497
1514
  },
1498
1515
  "create_group_result": {
1499
1516
  "type": "object",
1500
- "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).",
1517
+ "description": "Staged admission: the group exists with the directly-added members; `pending` lists group_invite requests awaiting policy approval, owner session setup, or both; `skipped` lists hard-denied targets.",
1501
1518
  "required": [
1502
1519
  "status",
1503
- "conversationId"
1520
+ "conversationId",
1521
+ "added",
1522
+ "pending",
1523
+ "skipped"
1504
1524
  ],
1505
1525
  "additionalProperties": true,
1506
1526
  "properties": {
@@ -1510,13 +1530,20 @@
1510
1530
  "conversationId": {
1511
1531
  "type": "string"
1512
1532
  },
1533
+ "added": {
1534
+ "type": "array",
1535
+ "items": {
1536
+ "type": "string"
1537
+ }
1538
+ },
1513
1539
  "pending": {
1514
1540
  "type": "array",
1515
1541
  "items": {
1516
1542
  "type": "object",
1517
1543
  "required": [
1518
1544
  "userId",
1519
- "requestId"
1545
+ "requestId",
1546
+ "requirements"
1520
1547
  ],
1521
1548
  "additionalProperties": true,
1522
1549
  "properties": {
@@ -1525,6 +1552,9 @@
1525
1552
  },
1526
1553
  "requestId": {
1527
1554
  "type": "string"
1555
+ },
1556
+ "requirements": {
1557
+ "$ref": "#/$defs/groupInviteRequirements"
1528
1558
  }
1529
1559
  }
1530
1560
  }
@@ -1552,7 +1582,7 @@
1552
1582
  },
1553
1583
  "add_member_input": {
1554
1584
  "type": "object",
1555
- "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.",
1585
+ "description": "A target needing policy approval and/or explicit owner session setup yields one pending group_invite. 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.",
1556
1586
  "required": [
1557
1587
  "conversationId",
1558
1588
  "userId"
@@ -1570,7 +1600,7 @@
1570
1600
  }
1571
1601
  },
1572
1602
  "add_member_result": {
1573
- "description": "approval-required targets yield a pending group_invite contact request routed to their approver.",
1603
+ "description": "Targets needing policy approval and/or owner session setup yield one pending group_invite routed to the target or agent owner.",
1574
1604
  "oneOf": [
1575
1605
  {
1576
1606
  "type": "object",
@@ -1588,7 +1618,8 @@
1588
1618
  "type": "object",
1589
1619
  "required": [
1590
1620
  "status",
1591
- "requestId"
1621
+ "requestId",
1622
+ "requirements"
1592
1623
  ],
1593
1624
  "additionalProperties": true,
1594
1625
  "properties": {
@@ -1597,6 +1628,9 @@
1597
1628
  },
1598
1629
  "requestId": {
1599
1630
  "type": "string"
1631
+ },
1632
+ "requirements": {
1633
+ "$ref": "#/$defs/groupInviteRequirements"
1600
1634
  }
1601
1635
  }
1602
1636
  }
@@ -1901,6 +1935,11 @@
1901
1935
  "type": "string",
1902
1936
  "pattern": "^[A-Za-z0-9_.:-]{1,160}$"
1903
1937
  },
1938
+ "messageId": {
1939
+ "type": "string",
1940
+ "pattern": "^[A-Za-z0-9_.:-]{1,160}$",
1941
+ "description": "The triggering message this silence answers, so the record cannot be misread against a later message."
1942
+ },
1904
1943
  "reason": {
1905
1944
  "type": "string",
1906
1945
  "maxLength": 500,
@@ -1,6 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readGroupInviteRequirements = readGroupInviteRequirements;
3
4
  exports.serializeContactRequest = serializeContactRequest;
5
+ function readGroupInviteRequirements(value) {
6
+ const record = isRecord(value) ? value : null;
7
+ if (typeof record?.policyApproval !== 'boolean'
8
+ || typeof record.ownerSessionSetup !== 'boolean') {
9
+ throw new TypeError('Invalid group invite requirements');
10
+ }
11
+ return {
12
+ policyApproval: record.policyApproval,
13
+ ownerSessionSetup: record.ownerSessionSetup,
14
+ };
15
+ }
4
16
  function isRecord(value) {
5
17
  return typeof value === 'object' && value !== null && !Array.isArray(value);
6
18
  }
@@ -85,7 +97,15 @@ function serializeContactRequest(requestId, data) {
85
97
  expiresAt: normalizeTimestamp(data.expiresAt),
86
98
  };
87
99
  if (groupContext) {
100
+ let requirements;
101
+ try {
102
+ requirements = readGroupInviteRequirements(data.requirements);
103
+ }
104
+ catch {
105
+ return null;
106
+ }
88
107
  payload.groupContext = groupContext;
108
+ payload.requirements = requirements;
89
109
  }
90
110
  if (data.targetUserType === 'ai_agent') {
91
111
  payload.targetOwnerId = data.targetOwnerId;
@@ -266,10 +266,9 @@ function normalizeVerbNativeMetadata(value) {
266
266
  return Object.keys(native).length > 0 ? native : undefined;
267
267
  }
268
268
  /**
269
- * Error code on the 400 a create_group receives when NO member is directly
270
- * addable (approval-required members can only be invited to an existing
271
- * group; a creator-only group is not created). The error detail carries the
272
- * partition: pendingRequired[] and skipped[].
269
+ * Error code on the 400 a create_group receives when no requested member can
270
+ * be added or invited. A creator-only group with at least one valid pending
271
+ * invite is allowed; the error detail carries the attempted partition.
273
272
  */
274
273
  exports.CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = 'CREATE_GROUP_NO_ADDABLE_MEMBERS';
275
274
  /**
@@ -34,6 +34,18 @@ exports.getVerbInputSchema = getVerbInputSchema;
34
34
  exports.getVerbResultSchema = getVerbResultSchema;
35
35
  const verbContract_js_1 = require("./verbContract.js");
36
36
  const REF = (def) => ({ $ref: `#/$defs/${def}` });
37
+ const groupInviteRequirementsDef = {
38
+ type: 'object',
39
+ description: 'Independent gates on a pending group invite. policyApproval is social '
40
+ + 'consent; ownerSessionSetup means the agent owner must choose the '
41
+ + 'runtime session configuration before membership is activated.',
42
+ required: ['policyApproval', 'ownerSessionSetup'],
43
+ additionalProperties: false,
44
+ properties: {
45
+ policyApproval: { type: 'boolean' },
46
+ ownerSessionSetup: { type: 'boolean' },
47
+ },
48
+ };
37
49
  const selfContextDef = {
38
50
  type: 'object',
39
51
  description: 'Private note-to-self attached to a cross-conversation send. Visible only to the '
@@ -820,13 +832,11 @@ const create_group_input = {
820
832
  description: "Create a group conversation. Each target's groupJoinPolicy is enforced "
821
833
  + 'server-side with staged admission: directly-addable members join at '
822
834
  + 'creation, approval-required members become pending group_invite '
823
- + 'requests, policy-denied members are skipped (see the result). At '
824
- + 'least one member must be directly addable — a creator-only group is '
825
- + 'rejected with error code CREATE_GROUP_NO_ADDABLE_MEMBERS. v1 scoping: '
826
- + 'coding-agent members that require explicit session setup are skipped '
827
- + '(reason setup-required), and a creator that itself requires setup '
828
- + 'cannot create groups (403 CREATE_GROUP_CREATOR_SETUP_REQUIRED); only '
829
- + 'the owner can set such agents up, from the app. Under MLS, membership '
835
+ + 'requests; agents needing explicit group-session setup become pending '
836
+ + 'owner requests; hard-denied members are skipped (see the result). A '
837
+ + 'creator-only group is valid when at least one requested member has a '
838
+ + 'pending invite. A creator that itself requires setup cannot create '
839
+ + 'groups (403 CREATE_GROUP_CREATOR_SETUP_REQUIRED). Under MLS, membership '
830
840
  + 'changes are Add/Remove proposals + Commit — a group operation is a '
831
841
  + 'cryptographic state change, not a codec swap.',
832
842
  required: ['name', 'memberIds'],
@@ -844,24 +854,28 @@ const create_group_input = {
844
854
  };
845
855
  const create_group_result = {
846
856
  type: 'object',
847
- description: 'Staged admission: the group exists with the directly-addable members; '
848
- + '`pending` lists approval-required members whose group_invite awaits '
849
- + 'their approver; `skipped` lists members whose policy denied the add '
850
- + '(reasons include owner-only, blocked, not-found, setup-required).',
851
- required: ['status', 'conversationId'],
857
+ description: 'Staged admission: the group exists with the directly-added members; '
858
+ + '`pending` lists group_invite requests awaiting policy approval, owner '
859
+ + 'session setup, or both; `skipped` lists hard-denied targets.',
860
+ required: ['status', 'conversationId', 'added', 'pending', 'skipped'],
852
861
  additionalProperties: true,
853
862
  properties: {
854
863
  status: { const: 'created' },
855
864
  conversationId: { type: 'string' },
865
+ added: {
866
+ type: 'array',
867
+ items: { type: 'string' },
868
+ },
856
869
  pending: {
857
870
  type: 'array',
858
871
  items: {
859
872
  type: 'object',
860
- required: ['userId', 'requestId'],
873
+ required: ['userId', 'requestId', 'requirements'],
861
874
  additionalProperties: true,
862
875
  properties: {
863
876
  userId: { type: 'string' },
864
877
  requestId: { type: 'string' },
878
+ requirements: REF('groupInviteRequirements'),
865
879
  },
866
880
  },
867
881
  },
@@ -881,11 +895,10 @@ const create_group_result = {
881
895
  };
882
896
  const add_member_input = {
883
897
  type: 'object',
884
- description: 'v1 scoping: a coding-agent target that requires explicit session setup '
885
- + 'is rejected (only its owner can set it up, from the app). Under MLS '
886
- + 'an add is an Add proposal + Commit (new epoch), not a server-side '
887
- + 'membership write — the verb semantics are stable, the mechanism is '
888
- + 'not.',
898
+ description: 'A target needing policy approval and/or explicit owner session setup '
899
+ + 'yields one pending group_invite. Under MLS an add is an Add proposal '
900
+ + '+ Commit (new epoch), not a server-side membership write — the verb '
901
+ + 'semantics are stable, the mechanism is not.',
889
902
  required: ['conversationId', 'userId'],
890
903
  additionalProperties: false,
891
904
  properties: {
@@ -894,8 +907,8 @@ const add_member_input = {
894
907
  },
895
908
  };
896
909
  const add_member_result = {
897
- description: 'approval-required targets yield a pending group_invite contact request '
898
- + 'routed to their approver.',
910
+ description: 'Targets needing policy approval and/or owner session setup yield one '
911
+ + 'pending group_invite routed to the target or agent owner.',
899
912
  oneOf: [
900
913
  {
901
914
  type: 'object',
@@ -905,9 +918,13 @@ const add_member_result = {
905
918
  },
906
919
  {
907
920
  type: 'object',
908
- required: ['status', 'requestId'],
921
+ required: ['status', 'requestId', 'requirements'],
909
922
  additionalProperties: true,
910
- properties: { status: { const: 'pending' }, requestId: { type: 'string' } },
923
+ properties: {
924
+ status: { const: 'pending' },
925
+ requestId: { type: 'string' },
926
+ requirements: REF('groupInviteRequirements'),
927
+ },
911
928
  },
912
929
  ],
913
930
  };
@@ -1065,6 +1082,11 @@ const no_reply_input = {
1065
1082
  additionalProperties: false,
1066
1083
  properties: {
1067
1084
  conversationId: { type: 'string', pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId },
1085
+ messageId: {
1086
+ type: 'string',
1087
+ pattern: verbContract_js_1.VERB_ID_PATTERNS.runtimeId,
1088
+ description: 'The triggering message this silence answers, so the record cannot be misread against a later message.',
1089
+ },
1068
1090
  reason: {
1069
1091
  type: 'string',
1070
1092
  maxLength: verbContract_js_1.VERB_LIMITS.noReplyReasonChars,
@@ -1095,6 +1117,7 @@ exports.CANON_VERBS_JSON_SCHEMA = {
1095
1117
  + 'deliberately NOT verbs — they stay host-mediated.',
1096
1118
  $defs: {
1097
1119
  messageId: messageIdDef,
1120
+ groupInviteRequirements: groupInviteRequirementsDef,
1098
1121
  selfContext: selfContextDef,
1099
1122
  sessionSelection: sessionSelectionDef,
1100
1123
  mediaAttachment: mediaAttachmentDef,
@@ -118,7 +118,7 @@ exports.VERB_WIRE_ENVELOPE_FIELDS = {
118
118
  list_contacts: { required: [], optional: [] },
119
119
  list_contact_requests: { required: [], optional: [] },
120
120
  list_conversations: { required: [], optional: ['limit'] },
121
- no_reply: { required: [], optional: ['conversationId'] },
121
+ no_reply: { required: [], optional: ['conversationId', 'messageId'] },
122
122
  };
123
123
  // ---------------------------------------------------------------------------
124
124
  // JSON Schema
@@ -478,7 +478,7 @@ function projectVerbIntentToWire(verb, intent, options) {
478
478
  break;
479
479
  }
480
480
  case 'no_reply': {
481
- envelope = compact({ conversationId: input.conversationId });
481
+ envelope = compact({ conversationId: input.conversationId, messageId: input.messageId });
482
482
  value = compact({ reason: input.reason });
483
483
  break;
484
484
  }
@@ -620,7 +620,7 @@ function mergeVerbWireToIntent(request) {
620
620
  case 'list_conversations':
621
621
  return compact({ limit: envelope.limit });
622
622
  case 'no_reply':
623
- return compact({ conversationId: envelope.conversationId, reason: value.reason });
623
+ return compact({ conversationId: envelope.conversationId, messageId: envelope.messageId, reason: value.reason });
624
624
  }
625
625
  }
626
626
  /**
@@ -1,4 +1,13 @@
1
1
  export type SerializedContactRequestStatus = 'pending' | 'approved' | 'rejected' | 'expired';
2
+ /**
3
+ * Independent gates that must be satisfied before a pending group invite can
4
+ * add its target.
5
+ */
6
+ export interface GroupInviteRequirements {
7
+ policyApproval: boolean;
8
+ ownerSessionSetup: boolean;
9
+ }
10
+ export declare function readGroupInviteRequirements(value: unknown): GroupInviteRequirements;
2
11
  export interface SerializedContactRequest {
3
12
  id: string;
4
13
  requesterId: string;
@@ -18,6 +27,8 @@ export interface SerializedContactRequest {
18
27
  conversationId: string;
19
28
  groupName: string | null;
20
29
  };
30
+ /** Present for group_invite payloads; omitted for DM requests. */
31
+ requirements?: GroupInviteRequirements;
21
32
  createdAt: string | null;
22
33
  resolvedAt?: string | null;
23
34
  expiresAt?: string | null;
@@ -1,3 +1,14 @@
1
+ export function readGroupInviteRequirements(value) {
2
+ const record = isRecord(value) ? value : null;
3
+ if (typeof record?.policyApproval !== 'boolean'
4
+ || typeof record.ownerSessionSetup !== 'boolean') {
5
+ throw new TypeError('Invalid group invite requirements');
6
+ }
7
+ return {
8
+ policyApproval: record.policyApproval,
9
+ ownerSessionSetup: record.ownerSessionSetup,
10
+ };
11
+ }
1
12
  function isRecord(value) {
2
13
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3
14
  }
@@ -82,7 +93,15 @@ export function serializeContactRequest(requestId, data) {
82
93
  expiresAt: normalizeTimestamp(data.expiresAt),
83
94
  };
84
95
  if (groupContext) {
96
+ let requirements;
97
+ try {
98
+ requirements = readGroupInviteRequirements(data.requirements);
99
+ }
100
+ catch {
101
+ return null;
102
+ }
85
103
  payload.groupContext = groupContext;
104
+ payload.requirements = requirements;
86
105
  }
87
106
  if (data.targetUserType === 'ai_agent') {
88
107
  payload.targetOwnerId = data.targetOwnerId;
@@ -42,6 +42,7 @@
42
42
  * `findVerbByteLimitViolations()` (or equivalent checks from the emitted
43
43
  * canon-verbs.limits.json) after schema validation.
44
44
  */
45
+ import type { GroupInviteRequirements } from './contactRequest.js';
45
46
  /** Identifier for this contract document. */
46
47
  export declare const CANON_VERBS_SCHEMA_VERSION = "canon.verbs.v1";
47
48
  /** $id of the emitted JSON Schema bundle. */
@@ -602,27 +603,29 @@ export interface CreateGroupInput {
602
603
  * groupJoinPolicy is enforced server-side. */
603
604
  memberIds: string[];
604
605
  }
606
+ export interface PendingGroupInviteResult {
607
+ userId: string;
608
+ requestId: string;
609
+ requirements: GroupInviteRequirements;
610
+ }
605
611
  export interface CreateGroupResult {
606
612
  status: 'created';
607
613
  conversationId: string;
608
- /** approval-required members — group_invite requests awaiting their approver. */
609
- pending?: Array<{
610
- userId: string;
611
- requestId: string;
612
- }>;
613
- /** members the staged create could not admit: policy denials (owner-only,
614
- * blocked, not-found, …) and setup-required coding agents (v1 scoping —
615
- * only their owner can provide session setup, from the app). */
616
- skipped?: Array<{
614
+ /** Members admitted synchronously during creation. */
615
+ added: string[];
616
+ /** group_invite requests awaiting policy approval, owner setup, or both. */
617
+ pending: PendingGroupInviteResult[];
618
+ /** Members the staged create could not admit: hard policy denials
619
+ * (owner-only, blocked, inactive, not-found, …). */
620
+ skipped: Array<{
617
621
  userId: string;
618
622
  reason: string;
619
623
  }>;
620
624
  }
621
625
  /**
622
- * Error code on the 400 a create_group receives when NO member is directly
623
- * addable (approval-required members can only be invited to an existing
624
- * group; a creator-only group is not created). The error detail carries the
625
- * partition: pendingRequired[] and skipped[].
626
+ * Error code on the 400 a create_group receives when no requested member can
627
+ * be added or invited. A creator-only group with at least one valid pending
628
+ * invite is allowed; the error detail carries the attempted partition.
626
629
  */
627
630
  export declare const CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = "CREATE_GROUP_NO_ADDABLE_MEMBERS";
628
631
  /**
@@ -637,12 +640,13 @@ export interface AddMemberInput {
637
640
  conversationId: string;
638
641
  userId: string;
639
642
  }
640
- /** approval-required targets yield a pending group_invite contact request. */
643
+ /** Policy approval and/or required owner setup yield one pending group invite. */
641
644
  export type AddMemberResult = {
642
645
  status: 'added';
643
646
  } | {
644
647
  status: 'pending';
645
648
  requestId: string;
649
+ requirements: GroupInviteRequirements;
646
650
  };
647
651
  export interface RemoveMemberInput {
648
652
  conversationId: string;
@@ -260,10 +260,9 @@ export function normalizeVerbNativeMetadata(value) {
260
260
  return Object.keys(native).length > 0 ? native : undefined;
261
261
  }
262
262
  /**
263
- * Error code on the 400 a create_group receives when NO member is directly
264
- * addable (approval-required members can only be invited to an existing
265
- * group; a creator-only group is not created). The error detail carries the
266
- * partition: pendingRequired[] and skipped[].
263
+ * Error code on the 400 a create_group receives when no requested member can
264
+ * be added or invited. A creator-only group with at least one valid pending
265
+ * invite is allowed; the error detail carries the attempted partition.
267
266
  */
268
267
  export const CREATE_GROUP_NO_ADDABLE_MEMBERS_CODE = 'CREATE_GROUP_NO_ADDABLE_MEMBERS';
269
268
  /**
@@ -183,6 +183,20 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
183
183
  };
184
184
  readonly description: string;
185
185
  };
186
+ readonly groupInviteRequirements: {
187
+ readonly type: "object";
188
+ readonly description: string;
189
+ readonly required: readonly ["policyApproval", "ownerSessionSetup"];
190
+ readonly additionalProperties: false;
191
+ readonly properties: {
192
+ readonly policyApproval: {
193
+ readonly type: "boolean";
194
+ };
195
+ readonly ownerSessionSetup: {
196
+ readonly type: "boolean";
197
+ };
198
+ };
199
+ };
186
200
  readonly selfContext: {
187
201
  readonly type: "object";
188
202
  readonly description: string;
@@ -1404,7 +1418,7 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
1404
1418
  readonly create_group_result: {
1405
1419
  readonly type: "object";
1406
1420
  readonly description: string;
1407
- readonly required: readonly ["status", "conversationId"];
1421
+ readonly required: readonly ["status", "conversationId", "added", "pending", "skipped"];
1408
1422
  readonly additionalProperties: true;
1409
1423
  readonly properties: {
1410
1424
  readonly status: {
@@ -1413,11 +1427,17 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
1413
1427
  readonly conversationId: {
1414
1428
  readonly type: "string";
1415
1429
  };
1430
+ readonly added: {
1431
+ readonly type: "array";
1432
+ readonly items: {
1433
+ readonly type: "string";
1434
+ };
1435
+ };
1416
1436
  readonly pending: {
1417
1437
  readonly type: "array";
1418
1438
  readonly items: {
1419
1439
  readonly type: "object";
1420
- readonly required: readonly ["userId", "requestId"];
1440
+ readonly required: readonly ["userId", "requestId", "requirements"];
1421
1441
  readonly additionalProperties: true;
1422
1442
  readonly properties: {
1423
1443
  readonly userId: {
@@ -1426,6 +1446,9 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
1426
1446
  readonly requestId: {
1427
1447
  readonly type: "string";
1428
1448
  };
1449
+ readonly requirements: {
1450
+ $ref: string;
1451
+ };
1429
1452
  };
1430
1453
  };
1431
1454
  };
@@ -1476,7 +1499,7 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
1476
1499
  };
1477
1500
  }, {
1478
1501
  readonly type: "object";
1479
- readonly required: readonly ["status", "requestId"];
1502
+ readonly required: readonly ["status", "requestId", "requirements"];
1480
1503
  readonly additionalProperties: true;
1481
1504
  readonly properties: {
1482
1505
  readonly status: {
@@ -1485,6 +1508,9 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
1485
1508
  readonly requestId: {
1486
1509
  readonly type: "string";
1487
1510
  };
1511
+ readonly requirements: {
1512
+ $ref: string;
1513
+ };
1488
1514
  };
1489
1515
  }];
1490
1516
  };
@@ -1709,6 +1735,11 @@ export declare const CANON_VERBS_JSON_SCHEMA: {
1709
1735
  readonly type: "string";
1710
1736
  readonly pattern: "^[A-Za-z0-9_.:-]{1,160}$";
1711
1737
  };
1738
+ readonly messageId: {
1739
+ readonly type: "string";
1740
+ readonly pattern: "^[A-Za-z0-9_.:-]{1,160}$";
1741
+ readonly description: "The triggering message this silence answers, so the record cannot be misread against a later message.";
1742
+ };
1712
1743
  readonly reason: {
1713
1744
  readonly type: "string";
1714
1745
  readonly maxLength: 500;
@@ -29,6 +29,18 @@
29
29
  */
30
30
  import { CANON_VERBS_SCHEMA_ID, CANON_VERB_NAMES, SELF_CONTEXT_TYPE, VERB_ID_PATTERNS, VERB_LIMITS, } from './verbContract.js';
31
31
  const REF = (def) => ({ $ref: `#/$defs/${def}` });
32
+ const groupInviteRequirementsDef = {
33
+ type: 'object',
34
+ description: 'Independent gates on a pending group invite. policyApproval is social '
35
+ + 'consent; ownerSessionSetup means the agent owner must choose the '
36
+ + 'runtime session configuration before membership is activated.',
37
+ required: ['policyApproval', 'ownerSessionSetup'],
38
+ additionalProperties: false,
39
+ properties: {
40
+ policyApproval: { type: 'boolean' },
41
+ ownerSessionSetup: { type: 'boolean' },
42
+ },
43
+ };
32
44
  const selfContextDef = {
33
45
  type: 'object',
34
46
  description: 'Private note-to-self attached to a cross-conversation send. Visible only to the '
@@ -815,13 +827,11 @@ const create_group_input = {
815
827
  description: "Create a group conversation. Each target's groupJoinPolicy is enforced "
816
828
  + 'server-side with staged admission: directly-addable members join at '
817
829
  + 'creation, approval-required members become pending group_invite '
818
- + 'requests, policy-denied members are skipped (see the result). At '
819
- + 'least one member must be directly addable — a creator-only group is '
820
- + 'rejected with error code CREATE_GROUP_NO_ADDABLE_MEMBERS. v1 scoping: '
821
- + 'coding-agent members that require explicit session setup are skipped '
822
- + '(reason setup-required), and a creator that itself requires setup '
823
- + 'cannot create groups (403 CREATE_GROUP_CREATOR_SETUP_REQUIRED); only '
824
- + 'the owner can set such agents up, from the app. Under MLS, membership '
830
+ + 'requests; agents needing explicit group-session setup become pending '
831
+ + 'owner requests; hard-denied members are skipped (see the result). A '
832
+ + 'creator-only group is valid when at least one requested member has a '
833
+ + 'pending invite. A creator that itself requires setup cannot create '
834
+ + 'groups (403 CREATE_GROUP_CREATOR_SETUP_REQUIRED). Under MLS, membership '
825
835
  + 'changes are Add/Remove proposals + Commit — a group operation is a '
826
836
  + 'cryptographic state change, not a codec swap.',
827
837
  required: ['name', 'memberIds'],
@@ -839,24 +849,28 @@ const create_group_input = {
839
849
  };
840
850
  const create_group_result = {
841
851
  type: 'object',
842
- description: 'Staged admission: the group exists with the directly-addable members; '
843
- + '`pending` lists approval-required members whose group_invite awaits '
844
- + 'their approver; `skipped` lists members whose policy denied the add '
845
- + '(reasons include owner-only, blocked, not-found, setup-required).',
846
- required: ['status', 'conversationId'],
852
+ description: 'Staged admission: the group exists with the directly-added members; '
853
+ + '`pending` lists group_invite requests awaiting policy approval, owner '
854
+ + 'session setup, or both; `skipped` lists hard-denied targets.',
855
+ required: ['status', 'conversationId', 'added', 'pending', 'skipped'],
847
856
  additionalProperties: true,
848
857
  properties: {
849
858
  status: { const: 'created' },
850
859
  conversationId: { type: 'string' },
860
+ added: {
861
+ type: 'array',
862
+ items: { type: 'string' },
863
+ },
851
864
  pending: {
852
865
  type: 'array',
853
866
  items: {
854
867
  type: 'object',
855
- required: ['userId', 'requestId'],
868
+ required: ['userId', 'requestId', 'requirements'],
856
869
  additionalProperties: true,
857
870
  properties: {
858
871
  userId: { type: 'string' },
859
872
  requestId: { type: 'string' },
873
+ requirements: REF('groupInviteRequirements'),
860
874
  },
861
875
  },
862
876
  },
@@ -876,11 +890,10 @@ const create_group_result = {
876
890
  };
877
891
  const add_member_input = {
878
892
  type: 'object',
879
- description: 'v1 scoping: a coding-agent target that requires explicit session setup '
880
- + 'is rejected (only its owner can set it up, from the app). Under MLS '
881
- + 'an add is an Add proposal + Commit (new epoch), not a server-side '
882
- + 'membership write — the verb semantics are stable, the mechanism is '
883
- + 'not.',
893
+ description: 'A target needing policy approval and/or explicit owner session setup '
894
+ + 'yields one pending group_invite. Under MLS an add is an Add proposal '
895
+ + '+ Commit (new epoch), not a server-side membership write — the verb '
896
+ + 'semantics are stable, the mechanism is not.',
884
897
  required: ['conversationId', 'userId'],
885
898
  additionalProperties: false,
886
899
  properties: {
@@ -889,8 +902,8 @@ const add_member_input = {
889
902
  },
890
903
  };
891
904
  const add_member_result = {
892
- description: 'approval-required targets yield a pending group_invite contact request '
893
- + 'routed to their approver.',
905
+ description: 'Targets needing policy approval and/or owner session setup yield one '
906
+ + 'pending group_invite routed to the target or agent owner.',
894
907
  oneOf: [
895
908
  {
896
909
  type: 'object',
@@ -900,9 +913,13 @@ const add_member_result = {
900
913
  },
901
914
  {
902
915
  type: 'object',
903
- required: ['status', 'requestId'],
916
+ required: ['status', 'requestId', 'requirements'],
904
917
  additionalProperties: true,
905
- properties: { status: { const: 'pending' }, requestId: { type: 'string' } },
918
+ properties: {
919
+ status: { const: 'pending' },
920
+ requestId: { type: 'string' },
921
+ requirements: REF('groupInviteRequirements'),
922
+ },
906
923
  },
907
924
  ],
908
925
  };
@@ -1060,6 +1077,11 @@ const no_reply_input = {
1060
1077
  additionalProperties: false,
1061
1078
  properties: {
1062
1079
  conversationId: { type: 'string', pattern: VERB_ID_PATTERNS.runtimeId },
1080
+ messageId: {
1081
+ type: 'string',
1082
+ pattern: VERB_ID_PATTERNS.runtimeId,
1083
+ description: 'The triggering message this silence answers, so the record cannot be misread against a later message.',
1084
+ },
1063
1085
  reason: {
1064
1086
  type: 'string',
1065
1087
  maxLength: VERB_LIMITS.noReplyReasonChars,
@@ -1090,6 +1112,7 @@ export const CANON_VERBS_JSON_SCHEMA = {
1090
1112
  + 'deliberately NOT verbs — they stay host-mediated.',
1091
1113
  $defs: {
1092
1114
  messageId: messageIdDef,
1115
+ groupInviteRequirements: groupInviteRequirementsDef,
1093
1116
  selfContext: selfContextDef,
1094
1117
  sessionSelection: sessionSelectionDef,
1095
1118
  mediaAttachment: mediaAttachmentDef,
package/dist/verbWire.js CHANGED
@@ -112,7 +112,7 @@ export const VERB_WIRE_ENVELOPE_FIELDS = {
112
112
  list_contacts: { required: [], optional: [] },
113
113
  list_contact_requests: { required: [], optional: [] },
114
114
  list_conversations: { required: [], optional: ['limit'] },
115
- no_reply: { required: [], optional: ['conversationId'] },
115
+ no_reply: { required: [], optional: ['conversationId', 'messageId'] },
116
116
  };
117
117
  // ---------------------------------------------------------------------------
118
118
  // JSON Schema
@@ -472,7 +472,7 @@ export function projectVerbIntentToWire(verb, intent, options) {
472
472
  break;
473
473
  }
474
474
  case 'no_reply': {
475
- envelope = compact({ conversationId: input.conversationId });
475
+ envelope = compact({ conversationId: input.conversationId, messageId: input.messageId });
476
476
  value = compact({ reason: input.reason });
477
477
  break;
478
478
  }
@@ -614,7 +614,7 @@ export function mergeVerbWireToIntent(request) {
614
614
  case 'list_conversations':
615
615
  return compact({ limit: envelope.limit });
616
616
  case 'no_reply':
617
- return compact({ conversationId: envelope.conversationId, reason: value.reason });
617
+ return compact({ conversationId: envelope.conversationId, messageId: envelope.messageId, reason: value.reason });
618
618
  }
619
619
  }
620
620
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/backend-contracts",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "description": "Canon backend contract helpers shared by Functions and stream-service",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",