@consiliency/agent-board-schema 1.68.0 → 1.74.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.
Files changed (155) hide show
  1. package/fixtures/already-applied.json +28 -4
  2. package/migrations/20261026000100_agent_board_thread_association.sql +1287 -0
  3. package/migrations/20261101000100_agent_board_session_measured_mint.sql +145 -0
  4. package/migrations/20261104065658_agent_board_reply_delivery.sql +659 -0
  5. package/migrations/20261104170000_agent_board_tasking_replay_scope.sql +320 -0
  6. package/migrations/20261104180000_agent_board_delivery_writer_execute.sql +51 -0
  7. package/migrations/20261104190000_agent_board_message_replay_scope.sql +373 -0
  8. package/package.json +1 -1
  9. package/schema-manifest.json +57 -3
  10. package/src/tooling.test.mjs +8 -2
  11. package/tests/agent_board_661_reply_delivery.test.sql +185 -0
  12. package/tests/agent_board_667_boundary.test.sql +162 -0
  13. package/tests/agent_board_667_conversion_branches.test.sql +91 -0
  14. package/tests/agent_board_667_conversion_profile.test.sql +112 -0
  15. package/tests/agent_board_667_history_class.test.sql +785 -0
  16. package/tests/agent_board_667_native_cast_effect.test.sql +88 -0
  17. package/tests/agent_board_667_realtime_apply_behavior.test.sql +117 -0
  18. package/tests/agent_board_667_realtime_apply_controls.test.sql +143 -0
  19. package/tests/agent_board_667_realtime_apply_profile.test.sql +61 -0
  20. package/tests/agent_board_667_retention.test.sql +201 -0
  21. package/tests/agent_board_667_routing.test.sql +294 -0
  22. package/tests/agent_board_667_sender_effects.test.sql +100 -0
  23. package/tests/agent_board_667_sender_profile_controls.test.sql +29 -0
  24. package/tests/agent_board_667_storage_delimiter_behavior.test.sql +84 -0
  25. package/tests/agent_board_667_storage_delimiter_controls.test.sql +23 -0
  26. package/tests/agent_board_667_storage_delimiter_profile.test.sql +61 -0
  27. package/tests/agent_board_667_storage_search_behavior.test.sql +93 -0
  28. package/tests/agent_board_667_storage_search_controls.test.sql +22 -0
  29. package/tests/agent_board_667_storage_search_profile.test.sql +60 -0
  30. package/tests/agent_board_667_storage_timestamp_behavior.test.sql +58 -0
  31. package/tests/agent_board_667_storage_timestamp_controls.test.sql +22 -0
  32. package/tests/agent_board_667_storage_timestamp_profile.test.sql +54 -0
  33. package/tests/agent_board_667_writers.test.sql +281 -0
  34. package/tests/agent_board_717_message_replay_scope.test.sql +828 -0
  35. package/tests/agent_board_717_tasking_replay_scope.test.sql +661 -0
  36. package/tests/agent_board_addressing.test.sql +8 -5
  37. package/tests/agent_board_claim_subject_gate.test.sql +7 -3
  38. package/tests/agent_board_claimpolicy_race.test.sql +39 -11
  39. package/tests/agent_board_delegate_role_class.test.sql +33 -15
  40. package/tests/agent_board_delivery_writer_execute.test.sql +371 -0
  41. package/tests/agent_board_fk_lock_edges.test.sql +3 -0
  42. package/tests/agent_board_hosted_delegates.test.sql +5 -2
  43. package/tests/agent_board_inbox_rpcs.test.sql +5 -3
  44. package/tests/agent_board_obsgate.test.sql +8 -4
  45. package/tests/agent_board_obsgate4.test.sql +2 -2
  46. package/tests/agent_board_outbox_wake_ordering.test.sql +25 -2
  47. package/tests/agent_board_own_post_not_delivered.test.sql +11 -9
  48. package/tests/agent_board_session_identity_behavior_classes.test.sql +2 -2
  49. package/tests/agent_board_session_identity_limits.test.sql +1 -1
  50. package/tests/agent_board_session_identity_measured_mint.test.sql +342 -0
  51. package/tests/agent_board_session_identity_predicate_isolation.test.sql +146 -0
  52. package/tests/agent_board_sidserver_authority.test.sql +69 -0
  53. package/tests/agent_board_sidserver_catalog.test.sql +47 -0
  54. package/tests/agent_board_signed_write_gateway.test.sql +4 -2
  55. package/tests/agent_board_task_thread_lock_order.test.sql +67 -42
  56. package/tests/agent_board_v2_invariants.test.sql +5 -2
  57. package/tests/agent_board_write_enforcement_class.test.sql +3 -3
  58. package/tests/catalog/function_execute_privileges.json +41 -5
  59. package/tests/catalog/p_metadata_parameters.json +16 -0
  60. package/tests/fixtures/agent_board_667_apply_rls_guard.sql +195 -0
  61. package/tests/fixtures/agent_board_667_apply_rls_provenance.md +125 -0
  62. package/tests/fixtures/agent_board_667_apply_rls_reference.sql +1903 -0
  63. package/tests/fixtures/agent_board_667_conversion_guard.sql +162 -0
  64. package/tests/fixtures/agent_board_667_conversion_reference.md +23 -0
  65. package/tests/fixtures/agent_board_667_conversion_reference.sql +5325 -0
  66. package/tests/fixtures/agent_board_667_delimiter_guard.sql +92 -0
  67. package/tests/fixtures/agent_board_667_delimiter_provenance.md +116 -0
  68. package/tests/fixtures/agent_board_667_delimiter_reference.sql +10150 -0
  69. package/tests/fixtures/agent_board_667_search_guard.sql +92 -0
  70. package/tests/fixtures/agent_board_667_search_provenance.md +141 -0
  71. package/tests/fixtures/agent_board_667_search_reference.sql +4779 -0
  72. package/tests/fixtures/agent_board_667_sender_guard.sql +102 -0
  73. package/tests/fixtures/agent_board_667_sender_reference.md +20 -0
  74. package/tests/fixtures/agent_board_667_sender_reference.sql +875 -0
  75. package/tests/fixtures/agent_board_667_timestamp_graph.sql +84 -0
  76. package/tests/fixtures/agent_board_667_timestamp_guard.sql +94 -0
  77. package/tests/fixtures/agent_board_667_timestamp_provenance.md +99 -0
  78. package/tests/fixtures/agent_board_667_timestamp_reference.sql +2938 -0
  79. package/tests/isolation/expected/association_artifact_anchor_order.out +108 -0
  80. package/tests/isolation/expected/association_board_vs_board.out +71 -0
  81. package/tests/isolation/expected/association_post_wait_authority.out +77 -0
  82. package/tests/isolation/expected/association_strong_history_writers.out +203 -0
  83. package/tests/isolation/expected/association_strong_isolation.out +185 -0
  84. package/tests/isolation/expected/association_vs_artifact_claim_escalation.out +199 -0
  85. package/tests/isolation/expected/association_vs_heartbeat.out +78 -0
  86. package/tests/isolation/expected/association_vs_multi_anchor_artifact.out +67 -0
  87. package/tests/isolation/expected/association_vs_private_message.out +67 -0
  88. package/tests/isolation/expected/association_vs_task_and_conflict.out +133 -0
  89. package/tests/isolation/expected/association_vs_taskful_cross_thread.out +133 -0
  90. package/tests/isolation/expected/message_replay_scope.out +209 -0
  91. package/tests/isolation/expected/tasking_replay_scope.out +125 -0
  92. package/tests/isolation/specs/association_artifact_anchor_order.spec +46 -0
  93. package/tests/isolation/specs/association_board_vs_board.spec +37 -0
  94. package/tests/isolation/specs/association_post_wait_authority.spec +48 -0
  95. package/tests/isolation/specs/association_strong_history_writers.spec +61 -0
  96. package/tests/isolation/specs/association_strong_isolation.spec +50 -0
  97. package/tests/isolation/specs/association_vs_artifact_claim_escalation.spec +41 -0
  98. package/tests/isolation/specs/association_vs_heartbeat.spec +47 -0
  99. package/tests/isolation/specs/association_vs_multi_anchor_artifact.spec +38 -0
  100. package/tests/isolation/specs/association_vs_private_message.spec +33 -0
  101. package/tests/isolation/specs/association_vs_task_and_conflict.spec +38 -0
  102. package/tests/isolation/specs/association_vs_taskful_cross_thread.spec +42 -0
  103. package/tests/isolation/specs/message_replay_scope.spec +77 -0
  104. package/tests/isolation/specs/tasking_replay_scope.spec +72 -0
  105. package/tests/mutations/717-f1/M0_prefix_main_acl_and_comments.sql +11 -0
  106. package/tests/mutations/717-f1/M10_wrapper_stops_delegating.sql +7 -0
  107. package/tests/mutations/717-f1/M11_transitive_recipient_content.sql +8 -0
  108. package/tests/mutations/717-f1/M12_transitive_recipient_visibility.sql +8 -0
  109. package/tests/mutations/717-f1/M13_transitive_worker_content.sql +15 -0
  110. package/tests/mutations/717-f1/M1a_regrant_upsert_delivery_recipient.sql +1 -0
  111. package/tests/mutations/717-f1/M1b_regrant_upsert_inbox_item.sql +1 -0
  112. package/tests/mutations/717-f1/M1c_regrant_queue_inbox_callbacks.sql +1 -0
  113. package/tests/mutations/717-f1/M1d_regrant_queue_board_post_callbacks.sql +1 -0
  114. package/tests/mutations/717-f1/M1e_regrant_next_thread_message_sequence.sql +1 -0
  115. package/tests/mutations/717-f1/M2_blanket_schema_grant.sql +1 -0
  116. package/tests/mutations/717-f1/M3_new_overload_of_sink.sql +10 -0
  117. package/tests/mutations/717-f1/M4_new_route_calling_sink.sql +7 -0
  118. package/tests/mutations/717-f1/M5_actor_trusts_supplied_id.sql +5 -0
  119. package/tests/mutations/717-f1/M6_signed_route_granted_to_authenticated.sql +1 -0
  120. package/tests/mutations/717-f1/M7_worker_granted_to_authenticated.sql +1 -0
  121. package/tests/mutations/717-f1/M8_recipient_state_rewrites_visibility.sql +5 -0
  122. package/tests/mutations/717-f1/M9_public_route_revoked_from_service_role.sql +1 -0
  123. package/tests/mutations/717-f1/outbox_wake_ordering/W1_regrant_queue_inbox_callbacks.sql +1 -0
  124. package/tests/mutations/717-f1/outbox_wake_ordering/W2_revoke_worker_from_service_role.sql +1 -0
  125. package/tests/mutations/717-f1/outbox_wake_ordering/W3_invoker_caller_of_sink.sql +1 -0
  126. package/tests/mutations/717-f1/outbox_wake_ordering/W4_public_regrant_board_post_sink.sql +1 -0
  127. package/tests/mutations/717-f1/run.sh +17 -0
  128. package/tests/mutations/717-f2b/C1_new_delivery_route.sql +4 -0
  129. package/tests/mutations/717-f2b/C2_new_replay_lookup.sql +3 -0
  130. package/tests/mutations/717-f2b/C3_internal_regranted.sql +2 -0
  131. package/tests/mutations/717-f2b/C4_wrapper_writes_delivery.sql +9 -0
  132. package/tests/mutations/717-f2b/C5_class_member_dynamic_sql.sql +9 -0
  133. package/tests/mutations/717-f2b/C6_drop_board_scope_constraint.sql +2 -0
  134. package/tests/mutations/717-f2b/C7_state_private_helper_content.sql +14 -0
  135. package/tests/mutations/717-f2b/L1_dm_lock_split_by_recipient.sql +8 -0
  136. package/tests/mutations/717-f2b/L2_dm_lock_removed.sql +12 -0
  137. package/tests/mutations/717-f2b/L3_mc_lock_split_by_recipients.sql +8 -0
  138. package/tests/mutations/717-f2b/L4_mc_lock_removed.sql +12 -0
  139. package/tests/mutations/717-f2b/M0_prefix_definitions.sql +17 -0
  140. package/tests/mutations/717-f2b/M10_mc_lookup_any_author.sql +15 -0
  141. package/tests/mutations/717-f2b/M11_dm_swallow_refusal.sql +8 -0
  142. package/tests/mutations/717-f2b/M12_mc_swallow_refusal.sql +8 -0
  143. package/tests/mutations/717-f2b/M13_signed_dm_swallows_refusal.sql +32 -0
  144. package/tests/mutations/717-f2b/M14_signed_mc_swallows_refusal.sql +20 -0
  145. package/tests/mutations/717-f2b/M15_mc_any_source.sql +7 -0
  146. package/tests/mutations/717-f2b/M1_dm_drop_recipient_proof.sql +16 -0
  147. package/tests/mutations/717-f2b/M2_mc_drop_recipient_proof.sql +16 -0
  148. package/tests/mutations/717-f2b/M3_dm_metadata_as_proof.sql +12 -0
  149. package/tests/mutations/717-f2b/M4_mc_metadata_as_proof.sql +16 -0
  150. package/tests/mutations/717-f2b/M5_dm_any_mode.sql +11 -0
  151. package/tests/mutations/717-f2b/M6_dm_board_delivery_counts.sql +11 -0
  152. package/tests/mutations/717-f2b/M7_mc_any_mode.sql +11 -0
  153. package/tests/mutations/717-f2b/M8_dm_thread_stamp_as_proof.sql +12 -0
  154. package/tests/mutations/717-f2b/M9_dm_lookup_any_author.sql +15 -0
  155. package/tests/mutations/717-f2b/run.sh +82 -0
