@consiliency/agent-board-schema 1.3.0 → 1.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.
Files changed (62) hide show
  1. package/bin/agent-board-schema.js +0 -0
  2. package/fixtures/already-applied.json +104 -0
  3. package/migrations/20260717000100_agent_board_board_role.sql +180 -0
  4. package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
  5. package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
  6. package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
  7. package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
  8. package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
  9. package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
  10. package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
  11. package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
  12. package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
  13. package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
  14. package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
  15. package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
  16. package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
  17. package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
  18. package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
  19. package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
  20. package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
  21. package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
  22. package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
  23. package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
  24. package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
  25. package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
  26. package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
  27. package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
  28. package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
  29. package/package.json +9 -9
  30. package/schema-manifest.json +238 -4
  31. package/src/tooling.test.mjs +42 -1
  32. package/tests/agent_board_agent_lineage.test.sql +73 -0
  33. package/tests/agent_board_agents_status_revoked.test.sql +60 -0
  34. package/tests/agent_board_auth_rpcs.test.sql +17 -16
  35. package/tests/agent_board_board_role.test.sql +129 -0
  36. package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
  37. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
  38. package/tests/agent_board_delegation_enforcement.test.sql +175 -0
  39. package/tests/agent_board_delegation_policies.test.sql +119 -0
  40. package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
  41. package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
  42. package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
  43. package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
  44. package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
  45. package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
  46. package/tests/agent_board_fleet_registry.test.sql +2 -2
  47. package/tests/agent_board_identity_providers.test.sql +104 -0
  48. package/tests/agent_board_inbox_rpcs.test.sql +6 -6
  49. package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
  50. package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
  51. package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
  52. package/tests/agent_board_resigning_refresh.test.sql +73 -0
  53. package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
  54. package/tests/agent_board_schema_version.test.sql +2 -2
  55. package/tests/agent_board_schema_version_rpc.test.sql +1 -1
  56. package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
  57. package/tests/agent_board_send_authz_config.test.sql +84 -0
  58. package/tests/agent_board_send_authz_matrix.test.sql +159 -0
  59. package/tests/agent_board_send_role_gate.test.sql +177 -0
  60. package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
  61. package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
  62. package/tests/agent_board_thread_acl.test.sql +129 -0
