@consiliency/agent-board-schema 1.64.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 (171) hide show
  1. package/README.md +19 -0
  2. package/fixtures/already-applied.json +44 -4
  3. package/migrations/20261015000100_agent_board_session_provenance.sql +367 -0
  4. package/migrations/20261015000200_agent_board_request_preserve_removed.sql +87 -0
  5. package/migrations/20261015000300_agent_board_rls_correlation.sql +132 -0
  6. package/migrations/20261015000400_agent_board_resolution_authority.sql +146 -0
  7. package/migrations/20261026000100_agent_board_thread_association.sql +1287 -0
  8. package/migrations/20261101000100_agent_board_session_measured_mint.sql +145 -0
  9. package/migrations/20261104065658_agent_board_reply_delivery.sql +659 -0
  10. package/migrations/20261104170000_agent_board_tasking_replay_scope.sql +320 -0
  11. package/migrations/20261104180000_agent_board_delivery_writer_execute.sql +51 -0
  12. package/migrations/20261104190000_agent_board_message_replay_scope.sql +373 -0
  13. package/package.json +1 -1
  14. package/schema-manifest.json +93 -3
  15. package/src/deployment-drift-main.test.mjs +156 -0
  16. package/src/deployment-drift-script.test.mjs +14 -1
  17. package/src/policy-comment.test.mjs +17 -0
  18. package/src/tooling.test.mjs +12 -2
  19. package/tests/agent_board_661_reply_delivery.test.sql +185 -0
  20. package/tests/agent_board_667_boundary.test.sql +162 -0
  21. package/tests/agent_board_667_conversion_branches.test.sql +91 -0
  22. package/tests/agent_board_667_conversion_profile.test.sql +112 -0
  23. package/tests/agent_board_667_history_class.test.sql +785 -0
  24. package/tests/agent_board_667_native_cast_effect.test.sql +88 -0
  25. package/tests/agent_board_667_realtime_apply_behavior.test.sql +117 -0
  26. package/tests/agent_board_667_realtime_apply_controls.test.sql +143 -0
  27. package/tests/agent_board_667_realtime_apply_profile.test.sql +61 -0
  28. package/tests/agent_board_667_retention.test.sql +201 -0
  29. package/tests/agent_board_667_routing.test.sql +294 -0
  30. package/tests/agent_board_667_sender_effects.test.sql +100 -0
  31. package/tests/agent_board_667_sender_profile_controls.test.sql +29 -0
  32. package/tests/agent_board_667_storage_delimiter_behavior.test.sql +84 -0
  33. package/tests/agent_board_667_storage_delimiter_controls.test.sql +23 -0
  34. package/tests/agent_board_667_storage_delimiter_profile.test.sql +61 -0
  35. package/tests/agent_board_667_storage_search_behavior.test.sql +93 -0
  36. package/tests/agent_board_667_storage_search_controls.test.sql +22 -0
  37. package/tests/agent_board_667_storage_search_profile.test.sql +60 -0
  38. package/tests/agent_board_667_storage_timestamp_behavior.test.sql +58 -0
  39. package/tests/agent_board_667_storage_timestamp_controls.test.sql +22 -0
  40. package/tests/agent_board_667_storage_timestamp_profile.test.sql +54 -0
  41. package/tests/agent_board_667_writers.test.sql +281 -0
  42. package/tests/agent_board_717_message_replay_scope.test.sql +828 -0
  43. package/tests/agent_board_717_tasking_replay_scope.test.sql +661 -0
  44. package/tests/agent_board_addressing.test.sql +8 -5
  45. package/tests/agent_board_claim_subject_gate.test.sql +7 -3
  46. package/tests/agent_board_claimpolicy_race.test.sql +39 -11
  47. package/tests/agent_board_consent_gating.test.sql +4 -4
  48. package/tests/agent_board_delegate_role_class.test.sql +33 -15
  49. package/tests/agent_board_delivery_writer_execute.test.sql +371 -0
  50. package/tests/agent_board_fk_lock_edges.test.sql +3 -0
  51. package/tests/agent_board_gatenull_membstate.test.sql +7 -4
  52. package/tests/agent_board_hosted_delegates.test.sql +5 -2
  53. package/tests/agent_board_inbox_rpcs.test.sql +5 -3
  54. package/tests/agent_board_obsgate.test.sql +8 -4
  55. package/tests/agent_board_obsgate4.test.sql +2 -2
  56. package/tests/agent_board_outbox_wake_ordering.test.sql +25 -2
  57. package/tests/agent_board_own_post_not_delivered.test.sql +11 -9
  58. package/tests/agent_board_policy_correlation.test.sql +189 -0
  59. package/tests/agent_board_request_removed.test.sql +96 -0
  60. package/tests/agent_board_resolution_authority.test.sql +190 -0
  61. package/tests/agent_board_session_identity_behavior_classes.test.sql +2 -2
  62. package/tests/agent_board_session_identity_limits.test.sql +1 -1
  63. package/tests/agent_board_session_identity_measured_mint.test.sql +342 -0
  64. package/tests/agent_board_session_identity_predicate_isolation.test.sql +146 -0
  65. package/tests/agent_board_session_provenance.test.sql +106 -0
  66. package/tests/agent_board_sidserver_authority.test.sql +69 -0
  67. package/tests/agent_board_sidserver_catalog.test.sql +47 -0
  68. package/tests/agent_board_signed_write_gateway.test.sql +4 -2
  69. package/tests/agent_board_task_thread_lock_order.test.sql +67 -42
  70. package/tests/agent_board_v2_invariants.test.sql +5 -2
  71. package/tests/agent_board_write_enforcement_class.test.sql +3 -3
  72. package/tests/catalog/function_execute_privileges.json +43 -7
  73. package/tests/catalog/p_metadata_parameters.json +16 -0
  74. package/tests/fixtures/agent_board_667_apply_rls_guard.sql +195 -0
  75. package/tests/fixtures/agent_board_667_apply_rls_provenance.md +125 -0
  76. package/tests/fixtures/agent_board_667_apply_rls_reference.sql +1903 -0
  77. package/tests/fixtures/agent_board_667_conversion_guard.sql +162 -0
  78. package/tests/fixtures/agent_board_667_conversion_reference.md +23 -0
  79. package/tests/fixtures/agent_board_667_conversion_reference.sql +5325 -0
  80. package/tests/fixtures/agent_board_667_delimiter_guard.sql +92 -0
  81. package/tests/fixtures/agent_board_667_delimiter_provenance.md +116 -0
  82. package/tests/fixtures/agent_board_667_delimiter_reference.sql +10150 -0
  83. package/tests/fixtures/agent_board_667_search_guard.sql +92 -0
  84. package/tests/fixtures/agent_board_667_search_provenance.md +141 -0
  85. package/tests/fixtures/agent_board_667_search_reference.sql +4779 -0
  86. package/tests/fixtures/agent_board_667_sender_guard.sql +102 -0
  87. package/tests/fixtures/agent_board_667_sender_reference.md +20 -0
  88. package/tests/fixtures/agent_board_667_sender_reference.sql +875 -0
  89. package/tests/fixtures/agent_board_667_timestamp_graph.sql +84 -0
  90. package/tests/fixtures/agent_board_667_timestamp_guard.sql +94 -0
  91. package/tests/fixtures/agent_board_667_timestamp_provenance.md +99 -0
  92. package/tests/fixtures/agent_board_667_timestamp_reference.sql +2938 -0
  93. package/tests/isolation/expected/association_artifact_anchor_order.out +108 -0
  94. package/tests/isolation/expected/association_board_vs_board.out +71 -0
  95. package/tests/isolation/expected/association_post_wait_authority.out +77 -0
  96. package/tests/isolation/expected/association_strong_history_writers.out +203 -0
  97. package/tests/isolation/expected/association_strong_isolation.out +185 -0
  98. package/tests/isolation/expected/association_vs_artifact_claim_escalation.out +199 -0
  99. package/tests/isolation/expected/association_vs_heartbeat.out +78 -0
  100. package/tests/isolation/expected/association_vs_multi_anchor_artifact.out +67 -0
  101. package/tests/isolation/expected/association_vs_private_message.out +67 -0
  102. package/tests/isolation/expected/association_vs_task_and_conflict.out +133 -0
  103. package/tests/isolation/expected/association_vs_taskful_cross_thread.out +133 -0
  104. package/tests/isolation/expected/consent_approval_lock.out +1 -1
  105. package/tests/isolation/expected/message_replay_scope.out +209 -0
  106. package/tests/isolation/expected/tasking_replay_scope.out +125 -0
  107. package/tests/isolation/specs/association_artifact_anchor_order.spec +46 -0
  108. package/tests/isolation/specs/association_board_vs_board.spec +37 -0
  109. package/tests/isolation/specs/association_post_wait_authority.spec +48 -0
  110. package/tests/isolation/specs/association_strong_history_writers.spec +61 -0
  111. package/tests/isolation/specs/association_strong_isolation.spec +50 -0
  112. package/tests/isolation/specs/association_vs_artifact_claim_escalation.spec +41 -0
  113. package/tests/isolation/specs/association_vs_heartbeat.spec +47 -0
  114. package/tests/isolation/specs/association_vs_multi_anchor_artifact.spec +38 -0
  115. package/tests/isolation/specs/association_vs_private_message.spec +33 -0
  116. package/tests/isolation/specs/association_vs_task_and_conflict.spec +38 -0
  117. package/tests/isolation/specs/association_vs_taskful_cross_thread.spec +42 -0
  118. package/tests/isolation/specs/consent_approval_lock.spec +13 -7
  119. package/tests/isolation/specs/message_replay_scope.spec +77 -0
  120. package/tests/isolation/specs/tasking_replay_scope.spec +72 -0
  121. package/tests/mutations/717-f1/M0_prefix_main_acl_and_comments.sql +11 -0
  122. package/tests/mutations/717-f1/M10_wrapper_stops_delegating.sql +7 -0
  123. package/tests/mutations/717-f1/M11_transitive_recipient_content.sql +8 -0
  124. package/tests/mutations/717-f1/M12_transitive_recipient_visibility.sql +8 -0
  125. package/tests/mutations/717-f1/M13_transitive_worker_content.sql +15 -0
  126. package/tests/mutations/717-f1/M1a_regrant_upsert_delivery_recipient.sql +1 -0
  127. package/tests/mutations/717-f1/M1b_regrant_upsert_inbox_item.sql +1 -0
  128. package/tests/mutations/717-f1/M1c_regrant_queue_inbox_callbacks.sql +1 -0
  129. package/tests/mutations/717-f1/M1d_regrant_queue_board_post_callbacks.sql +1 -0
  130. package/tests/mutations/717-f1/M1e_regrant_next_thread_message_sequence.sql +1 -0
  131. package/tests/mutations/717-f1/M2_blanket_schema_grant.sql +1 -0
  132. package/tests/mutations/717-f1/M3_new_overload_of_sink.sql +10 -0
  133. package/tests/mutations/717-f1/M4_new_route_calling_sink.sql +7 -0
  134. package/tests/mutations/717-f1/M5_actor_trusts_supplied_id.sql +5 -0
  135. package/tests/mutations/717-f1/M6_signed_route_granted_to_authenticated.sql +1 -0
  136. package/tests/mutations/717-f1/M7_worker_granted_to_authenticated.sql +1 -0
  137. package/tests/mutations/717-f1/M8_recipient_state_rewrites_visibility.sql +5 -0
  138. package/tests/mutations/717-f1/M9_public_route_revoked_from_service_role.sql +1 -0
  139. package/tests/mutations/717-f1/outbox_wake_ordering/W1_regrant_queue_inbox_callbacks.sql +1 -0
  140. package/tests/mutations/717-f1/outbox_wake_ordering/W2_revoke_worker_from_service_role.sql +1 -0
  141. package/tests/mutations/717-f1/outbox_wake_ordering/W3_invoker_caller_of_sink.sql +1 -0
  142. package/tests/mutations/717-f1/outbox_wake_ordering/W4_public_regrant_board_post_sink.sql +1 -0
  143. package/tests/mutations/717-f1/run.sh +17 -0
  144. package/tests/mutations/717-f2b/C1_new_delivery_route.sql +4 -0
  145. package/tests/mutations/717-f2b/C2_new_replay_lookup.sql +3 -0
  146. package/tests/mutations/717-f2b/C3_internal_regranted.sql +2 -0
  147. package/tests/mutations/717-f2b/C4_wrapper_writes_delivery.sql +9 -0
  148. package/tests/mutations/717-f2b/C5_class_member_dynamic_sql.sql +9 -0
  149. package/tests/mutations/717-f2b/C6_drop_board_scope_constraint.sql +2 -0
  150. package/tests/mutations/717-f2b/C7_state_private_helper_content.sql +14 -0
  151. package/tests/mutations/717-f2b/L1_dm_lock_split_by_recipient.sql +8 -0
  152. package/tests/mutations/717-f2b/L2_dm_lock_removed.sql +12 -0
  153. package/tests/mutations/717-f2b/L3_mc_lock_split_by_recipients.sql +8 -0
  154. package/tests/mutations/717-f2b/L4_mc_lock_removed.sql +12 -0
  155. package/tests/mutations/717-f2b/M0_prefix_definitions.sql +17 -0
  156. package/tests/mutations/717-f2b/M10_mc_lookup_any_author.sql +15 -0
  157. package/tests/mutations/717-f2b/M11_dm_swallow_refusal.sql +8 -0
  158. package/tests/mutations/717-f2b/M12_mc_swallow_refusal.sql +8 -0
  159. package/tests/mutations/717-f2b/M13_signed_dm_swallows_refusal.sql +32 -0
  160. package/tests/mutations/717-f2b/M14_signed_mc_swallows_refusal.sql +20 -0
  161. package/tests/mutations/717-f2b/M15_mc_any_source.sql +7 -0
  162. package/tests/mutations/717-f2b/M1_dm_drop_recipient_proof.sql +16 -0
  163. package/tests/mutations/717-f2b/M2_mc_drop_recipient_proof.sql +16 -0
  164. package/tests/mutations/717-f2b/M3_dm_metadata_as_proof.sql +12 -0
  165. package/tests/mutations/717-f2b/M4_mc_metadata_as_proof.sql +16 -0
  166. package/tests/mutations/717-f2b/M5_dm_any_mode.sql +11 -0
  167. package/tests/mutations/717-f2b/M6_dm_board_delivery_counts.sql +11 -0
  168. package/tests/mutations/717-f2b/M7_mc_any_mode.sql +11 -0
  169. package/tests/mutations/717-f2b/M8_dm_thread_stamp_as_proof.sql +12 -0
  170. package/tests/mutations/717-f2b/M9_dm_lookup_any_author.sql +15 -0
  171. package/tests/mutations/717-f2b/run.sh +82 -0