@@ -0,0 +1,373 @@
1
+ -- #717 F2b: a same-key send_direct_message / send_multicast_message replay cannot widen the
2
+ -- source message's audience.
3
+ --
4
+ -- Idempotency identity is unchanged: (rpc, thread, resolved author, trimmed key), the same
5
+ -- advisory lock domain and the same lookup. Before this migration a replay reused the author's
6
+ -- ORIGINAL message and delivered it to whatever recipient the retry named, so a newly named agent
7
+ -- could read the original body and every row anchored to it (an artifact another agent attached).
8
+ -- Now, on the replay branch and before any delivery, inbox or callback effect, every requested
9
+ -- recipient must already hold a persisted private delivery of that source message in the
10
+ -- function's own mode. Otherwise: 42501 'message replay cannot widen delivery scope'.
11
+ --
12
+ -- Only the two UUID cores change. The addressing overloads and the signed wrappers route through
13
+ -- them and inherit the refusal and its atomic rollback. Definitions are this tree's current ones
14
+ -- (20261104065658_agent_board_reply_delivery.sql) with the ceiling inserted; nothing else moves.
15
+ --
16
+ -- Compatibility limit, by design: a source message whose delivery rows are missing (deleted
17
+ -- recipient, historical corruption) cannot be replayed at all. Nothing reconstructs intent from
18
+ -- metadata. Use a new key.
19
+ --
20
+ -- Trust dependency: persisted deliveries are authoritative only once no API role can write them
21
+ -- directly (#717 F1, which revokes service_role EXECUTE on _upsert_delivery_recipient).
22
+
23
+ CREATE OR REPLACE FUNCTION agent_board.send_direct_message(p_thread_id uuid, p_author_agent_id uuid, p_recipient_agent_id uuid, p_subject text, p_body text, p_message_type text DEFAULT 'comment'::text, p_priority text DEFAULT 'medium'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
24
+ RETURNS TABLE(message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid, callback_outbox_count integer, created_at timestamp with time zone)
25
+ LANGUAGE plpgsql
26
+ SECURITY DEFINER
27
+ SET search_path TO 'agent_board', 'public'
28
+ AS $function$
29
+ declare
30
+ v_message agent_board.messages%rowtype;
31
+ v_created_message record;
32
+ v_recipient agent_board.delivery_recipients%rowtype;
33
+ v_inbox agent_board.inbox_items%rowtype;
34
+ v_callback_count integer := 0;
35
+ v_metadata jsonb;
36
+ v_preview text;
37
+ v_fetch_descriptor jsonb;
38
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
39
+ v_actor_agent_id uuid;
40
+ begin
41
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
42
+
43
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
44
+ raise exception 'subject and body are required' using errcode = '22023';
45
+ end if;
46
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
47
+ raise exception 'invalid priority' using errcode = '22023';
48
+ end if;
49
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
50
+ raise exception 'thread not found' using errcode = 'P0002';
51
+ end if;
52
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
53
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
54
+ end if;
55
+ if not exists (select 1 from agent_board.agents where id = p_recipient_agent_id) then
56
+ raise exception 'recipient agent not found' using errcode = 'P0002';
57
+ end if;
58
+
59
+ if agent_board._is_service_role() then
60
+ perform agent_board._record_send_authz_telemetry(
61
+ 'service_role_bypass',
62
+ coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
63
+ v_actor_agent_id,
64
+ null,
65
+ 'send_direct_message',
66
+ agent_board._resolve_send_fleet_scope(v_actor_agent_id),
67
+ null,
68
+ jsonb_build_object('redacted', true)
69
+ );
70
+ end if;
71
+
72
+ perform agent_board._lock_idempotency_key(
73
+ 'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
74
+ v_idempotency_key
75
+ );
76
+
77
+ if v_idempotency_key is not null then
78
+ select *
79
+ into v_message
80
+ from agent_board.messages
81
+ where thread_id = p_thread_id
82
+ and author_agent_id = v_actor_agent_id
83
+ and metadata ->> 'rpcName' = 'send_direct_message'
84
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
85
+ order by created_at desc
86
+ limit 1;
87
+ end if;
88
+
89
+ -- #717 F2b: a same-key replay may only re-deliver the source message to the recipient it was
90
+ -- already PRIVATELY delivered to. The proof is the persisted delivery row of THIS source
91
+ -- message in THIS function's mode, for THIS recipient, with no board scope. Metadata, the
92
+ -- thread's board stamp and any board-mode delivery are not evidence. Refused before any
93
+ -- replay effect, so the whole call (telemetry, signed evidence) rolls back. A new recipient
94
+ -- needs a new key, which creates a new message without the source's artifacts.
95
+ if v_message.id is not null and not exists (
96
+ select 1
97
+ from agent_board.delivery_recipients dr
98
+ where dr.source_kind = 'message'
99
+ and dr.source_message_id = v_message.id
100
+ and dr.delivery_mode = 'direct'
101
+ and dr.recipient_agent_id = p_recipient_agent_id
102
+ and dr.board_id is null
103
+ ) then
104
+ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';
105
+ end if;
106
+
107
+ if v_message.id is null then
108
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
109
+ || jsonb_build_object(
110
+ 'rpcName', 'send_direct_message',
111
+ 'subject', trim(p_subject),
112
+ 'priority', p_priority
113
+ );
114
+ if v_idempotency_key is not null then
115
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
116
+ end if;
117
+
118
+ select *
119
+ into v_created_message
120
+ from agent_board._insert_thread_message(
121
+ p_thread_id,
122
+ v_actor_agent_id,
123
+ p_body,
124
+ p_message_type,
125
+ null,
126
+ v_metadata
127
+ );
128
+
129
+ select *
130
+ into v_message
131
+ from agent_board.messages
132
+ where id = v_created_message.message_id;
133
+ end if;
134
+
135
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
136
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
137
+ 'thread',
138
+ p_thread_id,
139
+ 'agent_board.v_thread_feed',
140
+ 'direct delivery',
141
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', p_recipient_agent_id::text)
142
+ );
143
+
144
+ v_recipient := agent_board._upsert_delivery_recipient(
145
+ 'direct',
146
+ p_recipient_agent_id,
147
+ 'message',
148
+ v_message.id,
149
+ null,
150
+ null,
151
+ null,
152
+ null,
153
+ jsonb_build_object('priority', p_priority)
154
+ );
155
+
156
+ v_inbox := agent_board._upsert_inbox_item(
157
+ v_recipient.id,
158
+ p_recipient_agent_id,
159
+ null,
160
+ v_recipient.delivery_mode,
161
+ 'message',
162
+ v_message.id,
163
+ null,
164
+ null,
165
+ trim(p_subject),
166
+ v_preview,
167
+ p_priority,
168
+ true,
169
+ false,
170
+ v_fetch_descriptor,
171
+ jsonb_build_object('threadId', p_thread_id::text)
172
+ );
173
+
174
+ v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
175
+
176
+ return query
177
+ select v_message.id, v_recipient.id, v_inbox.id, v_callback_count, v_message.created_at;
178
+ end;
179
+ $function$;
180
+
181
+ CREATE OR REPLACE FUNCTION agent_board.send_multicast_message(p_thread_id uuid, p_author_agent_id uuid, p_recipient_agent_ids uuid[], p_subject text, p_body text, p_message_type text DEFAULT 'comment'::text, p_priority text DEFAULT 'medium'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
182
+ RETURNS TABLE(message_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamp with time zone)
183
+ LANGUAGE plpgsql
184
+ SECURITY DEFINER
185
+ SET search_path TO 'agent_board', 'public'
186
+ AS $function$
187
+ declare
188
+ v_message agent_board.messages%rowtype;
189
+ v_created_message record;
190
+ v_recipient_ids uuid[];
191
+ v_recipient_id uuid;
192
+ v_recipient agent_board.delivery_recipients%rowtype;
193
+ v_inbox agent_board.inbox_items%rowtype;
194
+ v_delivered_count integer := 0;
195
+ v_inbox_count integer := 0;
196
+ v_callback_count integer := 0;
197
+ v_metadata jsonb;
198
+ v_preview text;
199
+ v_fetch_descriptor jsonb;
200
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
201
+ v_actor_agent_id uuid;
202
+ begin
203
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
204
+
205
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
206
+ raise exception 'subject and body are required' using errcode = '22023';
207
+ end if;
208
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
209
+ raise exception 'invalid priority' using errcode = '22023';
210
+ end if;
211
+ if p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0 then
212
+ raise exception 'recipient_agent_ids must not be empty' using errcode = '22023';
213
+ end if;
214
+ if array_position(p_recipient_agent_ids, null) is not null then
215
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
216
+ end if;
217
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
218
+ raise exception 'thread not found' using errcode = 'P0002';
219
+ end if;
220
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
221
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
222
+ end if;
223
+
224
+ select array_agg(distinct recipient_id)
225
+ into v_recipient_ids
226
+ from unnest(p_recipient_agent_ids) as recipient_id;
227
+
228
+ if (
229
+ select count(*)
230
+ from agent_board.agents
231
+ where id = any(v_recipient_ids)
232
+ ) <> cardinality(v_recipient_ids) then
233
+ raise exception 'recipient agent not found' using errcode = 'P0002';
234
+ end if;
235
+
236
+ if agent_board._is_service_role() then
237
+ perform agent_board._record_send_authz_telemetry(
238
+ 'service_role_bypass',
239
+ coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
240
+ v_actor_agent_id,
241
+ null,
242
+ 'send_multicast_message',
243
+ agent_board._resolve_send_fleet_scope(v_actor_agent_id),
244
+ null,
245
+ jsonb_build_object('redacted', true)
246
+ );
247
+ end if;
248
+
249
+ perform agent_board._lock_idempotency_key(
250
+ 'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
251
+ v_idempotency_key
252
+ );
253
+
254
+ if v_idempotency_key is not null then
255
+ select *
256
+ into v_message
257
+ from agent_board.messages
258
+ where thread_id = p_thread_id
259
+ and author_agent_id = v_actor_agent_id
260
+ and metadata ->> 'rpcName' = 'send_multicast_message'
261
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
262
+ order by created_at desc
263
+ limit 1;
264
+ end if;
265
+
266
+ -- #717 F2b: a same-key replay may name the same normalized recipient set or a nonempty subset
267
+ -- of it, never a new recipient. Each requested recipient must already hold a persisted
268
+ -- multicast delivery of THIS source message with no board scope; an omitted recipient's
269
+ -- delivery is kept, not revoked. Metadata, the thread's board stamp and board-mode deliveries
270
+ -- are not evidence. Refused before any replay effect, so the whole call rolls back. A new
271
+ -- recipient needs a new key, which creates a new message without the source's artifacts.
272
+ if v_message.id is not null and exists (
273
+ select 1
274
+ from unnest(v_recipient_ids) as requested(recipient_id)
275
+ where not exists (
276
+ select 1
277
+ from agent_board.delivery_recipients dr
278
+ where dr.source_kind = 'message'
279
+ and dr.source_message_id = v_message.id
280
+ and dr.delivery_mode = 'multicast'
281
+ and dr.recipient_agent_id = requested.recipient_id
282
+ and dr.board_id is null
283
+ )
284
+ ) then
285
+ raise exception 'message replay cannot widen delivery scope' using errcode = '42501';
286
+ end if;
287
+
288
+ if v_message.id is null then
289
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
290
+ || jsonb_build_object(
291
+ 'rpcName', 'send_multicast_message',
292
+ 'subject', trim(p_subject),
293
+ 'priority', p_priority
294
+ );
295
+ if v_idempotency_key is not null then
296
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
297
+ end if;
298
+
299
+ select *
300
+ into v_created_message
301
+ from agent_board._insert_thread_message(
302
+ p_thread_id,
303
+ v_actor_agent_id,
304
+ p_body,
305
+ p_message_type,
306
+ null,
307
+ v_metadata
308
+ );
309
+
310
+ select *
311
+ into v_message
312
+ from agent_board.messages
313
+ where id = v_created_message.message_id;
314
+ end if;
315
+
316
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
317
+
318
+ foreach v_recipient_id in array v_recipient_ids loop
319
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
320
+ 'thread',
321
+ p_thread_id,
322
+ 'agent_board.v_thread_feed',
323
+ 'multicast delivery',
324
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', v_recipient_id::text)
325
+ );
326
+
327
+ v_recipient := agent_board._upsert_delivery_recipient(
328
+ 'multicast',
329
+ v_recipient_id,
330
+ 'message',
331
+ v_message.id,
332
+ null,
333
+ null,
334
+ null,
335
+ null,
336
+ jsonb_build_object('priority', p_priority)
337
+ );
338
+
339
+ v_inbox := agent_board._upsert_inbox_item(
340
+ v_recipient.id,
341
+ v_recipient_id,
342
+ null,
343
+ v_recipient.delivery_mode,
344
+ 'message',
345
+ v_message.id,
346
+ null,
347
+ null,
348
+ trim(p_subject),
349
+ v_preview,
350
+ p_priority,
351
+ true,
352
+ false,
353
+ v_fetch_descriptor,
354
+ jsonb_build_object('threadId', p_thread_id::text)
355
+ );
356
+
357
+ v_delivered_count := v_delivered_count + 1;
358
+ v_inbox_count := v_inbox_count + 1;
359
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
360
+ end loop;
361
+
362
+ return query
363
+ select v_message.id, v_delivered_count, v_inbox_count, v_callback_count, v_message.created_at;
364
+ end;
365
+ $function$;
366
+
367
+ revoke all on function agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) from public,anon,authenticated,service_role,agent_board_delegate;
368
+ grant execute on function agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) to authenticated,service_role;
369
+ comment on function agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb) is 'Sends privately to the explicit recipient, including on board threads; uses the common authorized insertion primitive. A same-key replay cannot widen the message''s audience: it is refused (42501) unless the recipient already holds a private direct delivery of the source message.';
370
+
371
+ revoke all on function agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb) from public,anon,authenticated,service_role,agent_board_delegate;
372
+ grant execute on function agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb) to authenticated,service_role;
373
+ comment on function agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb) is 'Sends privately to explicit recipients, including on board threads; uses the common authorized insertion primitive. A same-key replay cannot widen the message''s audience: it is refused (42501) unless every requested recipient already holds a private multicast delivery of the source message.';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-schema",
3
- "version": "1.68.0",
3
+ "version": "1.74.0",
4
4
  "description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