@@ -0,0 +1,804 @@
1
+ -- MBSENDAUTHZ SL-1 (Lane A): send-path board-role gate + service_role bypass telemetry.
2
+ --
3
+ -- Forward migration. Re-declares the four core send RPCs (bodies forward-copied
4
+ -- verbatim from 20260512000002 except the enumerated additions) to:
5
+ -- (i) add the board-role observer-denied gate to the board-target sends via
6
+ -- agent_board._current_agent_can_send_to_board (post_board_message always;
7
+ -- create_tasking_delivery only when a board target is present), and
8
+ -- (ii) record a service_role_bypass telemetry row (with the frozen ef|host|unknown
9
+ -- origin discriminator, resolved from p_metadata -> 'signed_write' ->> 'origin')
10
+ -- on every _is_service_role() send.
11
+ --
12
+ -- The restricted-thread ACL consult already fires via MBAUTHZ's
13
+ -- _current_agent_can_access_thread and is NOT re-implemented here. Dormancy is
14
+ -- preserved: all four cores stay revoked from authenticated (re-asserted below);
15
+ -- the role gate bites only once MBSESSION re-grants sends to authenticated.
16
+
17
+ -- 1. _current_agent_can_send_to_board (IF-0-MBSENDAUTHZ-1) -------------------
18
+ -- Mirrors _current_agent_has_board_access but excludes the observer role: the only
19
+ -- net-new send-path authz predicate.
20
+ create or replace function agent_board._current_agent_can_send_to_board(
21
+ p_board_id uuid
22
+ )
23
+ returns boolean
24
+ language sql
25
+ stable
26
+ security definer
27
+ set search_path = ''
28
+ as $$
29
+ select
30
+ agent_board._is_service_role()
31
+ or exists (
32
+ select 1
33
+ from agent_board.board_memberships bm
34
+ where bm.board_id = p_board_id
35
+ and bm.agent_id = agent_board.current_agent_id()
36
+ and bm.membership_state in ('joined', 'subscribed')
37
+ and bm.role <> 'observer'
38
+ )
39
+ $$;
40
+
41
+ comment on function agent_board._current_agent_can_send_to_board(uuid) is
42
+ 'True iff the current agent may send to the board: service_role, or an active board member whose role is not observer. The net-new send-path authz predicate (restricted-thread ACL is inherited from _current_agent_can_access_thread).';
43
+
44
+ revoke all on function agent_board._current_agent_can_send_to_board(uuid) from public, anon, authenticated;
45
+ grant execute on function agent_board._current_agent_can_send_to_board(uuid) to service_role;
46
+
47
+ -- 2. Re-declared core send RPCs (verbatim bodies + gate/telemetry additions) --
48
+ create or replace function agent_board.send_direct_message(
49
+ p_thread_id uuid,
50
+ p_author_agent_id uuid,
51
+ p_recipient_agent_id uuid,
52
+ p_subject text,
53
+ p_body text,
54
+ p_message_type text default 'comment',
55
+ p_priority text default 'medium',
56
+ p_idempotency_key text default null,
57
+ p_metadata jsonb default '{}'::jsonb
58
+ )
59
+ returns table(message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid, callback_outbox_count integer, created_at timestamptz)
60
+ language plpgsql
61
+ security definer
62
+ set search_path = agent_board, public
63
+ as $$
64
+ declare
65
+ v_message agent_board.messages%rowtype;
66
+ v_created_message record;
67
+ v_recipient agent_board.delivery_recipients%rowtype;
68
+ v_inbox agent_board.inbox_items%rowtype;
69
+ v_callback_count integer := 0;
70
+ v_metadata jsonb;
71
+ v_preview text;
72
+ v_fetch_descriptor jsonb;
73
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
74
+ v_actor_agent_id uuid;
75
+ begin
76
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
77
+
78
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
79
+ raise exception 'subject and body are required' using errcode = '22023';
80
+ end if;
81
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
82
+ raise exception 'invalid priority' using errcode = '22023';
83
+ end if;
84
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
85
+ raise exception 'thread not found' using errcode = 'P0002';
86
+ end if;
87
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
88
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
89
+ end if;
90
+ if not exists (select 1 from agent_board.agents where id = p_recipient_agent_id) then
91
+ raise exception 'recipient agent not found' using errcode = 'P0002';
92
+ end if;
93
+
94
+ if agent_board._is_service_role() then
95
+ perform agent_board._record_send_authz_telemetry(
96
+ 'service_role_bypass',
97
+ coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
98
+ v_actor_agent_id,
99
+ null,
100
+ 'send_direct_message',
101
+ agent_board._resolve_send_fleet_scope(v_actor_agent_id),
102
+ null,
103
+ jsonb_build_object('redacted', true)
104
+ );
105
+ end if;
106
+
107
+ perform agent_board._lock_idempotency_key(
108
+ 'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
109
+ v_idempotency_key
110
+ );
111
+
112
+ if v_idempotency_key is not null then
113
+ select *
114
+ into v_message
115
+ from agent_board.messages
116
+ where thread_id = p_thread_id
117
+ and author_agent_id = v_actor_agent_id
118
+ and metadata ->> 'rpcName' = 'send_direct_message'
119
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
120
+ order by created_at desc
121
+ limit 1;
122
+ end if;
123
+
124
+ if v_message.id is null then
125
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
126
+ || jsonb_build_object(
127
+ 'rpcName', 'send_direct_message',
128
+ 'subject', trim(p_subject),
129
+ 'priority', p_priority
130
+ );
131
+ if v_idempotency_key is not null then
132
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
133
+ end if;
134
+
135
+ select *
136
+ into v_created_message
137
+ from agent_board.post_message(
138
+ p_thread_id,
139
+ v_actor_agent_id,
140
+ p_body,
141
+ p_message_type,
142
+ null,
143
+ v_metadata
144
+ );
145
+
146
+ select *
147
+ into v_message
148
+ from agent_board.messages
149
+ where id = v_created_message.message_id;
150
+ end if;
151
+
152
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
153
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
154
+ 'thread',
155
+ p_thread_id,
156
+ 'agent_board.v_thread_feed',
157
+ 'direct delivery',
158
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', p_recipient_agent_id::text)
159
+ );
160
+
161
+ v_recipient := agent_board._upsert_delivery_recipient(
162
+ 'direct',
163
+ p_recipient_agent_id,
164
+ 'message',
165
+ v_message.id,
166
+ null,
167
+ null,
168
+ null,
169
+ null,
170
+ jsonb_build_object('priority', p_priority)
171
+ );
172
+
173
+ v_inbox := agent_board._upsert_inbox_item(
174
+ v_recipient.id,
175
+ p_recipient_agent_id,
176
+ null,
177
+ v_recipient.delivery_mode,
178
+ 'message',
179
+ v_message.id,
180
+ null,
181
+ null,
182
+ trim(p_subject),
183
+ v_preview,
184
+ p_priority,
185
+ true,
186
+ false,
187
+ v_fetch_descriptor,
188
+ jsonb_build_object('threadId', p_thread_id::text)
189
+ );
190
+
191
+ v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
192
+
193
+ return query
194
+ select v_message.id, v_recipient.id, v_inbox.id, v_callback_count, v_message.created_at;
195
+ end;
196
+ $$;
197
+
198
+ create or replace function agent_board.send_multicast_message(
199
+ p_thread_id uuid,
200
+ p_author_agent_id uuid,
201
+ p_recipient_agent_ids uuid[],
202
+ p_subject text,
203
+ p_body text,
204
+ p_message_type text default 'comment',
205
+ p_priority text default 'medium',
206
+ p_idempotency_key text default null,
207
+ p_metadata jsonb default '{}'::jsonb
208
+ )
209
+ returns table(message_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz)
210
+ language plpgsql
211
+ security definer
212
+ set search_path = agent_board, public
213
+ as $$
214
+ declare
215
+ v_message agent_board.messages%rowtype;
216
+ v_created_message record;
217
+ v_recipient_ids uuid[];
218
+ v_recipient_id uuid;
219
+ v_recipient agent_board.delivery_recipients%rowtype;
220
+ v_inbox agent_board.inbox_items%rowtype;
221
+ v_delivered_count integer := 0;
222
+ v_inbox_count integer := 0;
223
+ v_callback_count integer := 0;
224
+ v_metadata jsonb;
225
+ v_preview text;
226
+ v_fetch_descriptor jsonb;
227
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
228
+ v_actor_agent_id uuid;
229
+ begin
230
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
231
+
232
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
233
+ raise exception 'subject and body are required' using errcode = '22023';
234
+ end if;
235
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
236
+ raise exception 'invalid priority' using errcode = '22023';
237
+ end if;
238
+ if p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0 then
239
+ raise exception 'recipient_agent_ids must not be empty' using errcode = '22023';
240
+ end if;
241
+ if array_position(p_recipient_agent_ids, null) is not null then
242
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
243
+ end if;
244
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
245
+ raise exception 'thread not found' using errcode = 'P0002';
246
+ end if;
247
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
248
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
249
+ end if;
250
+
251
+ select array_agg(distinct recipient_id)
252
+ into v_recipient_ids
253
+ from unnest(p_recipient_agent_ids) as recipient_id;
254
+
255
+ if (
256
+ select count(*)
257
+ from agent_board.agents
258
+ where id = any(v_recipient_ids)
259
+ ) <> cardinality(v_recipient_ids) then
260
+ raise exception 'recipient agent not found' using errcode = 'P0002';
261
+ end if;
262
+
263
+ if agent_board._is_service_role() then
264
+ perform agent_board._record_send_authz_telemetry(
265
+ 'service_role_bypass',
266
+ coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
267
+ v_actor_agent_id,
268
+ null,
269
+ 'send_multicast_message',
270
+ agent_board._resolve_send_fleet_scope(v_actor_agent_id),
271
+ null,
272
+ jsonb_build_object('redacted', true)
273
+ );
274
+ end if;
275
+
276
+ perform agent_board._lock_idempotency_key(
277
+ 'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
278
+ v_idempotency_key
279
+ );
280
+
281
+ if v_idempotency_key is not null then
282
+ select *
283
+ into v_message
284
+ from agent_board.messages
285
+ where thread_id = p_thread_id
286
+ and author_agent_id = v_actor_agent_id
287
+ and metadata ->> 'rpcName' = 'send_multicast_message'
288
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
289
+ order by created_at desc
290
+ limit 1;
291
+ end if;
292
+
293
+ if v_message.id is null then
294
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
295
+ || jsonb_build_object(
296
+ 'rpcName', 'send_multicast_message',
297
+ 'subject', trim(p_subject),
298
+ 'priority', p_priority
299
+ );
300
+ if v_idempotency_key is not null then
301
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
302
+ end if;
303
+
304
+ select *
305
+ into v_created_message
306
+ from agent_board.post_message(
307
+ p_thread_id,
308
+ v_actor_agent_id,
309
+ p_body,
310
+ p_message_type,
311
+ null,
312
+ v_metadata
313
+ );
314
+
315
+ select *
316
+ into v_message
317
+ from agent_board.messages
318
+ where id = v_created_message.message_id;
319
+ end if;
320
+
321
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
322
+
323
+ foreach v_recipient_id in array v_recipient_ids loop
324
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
325
+ 'thread',
326
+ p_thread_id,
327
+ 'agent_board.v_thread_feed',
328
+ 'multicast delivery',
329
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', v_recipient_id::text)
330
+ );
331
+
332
+ v_recipient := agent_board._upsert_delivery_recipient(
333
+ 'multicast',
334
+ v_recipient_id,
335
+ 'message',
336
+ v_message.id,
337
+ null,
338
+ null,
339
+ null,
340
+ null,
341
+ jsonb_build_object('priority', p_priority)
342
+ );
343
+
344
+ v_inbox := agent_board._upsert_inbox_item(
345
+ v_recipient.id,
346
+ v_recipient_id,
347
+ null,
348
+ v_recipient.delivery_mode,
349
+ 'message',
350
+ v_message.id,
351
+ null,
352
+ null,
353
+ trim(p_subject),
354
+ v_preview,
355
+ p_priority,
356
+ true,
357
+ false,
358
+ v_fetch_descriptor,
359
+ jsonb_build_object('threadId', p_thread_id::text)
360
+ );
361
+
362
+ v_delivered_count := v_delivered_count + 1;
363
+ v_inbox_count := v_inbox_count + 1;
364
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
365
+ end loop;
366
+
367
+ return query
368
+ select v_message.id, v_delivered_count, v_inbox_count, v_callback_count, v_message.created_at;
369
+ end;
370
+ $$;
371
+
372
+ create or replace function agent_board.post_board_message(
373
+ p_thread_id uuid,
374
+ p_author_agent_id uuid,
375
+ p_board_id uuid,
376
+ p_subject text,
377
+ p_body text,
378
+ p_message_type text default 'comment',
379
+ p_priority text default 'medium',
380
+ p_idempotency_key text default null,
381
+ p_metadata jsonb default '{}'::jsonb
382
+ )
383
+ returns table(message_id uuid, board_id uuid, delivered_recipient_count integer, callback_outbox_count integer, created_at timestamptz)
384
+ language plpgsql
385
+ security definer
386
+ set search_path = agent_board, public
387
+ as $$
388
+ declare
389
+ v_message agent_board.messages%rowtype;
390
+ v_created_message record;
391
+ v_membership record;
392
+ v_recipient agent_board.delivery_recipients%rowtype;
393
+ v_inbox agent_board.inbox_items%rowtype;
394
+ v_delivered_count integer := 0;
395
+ v_callback_count integer := 0;
396
+ v_metadata jsonb;
397
+ v_preview text;
398
+ v_fetch_descriptor jsonb;
399
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
400
+ v_actor_agent_id uuid;
401
+ begin
402
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
403
+
404
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
405
+ raise exception 'subject and body are required' using errcode = '22023';
406
+ end if;
407
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
408
+ raise exception 'invalid priority' using errcode = '22023';
409
+ end if;
410
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
411
+ raise exception 'thread not found' using errcode = 'P0002';
412
+ end if;
413
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
414
+ raise exception 'board not found' using errcode = 'P0002';
415
+ end if;
416
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
417
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
418
+ end if;
419
+ if not agent_board._current_agent_has_board_access(p_board_id) then
420
+ raise exception 'board not accessible to current agent' using errcode = '42501';
421
+ end if;
422
+ if not agent_board._current_agent_can_send_to_board(p_board_id) then
423
+ raise exception 'board send requires a non-observer board role' using errcode = '42501';
424
+ end if;
425
+
426
+ if agent_board._is_service_role() then
427
+ perform agent_board._record_send_authz_telemetry(
428
+ 'service_role_bypass',
429
+ coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
430
+ v_actor_agent_id,
431
+ p_board_id,
432
+ 'post_board_message',
433
+ agent_board._resolve_send_fleet_scope(v_actor_agent_id),
434
+ null,
435
+ jsonb_build_object('redacted', true)
436
+ );
437
+ end if;
438
+
439
+ perform agent_board._lock_idempotency_key(
440
+ 'post_board_message:' || p_thread_id::text || ':' || v_actor_agent_id::text || ':' || p_board_id::text,
441
+ v_idempotency_key
442
+ );
443
+
444
+ if v_idempotency_key is not null then
445
+ select *
446
+ into v_message
447
+ from agent_board.messages
448
+ where thread_id = p_thread_id
449
+ and author_agent_id = v_actor_agent_id
450
+ and metadata ->> 'rpcName' = 'post_board_message'
451
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
452
+ order by created_at desc
453
+ limit 1;
454
+ end if;
455
+
456
+ if v_message.id is null then
457
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
458
+ || jsonb_build_object(
459
+ 'rpcName', 'post_board_message',
460
+ 'subject', trim(p_subject),
461
+ 'priority', p_priority,
462
+ 'boardId', p_board_id::text
463
+ );
464
+ if v_idempotency_key is not null then
465
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
466
+ end if;
467
+
468
+ select *
469
+ into v_created_message
470
+ from agent_board.post_message(
471
+ p_thread_id,
472
+ v_actor_agent_id,
473
+ p_body,
474
+ p_message_type,
475
+ null,
476
+ v_metadata
477
+ );
478
+
479
+ select *
480
+ into v_message
481
+ from agent_board.messages
482
+ where id = v_created_message.message_id;
483
+ end if;
484
+
485
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
486
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
487
+ 'board',
488
+ p_board_id,
489
+ 'agent_board.v_board_feed',
490
+ 'board delivery',
491
+ jsonb_build_object('messageId', v_message.id::text)
492
+ );
493
+
494
+ for v_membership in
495
+ select bm.id, bm.agent_id
496
+ from agent_board.board_memberships bm
497
+ where bm.board_id = p_board_id
498
+ and bm.membership_state in ('joined', 'subscribed')
499
+ order by bm.created_at
500
+ loop
501
+ v_recipient := agent_board._upsert_delivery_recipient(
502
+ 'board',
503
+ v_membership.agent_id,
504
+ 'message',
505
+ v_message.id,
506
+ null,
507
+ null,
508
+ p_board_id,
509
+ v_membership.id,
510
+ jsonb_build_object('priority', p_priority)
511
+ );
512
+
513
+ v_inbox := agent_board._upsert_inbox_item(
514
+ v_recipient.id,
515
+ v_membership.agent_id,
516
+ p_board_id,
517
+ v_recipient.delivery_mode,
518
+ 'message',
519
+ v_message.id,
520
+ null,
521
+ null,
522
+ trim(p_subject),
523
+ v_preview,
524
+ p_priority,
525
+ true,
526
+ true,
527
+ v_fetch_descriptor,
528
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
529
+ );
530
+
531
+ v_delivered_count := v_delivered_count + 1;
532
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
533
+ end loop;
534
+
535
+ v_callback_count := v_callback_count + agent_board._queue_board_post_callbacks(
536
+ p_board_id,
537
+ v_message.id,
538
+ trim(p_subject),
539
+ v_preview,
540
+ p_priority,
541
+ v_fetch_descriptor
542
+ );
543
+
544
+ return query
545
+ select v_message.id, p_board_id, v_delivered_count, v_callback_count, v_message.created_at;
546
+ end;
547
+ $$;
548
+
549
+ create or replace function agent_board.create_tasking_delivery(
550
+ p_thread_id uuid,
551
+ p_created_by_agent_id uuid,
552
+ p_title text,
553
+ p_details text default null,
554
+ p_recipient_agent_ids uuid[] default null,
555
+ p_board_id uuid default null,
556
+ p_priority text default 'medium',
557
+ p_orchestration_mode text default 'distributed',
558
+ p_idempotency_key text default null,
559
+ p_metadata jsonb default '{}'::jsonb
560
+ )
561
+ returns table(task_id uuid, created_event_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz, updated_at timestamptz)
562
+ language plpgsql
563
+ security definer
564
+ set search_path = agent_board, public
565
+ as $$
566
+ declare
567
+ v_task agent_board.tasks%rowtype;
568
+ v_created_event_id uuid;
569
+ v_created_task record;
570
+ v_recipient_ids uuid[];
571
+ v_recipient_id uuid;
572
+ v_membership record;
573
+ v_recipient agent_board.delivery_recipients%rowtype;
574
+ v_inbox agent_board.inbox_items%rowtype;
575
+ v_delivered_count integer := 0;
576
+ v_inbox_count integer := 0;
577
+ v_callback_count integer := 0;
578
+ v_metadata jsonb;
579
+ v_preview text;
580
+ v_fetch_descriptor jsonb;
581
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
582
+ v_actor_agent_id uuid;
583
+ begin
584
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
585
+
586
+ if nullif(trim(p_title), '') is null then
587
+ raise exception 'task title is required' using errcode = '22023';
588
+ end if;
589
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
590
+ raise exception 'invalid priority' using errcode = '22023';
591
+ end if;
592
+ if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
593
+ raise exception 'invalid orchestration mode' using errcode = '22023';
594
+ end if;
595
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
596
+ raise exception 'thread not found' using errcode = 'P0002';
597
+ end if;
598
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
599
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
600
+ end if;
601
+ if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
602
+ raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
603
+ end if;
604
+ if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
605
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
606
+ end if;
607
+ if p_board_id is not null then
608
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
609
+ raise exception 'board not found' using errcode = 'P0002';
610
+ end if;
611
+ if not agent_board._current_agent_has_board_access(p_board_id) then
612
+ raise exception 'board not accessible to current agent' using errcode = '42501';
613
+ end if;
614
+ if not agent_board._current_agent_can_send_to_board(p_board_id) then
615
+ raise exception 'board send requires a non-observer board role' using errcode = '42501';
616
+ end if;
617
+ end if;
618
+
619
+ select array_agg(distinct recipient_id)
620
+ into v_recipient_ids
621
+ from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
622
+
623
+ if v_recipient_ids is not null and (
624
+ select count(*)
625
+ from agent_board.agents
626
+ where id = any(v_recipient_ids)
627
+ ) <> cardinality(v_recipient_ids) then
628
+ raise exception 'recipient agent not found' using errcode = 'P0002';
629
+ end if;
630
+
631
+ if agent_board._is_service_role() then
632
+ perform agent_board._record_send_authz_telemetry(
633
+ 'service_role_bypass',
634
+ coalesce(nullif(p_metadata -> 'signed_write' ->> 'origin', ''), 'unknown'),
635
+ v_actor_agent_id,
636
+ p_board_id,
637
+ 'create_tasking_delivery',
638
+ agent_board._resolve_send_fleet_scope(v_actor_agent_id),
639
+ null,
640
+ jsonb_build_object('redacted', true)
641
+ );
642
+ end if;
643
+
644
+ perform agent_board._lock_idempotency_key(
645
+ 'create_tasking_delivery:' || p_thread_id::text || ':' || v_actor_agent_id::text,
646
+ v_idempotency_key
647
+ );
648
+
649
+ if v_idempotency_key is not null then
650
+ select *
651
+ into v_task
652
+ from agent_board.tasks
653
+ where thread_id = p_thread_id
654
+ and created_by_agent_id = v_actor_agent_id
655
+ and metadata ->> 'rpcName' = 'create_tasking_delivery'
656
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
657
+ order by created_at desc
658
+ limit 1;
659
+ end if;
660
+
661
+ if v_task.id is null then
662
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
663
+ || jsonb_build_object(
664
+ 'rpcName', 'create_tasking_delivery',
665
+ 'priority', p_priority
666
+ );
667
+ if p_board_id is not null then
668
+ v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
669
+ end if;
670
+ if v_idempotency_key is not null then
671
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
672
+ end if;
673
+
674
+ select *
675
+ into v_created_task
676
+ from agent_board.create_task(
677
+ p_thread_id,
678
+ v_actor_agent_id,
679
+ trim(p_title),
680
+ p_details,
681
+ null,
682
+ null,
683
+ p_priority,
684
+ p_orchestration_mode,
685
+ v_metadata
686
+ );
687
+
688
+ v_created_event_id := v_created_task.created_event_id;
689
+
690
+ select *
691
+ into v_task
692
+ from agent_board.tasks
693
+ where id = v_created_task.task_id;
694
+ else
695
+ select te.id
696
+ into v_created_event_id
697
+ from agent_board.task_events te
698
+ where te.task_id = v_task.id
699
+ and te.event_sequence = 1;
700
+ end if;
701
+
702
+ v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
703
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
704
+ 'task',
705
+ v_task.id,
706
+ 'agent_board.v_task_tree',
707
+ 'tasking delivery',
708
+ jsonb_build_object('threadId', p_thread_id::text)
709
+ );
710
+
711
+ if v_recipient_ids is not null then
712
+ foreach v_recipient_id in array v_recipient_ids loop
713
+ v_recipient := agent_board._upsert_delivery_recipient(
714
+ 'tasking',
715
+ v_recipient_id,
716
+ 'task',
717
+ null,
718
+ v_task.id,
719
+ null,
720
+ null,
721
+ null,
722
+ jsonb_build_object('priority', p_priority)
723
+ );
724
+
725
+ v_inbox := agent_board._upsert_inbox_item(
726
+ v_recipient.id,
727
+ v_recipient_id,
728
+ null,
729
+ v_recipient.delivery_mode,
730
+ 'task',
731
+ null,
732
+ v_task.id,
733
+ null,
734
+ trim(p_title),
735
+ v_preview,
736
+ p_priority,
737
+ true,
738
+ false,
739
+ v_fetch_descriptor,
740
+ jsonb_build_object('threadId', p_thread_id::text)
741
+ );
742
+
743
+ v_delivered_count := v_delivered_count + 1;
744
+ v_inbox_count := v_inbox_count + 1;
745
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
746
+ end loop;
747
+ end if;
748
+
749
+ if p_board_id is not null then
750
+ for v_membership in
751
+ select bm.id, bm.agent_id
752
+ from agent_board.board_memberships bm
753
+ where bm.board_id = p_board_id
754
+ and bm.membership_state in ('joined', 'subscribed')
755
+ order by bm.created_at
756
+ loop
757
+ v_recipient := agent_board._upsert_delivery_recipient(
758
+ 'tasking',
759
+ v_membership.agent_id,
760
+ 'task',
761
+ null,
762
+ v_task.id,
763
+ null,
764
+ p_board_id,
765
+ v_membership.id,
766
+ jsonb_build_object('priority', p_priority)
767
+ );
768
+
769
+ v_inbox := agent_board._upsert_inbox_item(
770
+ v_recipient.id,
771
+ v_membership.agent_id,
772
+ v_recipient.board_id,
773
+ v_recipient.delivery_mode,
774
+ 'task',
775
+ null,
776
+ v_task.id,
777
+ null,
778
+ trim(p_title),
779
+ v_preview,
780
+ p_priority,
781
+ true,
782
+ v_recipient.board_id is not null,
783
+ v_fetch_descriptor,
784
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
785
+ );
786
+
787
+ v_delivered_count := v_delivered_count + 1;
788
+ v_inbox_count := v_inbox_count + 1;
789
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
790
+ end loop;
791
+ end if;
792
+
793
+ return query
794
+ 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;
795
+ end;
796
+ $$;
797
+
798
+ -- 3. Re-assert dormancy grants: all four core send RPCs stay revoked from
799
+ -- authenticated (mirrors 20260711000100:224-231). create or replace preserves
800
+ -- grants, but re-assert so a fresh replay is order-independent.
801
+ revoke execute on function agent_board.send_direct_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
802
+ revoke execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) from authenticated;
803
+ revoke execute on function agent_board.post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
804
+ revoke execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) from authenticated;