@@ -0,0 +1,320 @@
1
+ -- #717 F2: same-key tasking replay must not widen a task's delivery scope.
2
+ --
3
+ -- create_tasking_delivery keys idempotency on (thread, resolved creator, key) and, on a replay,
4
+ -- reused the task and fanned it out to whatever audience the RETRY named. A recipient-only task
5
+ -- replayed with a board target therefore became readable by board members, and through the hosted
6
+ -- delegate policies (which key on a board-scoped delivery of the task) by that board's delegates,
7
+ -- together with the task history other agents authored on it.
8
+ --
9
+ -- The identity, the advisory lock and the lookup are unchanged; board_id is deliberately NOT added
10
+ -- to the key, which would silently turn a retry into a second task. A replay may name the same
11
+ -- audience or a subset of it, and a same-board replay still fans out to the board's CURRENT members,
12
+ -- as before. Anything the persisted deliveries do not already prove is refused with 42501 before any
13
+ -- replay side effect, and the raise rolls back the whole call, including any first association,
14
+ -- telemetry, and the signed wrappers' evidence (they do not catch). Compatibility limit, recorded:
15
+ -- a task whose deliveries were never written or were removed cannot be replayed with any target.
16
+
17
+ CREATE OR REPLACE FUNCTION agent_board.create_tasking_delivery(p_thread_id uuid, p_created_by_agent_id uuid, p_title text, p_details text DEFAULT NULL::text, p_recipient_agent_ids uuid[] DEFAULT NULL::uuid[], p_board_id uuid DEFAULT NULL::uuid, p_priority text DEFAULT 'medium'::text, p_orchestration_mode text DEFAULT 'distributed'::text, p_idempotency_key text DEFAULT NULL::text, p_metadata jsonb DEFAULT '{}'::jsonb)
18
+ RETURNS TABLE(task_id uuid, created_event_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamp with time zone, updated_at timestamp with time zone)
19
+ LANGUAGE plpgsql
20
+ SECURITY DEFINER
21
+ SET search_path TO 'agent_board', 'public'
22
+ AS $function$
23
+ declare
24
+ v_task agent_board.tasks%rowtype;
25
+ v_created_event_id uuid;
26
+ v_created_task record;
27
+ v_recipient_ids uuid[];
28
+ v_recipient_id uuid;
29
+ v_membership record;
30
+ v_recipient agent_board.delivery_recipients%rowtype;
31
+ v_inbox agent_board.inbox_items%rowtype;
32
+ v_delivered_count integer := 0;
33
+ v_inbox_count integer := 0;
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
+ v_is_creator boolean;
41
+ begin
42
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
43
+
44
+ if nullif(trim(p_title), '') is null then
45
+ raise exception 'task title is required' using errcode = '22023';
46
+ end if;
47
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
48
+ raise exception 'invalid priority' using errcode = '22023';
49
+ end if;
50
+ if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
51
+ raise exception 'invalid orchestration mode' using errcode = '22023';
52
+ end if;
53
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
54
+ raise exception 'thread not found' using errcode = 'P0002';
55
+ end if;
56
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
57
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
58
+ end if;
59
+ if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
60
+ raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
61
+ end if;
62
+ if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
63
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
64
+ end if;
65
+ if p_board_id is not null then
66
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
67
+ raise exception 'board not found' using errcode = 'P0002';
68
+ end if;
69
+ if not agent_board._current_agent_has_board_access(p_board_id) then
70
+ raise exception 'board not accessible to current agent' using errcode = '42501';
71
+ end if;
72
+ if not agent_board._current_agent_can_send_to_board(p_board_id) then
73
+ raise exception 'board send requires a non-observer board role' using errcode = '42501';
74
+ end if;
75
+ end if;
76
+
77
+ select array_agg(distinct recipient_id)
78
+ into v_recipient_ids
79
+ from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
80
+
81
+ if v_recipient_ids is not null and (
82
+ select count(*)
83
+ from agent_board.agents
84
+ where id = any(v_recipient_ids)
85
+ ) <> cardinality(v_recipient_ids) then
86
+ raise exception 'recipient agent not found' using errcode = 'P0002';
87
+ end if;
88
+
89
+ if agent_board._is_service_role() then
90
+ perform agent_board._record_send_authz_telemetry(
91
+ 'service_role_bypass',
92
+ coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
93
+ v_actor_agent_id,
94
+ p_board_id,
95
+ 'create_tasking_delivery',
96
+ agent_board._resolve_send_fleet_scope(v_actor_agent_id),
97
+ null,
98
+ jsonb_build_object('redacted', true)
99
+ );
100
+ end if;
101
+
102
+ perform agent_board._lock_idempotency_key(
103
+ 'create_tasking_delivery:' || p_thread_id::text || ':' || v_actor_agent_id::text,
104
+ v_idempotency_key
105
+ );
106
+
107
+ if p_board_id is not null then
108
+ perform agent_board._associate_empty_thread(p_thread_id, p_board_id);
109
+ -- Both checks are new statements after the thread-lock wait.
110
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
111
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
112
+ end if;
113
+ if not agent_board._current_agent_has_board_access(p_board_id) then
114
+ raise exception 'board not accessible to current agent' using errcode = '42501';
115
+ end if;
116
+ if not agent_board._current_agent_can_send_to_board(p_board_id) then
117
+ raise exception 'board send requires a non-observer board role' using errcode = '42501';
118
+ end if;
119
+ end if;
120
+
121
+ if v_idempotency_key is not null then
122
+ select *
123
+ into v_task
124
+ from agent_board.tasks
125
+ where thread_id = p_thread_id
126
+ and created_by_agent_id = v_actor_agent_id
127
+ and metadata ->> 'rpcName' = 'create_tasking_delivery'
128
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
129
+ order by created_at desc, id desc
130
+ limit 1;
131
+ end if;
132
+
133
+ if v_task.id is null then
134
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
135
+ || jsonb_build_object(
136
+ 'rpcName', 'create_tasking_delivery',
137
+ 'priority', p_priority
138
+ );
139
+ if p_board_id is not null then
140
+ v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
141
+ end if;
142
+ if v_idempotency_key is not null then
143
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
144
+ end if;
145
+
146
+ select *
147
+ into v_created_task
148
+ from agent_board.create_task(
149
+ p_thread_id,
150
+ v_actor_agent_id,
151
+ trim(p_title),
152
+ p_details,
153
+ null,
154
+ null,
155
+ p_priority,
156
+ p_orchestration_mode,
157
+ v_metadata
158
+ );
159
+
160
+ v_created_event_id := v_created_task.created_event_id;
161
+
162
+ select *
163
+ into v_task
164
+ from agent_board.tasks
165
+ where id = v_created_task.task_id;
166
+ else
167
+ -- #717 F2: a replay reuses the task, so it may not widen who can read it. The audience a
168
+ -- replay may name is bounded by the task's PERSISTED tasking deliveries: a board only if a
169
+ -- board-scoped delivery of this task already names it, and each explicit recipient only if
170
+ -- it already holds a private (null-board) delivery of this task. Thread stamps and task or
171
+ -- caller metadata are never evidence: metadata is writable through other task creators.
172
+ -- Missing evidence refuses; a new audience needs a new idempotency key (a new task).
173
+ if (
174
+ p_board_id is not null
175
+ and not exists (
176
+ select 1
177
+ from agent_board.delivery_recipients dr
178
+ where dr.source_task_id = v_task.id
179
+ and dr.delivery_mode = 'tasking'
180
+ and dr.board_id = p_board_id
181
+ )
182
+ ) or exists (
183
+ select 1
184
+ from unnest(coalesce(v_recipient_ids, '{}'::uuid[])) as requested(agent_id)
185
+ where not exists (
186
+ select 1
187
+ from agent_board.delivery_recipients dr
188
+ where dr.source_task_id = v_task.id
189
+ and dr.delivery_mode = 'tasking'
190
+ and dr.recipient_agent_id = requested.agent_id
191
+ and dr.board_id is null
192
+ )
193
+ ) then
194
+ raise exception 'tasking replay cannot widen delivery scope' using errcode = '42501';
195
+ end if;
196
+
197
+ select te.id
198
+ into v_created_event_id
199
+ from agent_board.task_events te
200
+ where te.task_id = v_task.id
201
+ and te.event_sequence = 1;
202
+ end if;
203
+
204
+ v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
205
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
206
+ 'task',
207
+ v_task.id,
208
+ 'agent_board.v_task_tree',
209
+ 'tasking delivery',
210
+ jsonb_build_object('threadId', p_thread_id::text)
211
+ );
212
+
213
+ if v_recipient_ids is not null then
214
+ foreach v_recipient_id in array v_recipient_ids loop
215
+ v_recipient := agent_board._upsert_delivery_recipient(
216
+ 'tasking',
217
+ v_recipient_id,
218
+ 'task',
219
+ null,
220
+ v_task.id,
221
+ null,
222
+ null,
223
+ null,
224
+ jsonb_build_object('priority', p_priority)
225
+ );
226
+
227
+ v_inbox := agent_board._upsert_inbox_item(
228
+ v_recipient.id,
229
+ v_recipient_id,
230
+ null,
231
+ v_recipient.delivery_mode,
232
+ 'task',
233
+ null,
234
+ v_task.id,
235
+ null,
236
+ trim(p_title),
237
+ v_preview,
238
+ p_priority,
239
+ true,
240
+ false,
241
+ v_fetch_descriptor,
242
+ jsonb_build_object('threadId', p_thread_id::text)
243
+ );
244
+
245
+ v_delivered_count := v_delivered_count + 1;
246
+ v_inbox_count := v_inbox_count + 1;
247
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
248
+ end loop;
249
+ end if;
250
+
251
+ if p_board_id is not null then
252
+ for v_membership in
253
+ select bm.id, bm.agent_id
254
+ from agent_board.board_memberships bm
255
+ where bm.board_id = p_board_id
256
+ and bm.membership_state in ('joined', 'subscribed')
257
+ order by bm.created_at
258
+ loop
259
+ -- #608: the board fan-out does not deliver a task to the agent that created it. The
260
+ -- creator's row is KEPT, feed-only, exactly as a board post's author row is. Before #667,
261
+ -- _upsert_delivery_recipient was the only board-stamp path; skipping that row
262
+ -- left a creator-only board's thread unlinked and post_message on the access-only branch of
263
+ -- the observer gate (codex and grok seats, PR #615 round 1). It also keeps the board
264
+ -- directory's activity and delegate visibility as they were. An explicit recipient list
265
+ -- naming the creator still delivers to it: that is the caller addressing itself, and the
266
+ -- board loop must not then demote the row the explicit loop wrote. It shares that row (the
267
+ -- delivery upsert keeps the explicit row's null board_id), so demoting it wrote both
268
+ -- visibility flags false and violated inbox_items_visibility_shape, aborting the whole task
269
+ -- (every seat on PR #615 round 2).
270
+ v_is_creator := v_membership.agent_id is not distinct from v_actor_agent_id
271
+ and not (v_membership.agent_id = any(coalesce(v_recipient_ids, '{}'::uuid[])));
272
+ v_recipient := agent_board._upsert_delivery_recipient(
273
+ 'tasking',
274
+ v_membership.agent_id,
275
+ 'task',
276
+ null,
277
+ v_task.id,
278
+ null,
279
+ p_board_id,
280
+ v_membership.id,
281
+ jsonb_build_object('priority', p_priority)
282
+ );
283
+
284
+ v_inbox := agent_board._upsert_inbox_item(
285
+ v_recipient.id,
286
+ v_membership.agent_id,
287
+ v_recipient.board_id,
288
+ v_recipient.delivery_mode,
289
+ 'task',
290
+ null,
291
+ v_task.id,
292
+ null,
293
+ trim(p_title),
294
+ v_preview,
295
+ p_priority,
296
+ not v_is_creator,
297
+ v_recipient.board_id is not null,
298
+ v_fetch_descriptor,
299
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
300
+ );
301
+
302
+ -- The counts are of RECIPIENTS; the creator's feed-only row is not one.
303
+ if not v_is_creator then
304
+ v_delivered_count := v_delivered_count + 1;
305
+ v_inbox_count := v_inbox_count + 1;
306
+ end if;
307
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
308
+ end loop;
309
+ end if;
310
+
311
+ return query
312
+ select v_task.id, v_created_event_id, v_delivered_count, v_inbox_count, v_callback_count, v_task.created_at, v_task.updated_at;
313
+ end;
314
+ $function$
315
+ ;
316
+
317
+ revoke all on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) from public, anon, authenticated, service_role, agent_board_delegate;
318
+ grant execute on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) to "service_role";
319
+ grant execute on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) to "authenticated";
320
+ comment on function agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb) is 'Creates or reuses a tasking send after validating the session actor and any board target. First board association precedes its own task and event writes; explicit recipient delivery rows preserve their private scope. A replay of an existing (thread, creator, key) cannot widen the task''s audience (#717 F2): a board must already hold a board-scoped tasking delivery of the task and each explicit recipient a private one, else 42501 ''tasking replay cannot widen delivery scope'' before any replay effect; a new audience needs a new key.';
@@ -0,0 +1,51 @@
1
+ -- DELIVERYWRITEREXEC (#717 F1): the internal message/delivery/inbox writers are not API routes.
2
+ --
3
+ -- Five SECURITY DEFINER helpers write the delivery sinks (delivery_recipients, inbox_items,
4
+ -- callback_outbox_events) or the thread message sequence, and each takes its authority from its
5
+ -- ARGUMENTS: a recipient, a board, a membership, a message id, a subject. They were EXECUTE-able
6
+ -- by service_role through the schema-wide `grant execute on all functions in schema agent_board
7
+ -- to service_role` of the 2026-05 migrations, never by a deliberate grant.
8
+ --
9
+ -- Called directly there is no sending RPC in front of them, so nothing checks that the caller is
10
+ -- the author, that the recipient was addressed, or that the board was targeted. #717 measured it
11
+ -- (review S4/S4b): `_upsert_delivery_recipient('board', member, 'message', <a DM on a board
12
+ -- thread>, …)` returned OK and a board member then read the DM; `'direct'` mode re-addressed the
13
+ -- same DM to an arbitrary agent. Both keep the ORIGINAL author on the message row, so the
14
+ -- republished DM reads as authored by its sender. `_upsert_inbox_item`, `_queue_inbox_callbacks`,
15
+ -- `_queue_board_post_callbacks` (forged wake payloads, caller-chosen subject and preview against
16
+ -- a real message id) and `_next_thread_message_sequence` (sequence consumption on any thread)
17
+ -- are the same class: internal, argument-authorised, reachable only because of the blanket grant.
18
+ --
19
+ -- The class is catalog-derived in tests/agent_board_delivery_writer_execute.test.sql (DWX):
20
+ -- every routine that writes a message/delivery/inbox sink, directly or transitively, either has
21
+ -- no API-role EXECUTE or is a named, positively-asserted public route. No caller outside the
22
+ -- schema invokes any of these five (repository and Portal searched; edge functions call fixed
23
+ -- public RPC names). The public send routes keep working: they are SECURITY DEFINER and reach the
24
+ -- helpers as the function owner, not as the API caller.
25
+ --
26
+ -- No function body changes, so the source tree's existing definitions stay the sole ones.
27
+
28
+ revoke all on function agent_board._upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb)
29
+ from public, anon, authenticated, service_role, agent_board_delegate;
30
+ comment on function agent_board._upsert_delivery_recipient(text,uuid,text,uuid,uuid,uuid,uuid,uuid,jsonb) is
31
+ 'Internal delivery sink, not an API route: no API role may EXECUTE it (#717 F1). Enforces thread/board identity and the locked empty-history boundary for message, task and delegation delivery sources. Supplying a source ID does not exempt its private history. The sending RPC validates caller authority before its own writes.';
32
+
33
+ revoke all on function agent_board._upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb)
34
+ from public, anon, authenticated, service_role, agent_board_delegate;
35
+ comment on function agent_board._upsert_inbox_item(uuid,uuid,uuid,text,text,uuid,uuid,uuid,text,text,text,boolean,boolean,jsonb,jsonb) is
36
+ 'Internal inbox sink, not an API route: no API role may EXECUTE it (#717 F1). The sole inbox_items inserter; its recipient, board, visibility and preview are trusted from the calling send/tasking RPC, which validated caller authority first.';
37
+
38
+ revoke all on function agent_board._queue_inbox_callbacks(uuid,text)
39
+ from public, anon, authenticated, service_role, agent_board_delegate;
40
+ comment on function agent_board._queue_inbox_callbacks(uuid,text) is
41
+ 'Internal callback sink, not an API route: no API role may EXECUTE it (#717 F1). Queues inbox callback outbox events for an inbox item the calling delivery RPC has just written.';
42
+
43
+ revoke all on function agent_board._queue_board_post_callbacks(uuid,uuid,text,text,text,jsonb)
44
+ from public, anon, authenticated, service_role, agent_board_delegate;
45
+ comment on function agent_board._queue_board_post_callbacks(uuid,uuid,text,text,text,jsonb) is
46
+ 'Internal callback sink, not an API route: no API role may EXECUTE it (#717 F1). Queues board-post callback outbox events with the subject, preview and fetch descriptor supplied by the board delivery that called it.';
47
+
48
+ revoke all on function agent_board._next_thread_message_sequence(uuid)
49
+ from public, anon, authenticated, service_role, agent_board_delegate;
50
+ comment on function agent_board._next_thread_message_sequence(uuid) is
51
+ 'Internal sequence writer, not an API route: no API role may EXECUTE it (#717 F1). Advances threads.last_message_sequence under the thread row lock for the message insertion that called it.';