5
5
  "repository": "https://github.com/Consiliency/message-board.git",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  "schema": "agent_board_schema_manifest.v1",
3
3
  "interface": "IF-0-MBPLANE-BUNDLE-1",
4
4
  "package": "@consiliency/agent-board-schema",
5
- "packageVersion": "1.68.0",
5
+ "packageVersion": "1.74.0",
6
6
  "schemaVersion": 8,
7
7
  "minClientVersion": "1.1.0",
8
8
  "source": {
@@ -46,8 +46,8 @@
46
46
  "usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
47
47
  }
48
48
  ],
49
- "migrationCount": 166,
50
- "aggregateSha256": "c59fcf6ae0ce9cf567f68a27abee5c8a3a11d0d98ebc4d5cb07a496e7832964c",
49
+ "migrationCount": 172,
50
+ "aggregateSha256": "1787dd0d972779bbdc1645b67460fc2f2c7b910f128414cf39b6dfb2b00b3b42",
51
51
  "migrations": [
52
52
  {
53
53
  "order": 0,
@@ -1542,6 +1542,60 @@
1542
1542
  "sourceRef": "message-board",
1543
1543
  "bytes": 7396,
1544
1544
  "sha256": "bfeb7bda1eb41bc431416502db69c33156ff72acfd79cefdfd556d933f79d809"
1545
+ },
1546
+ {
1547
+ "order": 166,
1548
+ "id": "20261026000100_agent_board_thread_association",
1549
+ "filename": "20261026000100_agent_board_thread_association.sql",
1550
+ "origin": "message-board-forward",
1551
+ "sourceRef": "message-board",
1552
+ "bytes": 70992,
1553
+ "sha256": "93c3ffc694f5dbd3a5d34d5ef392462707f959119e8c0ff52677c74720dd2c6d"
1554
+ },
1555
+ {
1556
+ "order": 167,
1557
+ "id": "20261101000100_agent_board_session_measured_mint",
1558
+ "filename": "20261101000100_agent_board_session_measured_mint.sql",
1559
+ "origin": "message-board-forward",
1560
+ "sourceRef": "message-board",
1561
+ "bytes": 12087,
1562
+ "sha256": "b18323397aaacfb7be138f5befedb8cb9b6a0247b868bc22a2ed3481b5f17c06"
1563
+ },
1564
+ {
1565
+ "order": 168,
1566
+ "id": "20261104065658_agent_board_reply_delivery",
1567
+ "filename": "20261104065658_agent_board_reply_delivery.sql",
1568
+ "origin": "message-board-forward",
1569
+ "sourceRef": "message-board",
1570
+ "bytes": 27537,
1571
+ "sha256": "0f785d8c2b6dd389fec88851d88370095c2de5d07764b7c8ced277388a471660"
1572
+ },
1573
+ {
1574
+ "order": 169,
1575
+ "id": "20261104170000_agent_board_tasking_replay_scope",
1576
+ "filename": "20261104170000_agent_board_tasking_replay_scope.sql",
1577
+ "origin": "message-board-forward",
1578
+ "sourceRef": "message-board",
1579
+ "bytes": 13932,
1580
+ "sha256": "dc77bd6b017e067ab20aa5ecea7d31377420d8593d33e2731059c0cbba52e4c0"
1581
+ },
1582
+ {
1583
+ "order": 170,
1584
+ "id": "20261104180000_agent_board_delivery_writer_execute",
1585
+ "filename": "20261104180000_agent_board_delivery_writer_execute.sql",
1586
+ "origin": "message-board-forward",
1587
+ "sourceRef": "message-board",
1588
+ "bytes": 4567,
1589
+ "sha256": "732cd0b61706f1f901eeb88f3e3a30e91a34723b8df4cc972218ebca0859e07a"
1590
+ },
1591
+ {
1592
+ "order": 171,
1593
+ "id": "20261104190000_agent_board_message_replay_scope",
1594
+ "filename": "20261104190000_agent_board_message_replay_scope.sql",
1595
+ "origin": "message-board-forward",
1596
+ "sourceRef": "message-board",
1597
+ "bytes": 15050,
1598
+ "sha256": "dbfd60300e8ce8ed78df8ae851f3990f58bcb43a7f9d53c2e25499a22e775a67"
1545
1599
  }
1546
1600
  ],
1547
1601
  "excludedBridge": {
@@ -41,9 +41,9 @@ describe('manifest integrity', () => {
41
41
  expect(manifest.schemaVersion).toBe(8);
42
42
  expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
43
43
  });
44
- it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST + one SIDSCHEMA + one SIDOWNER + one GRANTTARGETROLE (#674) + one LASTOWNER (Consiliency/message-board#675) + one SESSIONPROVENANCE (Consiliency/message-board#665) + one REQUESTREMOVED (Consiliency/message-board#680) + one RLSCORRELATION (Consiliency/message-board#589) + one RESOLUTIONAUTHORITY (Consiliency/message-board#668) forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 166)', () => {
44
+ it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST + one SIDSCHEMA + one SIDOWNER + one GRANTTARGETROLE (#674) + one LASTOWNER (Consiliency/message-board#675) + one SESSIONPROVENANCE (Consiliency/message-board#665) + one REQUESTREMOVED (Consiliency/message-board#680) + one RLSCORRELATION (Consiliency/message-board#589) + one RESOLUTIONAUTHORITY (Consiliency/message-board#668) + one THREADASSOCIATION checkpoint (#667) + one SIDSERVER-MEASUREDMINT (Consiliency/message-board#653) + one REPLYDELIVERY (#661) + one DELIVERYWRITEREXEC (#717 F1) + one REPLAYSCOPE (#717 F2) + one MESSAGEREPLAYSCOPE (#717 F2b) forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 172)', () => {
45
45
  expect(manifest.migrationCount).toBe(manifest.migrations.length);
46
- expect(manifest.migrationCount).toBe(166);
46
+ expect(manifest.migrationCount).toBe(172);
47
47
  // MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
48
48
  // schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
49
49
  // enrollment-only additive surface (identity_providers registry, enrollment audit,
@@ -368,6 +368,12 @@ describe('manifest integrity', () => {
368
368
  '20261015000200_agent_board_request_preserve_removed.sql',
369
369
  '20261015000300_agent_board_rls_correlation.sql',
370
370
  '20261015000400_agent_board_resolution_authority.sql',
371
+ '20261026000100_agent_board_thread_association.sql',
372
+ '20261101000100_agent_board_session_measured_mint.sql',
373
+ '20261104065658_agent_board_reply_delivery.sql',
374
+ '20261104170000_agent_board_tasking_replay_scope.sql',
375
+ '20261104180000_agent_board_delivery_writer_execute.sql',
376
+ '20261104190000_agent_board_message_replay_scope.sql',
371
377
  ]) {
372
378
  expect(names.has(f)).toBe(true);
373
379
  }