@consiliency/agent-board-schema 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +55 -0
  4. package/bin/agent-board-schema.js +97 -0
  5. package/fixtures/already-applied.json +181 -0
  6. package/fixtures/divergent-history.json +173 -0
  7. package/fixtures/production-history.json +173 -0
  8. package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
  9. package/migrations/20260502000000_agent_board_schema.sql +180 -0
  10. package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
  11. package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
  12. package/migrations/20260504000000_agent_board_read_models.sql +206 -0
  13. package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
  14. package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
  15. package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
  16. package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
  17. package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
  18. package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
  19. package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
  20. package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
  21. package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
  22. package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
  23. package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
  24. package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
  25. package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
  26. package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
  27. package/migrations/20260513000001_agent_board_retention.sql +160 -0
  28. package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
  29. package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
  30. package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
  31. package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
  32. package/migrations/20260517000000_agent_board_addressing.sql +542 -0
  33. package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
  34. package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
  35. package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
  36. package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
  37. package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
  38. package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
  39. package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
  40. package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
  41. package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
  42. package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
  43. package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
  44. package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
  45. package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
  46. package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
  47. package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
  48. package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
  49. package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
  50. package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
  51. package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
  52. package/package.json +42 -0
  53. package/provenance.json +580 -0
  54. package/schema-manifest.json +458 -0
  55. package/src/check.js +23 -0
  56. package/src/index.js +50 -0
  57. package/src/materialize.js +73 -0
  58. package/src/plan.js +44 -0
  59. package/src/tooling.test.mjs +97 -0
  60. package/tests/agent_board_addressing.test.sql +166 -0
  61. package/tests/agent_board_auth_rls.test.sql +243 -0
  62. package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
  63. package/tests/agent_board_auth_rpcs.test.sql +311 -0
  64. package/tests/agent_board_auth_schema.test.sql +411 -0
  65. package/tests/agent_board_authbound_boundary.test.sql +148 -0
  66. package/tests/agent_board_boardopt.test.sql +170 -0
  67. package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
  68. package/tests/agent_board_callback_runtime.test.sql +425 -0
  69. package/tests/agent_board_callbackcap.test.sql +222 -0
  70. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
  71. package/tests/agent_board_dblint_rpcs.test.sql +247 -0
  72. package/tests/agent_board_fleet_policy.test.sql +370 -0
  73. package/tests/agent_board_fleet_read_models.test.sql +158 -0
  74. package/tests/agent_board_fleet_registry.test.sql +149 -0
  75. package/tests/agent_board_inbox_reads.test.sql +471 -0
  76. package/tests/agent_board_inbox_rpcs.test.sql +452 -0
  77. package/tests/agent_board_inbox_schema.test.sql +355 -0
  78. package/tests/agent_board_nonfleet_identity.test.sql +26 -0
  79. package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
  80. package/tests/agent_board_schema_version.test.sql +24 -0
  81. package/tests/agent_board_schema_version_rpc.test.sql +40 -0
  82. package/tests/agent_board_signature_rls_reads.test.sql +133 -0
  83. package/tests/agent_board_signed_write_gateway.test.sql +219 -0
  84. package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
  85. package/tests/agent_board_signing_store.test.sql +183 -0
  86. package/tests/agent_board_smoke.test.sql +12 -0
  87. package/tests/agent_board_v1_invariants.test.sql +298 -0
  88. package/tests/agent_board_v2_invariants.test.sql +753 -0
  89. package/tests/agent_board_workload_providers.test.sql +15 -0
  90. package/tests/fixtures/adopter-base.sql +19 -0
  91. package/tests/fixtures/operator-hardening.sql +27 -0
  92. package/tests/fixtures/test-prereqs.sql +84 -0
@@ -0,0 +1,1243 @@
1
+ create or replace function agent_board.create_board(
2
+ p_created_by_agent_id uuid,
3
+ p_slug text,
4
+ p_name text,
5
+ p_description text default null,
6
+ p_metadata jsonb default '{}'::jsonb
7
+ )
8
+ returns table(board_id uuid, slug text, name text, status text, created_at timestamptz, updated_at timestamptz)
9
+ language plpgsql
10
+ security definer
11
+ set search_path = agent_board, public
12
+ as $$
13
+ declare
14
+ v_actor_agent_id uuid;
15
+ begin
16
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
17
+
18
+ if nullif(trim(p_slug), '') is null or nullif(trim(p_name), '') is null then
19
+ raise exception 'board slug and name are required' using errcode = '22023';
20
+ end if;
21
+
22
+ return query
23
+ insert into agent_board.boards as b(slug, name, description, created_by_agent_id, metadata)
24
+ values (trim(p_slug), trim(p_name), nullif(trim(p_description), ''), v_actor_agent_id, coalesce(p_metadata, '{}'::jsonb))
25
+ on conflict on constraint boards_slug_key do update
26
+ set name = excluded.name,
27
+ description = excluded.description,
28
+ status = 'active',
29
+ metadata = coalesce(b.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb),
30
+ updated_at = now()
31
+ returning b.id, b.slug, b.name, b.status, b.created_at, b.updated_at;
32
+ end;
33
+ $$;
34
+
35
+ create or replace function agent_board.join_board(
36
+ p_board_id uuid,
37
+ p_agent_id uuid,
38
+ p_membership_state text default 'joined',
39
+ p_created_by_agent_id uuid default null,
40
+ p_metadata jsonb default '{}'::jsonb
41
+ )
42
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, created_at timestamptz, updated_at timestamptz)
43
+ language plpgsql
44
+ security definer
45
+ set search_path = agent_board, public
46
+ as $$
47
+ declare
48
+ v_membership agent_board.board_memberships%rowtype;
49
+ v_actor_agent_id uuid;
50
+ v_created_by_agent_id uuid;
51
+ begin
52
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
53
+ if p_created_by_agent_id is null then
54
+ v_created_by_agent_id := case when agent_board._is_service_role() then null else v_actor_agent_id end;
55
+ else
56
+ v_created_by_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
57
+ end if;
58
+
59
+ if p_membership_state not in ('joined', 'subscribed') then
60
+ raise exception 'join_board only accepts joined or subscribed' using errcode = '22023';
61
+ end if;
62
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
63
+ raise exception 'board not found' using errcode = 'P0002';
64
+ end if;
65
+
66
+ select *
67
+ into v_membership
68
+ from agent_board.board_memberships bm
69
+ where bm.board_id = p_board_id
70
+ and bm.agent_id = v_actor_agent_id
71
+ order by bm.created_at desc
72
+ limit 1
73
+ for update;
74
+
75
+ if v_membership.id is null then
76
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, metadata)
77
+ values (p_board_id, v_actor_agent_id, p_membership_state, coalesce(v_created_by_agent_id, v_actor_agent_id), coalesce(p_metadata, '{}'::jsonb))
78
+ returning * into v_membership;
79
+ else
80
+ update agent_board.board_memberships bm
81
+ set membership_state = p_membership_state,
82
+ left_at = null,
83
+ metadata = coalesce(bm.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
84
+ updated_at = now()
85
+ where bm.id = v_membership.id
86
+ returning * into v_membership;
87
+ end if;
88
+
89
+ return query
90
+ select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.created_at, v_membership.updated_at;
91
+ end;
92
+ $$;
93
+
94
+ create or replace function agent_board.update_board_membership(
95
+ p_board_id uuid,
96
+ p_agent_id uuid,
97
+ p_membership_state text,
98
+ p_metadata jsonb default '{}'::jsonb
99
+ )
100
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamptz, updated_at timestamptz)
101
+ language plpgsql
102
+ security definer
103
+ set search_path = agent_board, public
104
+ as $$
105
+ declare
106
+ v_membership agent_board.board_memberships%rowtype;
107
+ v_actor_agent_id uuid;
108
+ begin
109
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
110
+
111
+ if p_membership_state not in ('joined', 'subscribed', 'left', 'removed') then
112
+ raise exception 'invalid board membership state' using errcode = '22023';
113
+ end if;
114
+
115
+ select *
116
+ into v_membership
117
+ from agent_board.board_memberships bm
118
+ where bm.board_id = p_board_id
119
+ and bm.agent_id = v_actor_agent_id
120
+ order by bm.created_at desc
121
+ limit 1
122
+ for update;
123
+
124
+ if v_membership.id is null then
125
+ raise exception 'board membership not found' using errcode = 'P0002';
126
+ end if;
127
+
128
+ update agent_board.board_memberships bm
129
+ set membership_state = p_membership_state,
130
+ left_at = case when p_membership_state in ('left', 'removed') then now() else null end,
131
+ metadata = coalesce(bm.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
132
+ updated_at = now()
133
+ where bm.id = v_membership.id
134
+ returning * into v_membership;
135
+
136
+ return query
137
+ select v_membership.id, v_membership.board_id, v_membership.agent_id, v_membership.membership_state, v_membership.left_at, v_membership.updated_at;
138
+ end;
139
+ $$;
140
+
141
+ create or replace function agent_board.leave_board(
142
+ p_board_id uuid,
143
+ p_agent_id uuid,
144
+ p_metadata jsonb default '{}'::jsonb
145
+ )
146
+ returns table(membership_id uuid, board_id uuid, agent_id uuid, membership_state text, left_at timestamptz, updated_at timestamptz)
147
+ language plpgsql
148
+ security definer
149
+ set search_path = agent_board, public
150
+ as $$
151
+ declare
152
+ v_actor_agent_id uuid;
153
+ begin
154
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
155
+
156
+ return query
157
+ select *
158
+ from agent_board.update_board_membership(p_board_id, v_actor_agent_id, 'left', p_metadata);
159
+ end;
160
+ $$;
161
+
162
+ create or replace function agent_board.send_direct_message(
163
+ p_thread_id uuid,
164
+ p_author_agent_id uuid,
165
+ p_recipient_agent_id uuid,
166
+ p_subject text,
167
+ p_body text,
168
+ p_message_type text default 'comment',
169
+ p_priority text default 'medium',
170
+ p_idempotency_key text default null,
171
+ p_metadata jsonb default '{}'::jsonb
172
+ )
173
+ returns table(message_id uuid, delivery_recipient_id uuid, inbox_item_id uuid, callback_outbox_count integer, created_at timestamptz)
174
+ language plpgsql
175
+ security definer
176
+ set search_path = agent_board, public
177
+ as $$
178
+ declare
179
+ v_message agent_board.messages%rowtype;
180
+ v_created_message record;
181
+ v_recipient agent_board.delivery_recipients%rowtype;
182
+ v_inbox agent_board.inbox_items%rowtype;
183
+ v_callback_count integer := 0;
184
+ v_metadata jsonb;
185
+ v_preview text;
186
+ v_fetch_descriptor jsonb;
187
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
188
+ v_actor_agent_id uuid;
189
+ begin
190
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
191
+
192
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
193
+ raise exception 'subject and body are required' using errcode = '22023';
194
+ end if;
195
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
196
+ raise exception 'invalid priority' using errcode = '22023';
197
+ end if;
198
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
199
+ raise exception 'thread not found' using errcode = 'P0002';
200
+ end if;
201
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
202
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
203
+ end if;
204
+ if not exists (select 1 from agent_board.agents where id = p_recipient_agent_id) then
205
+ raise exception 'recipient agent not found' using errcode = 'P0002';
206
+ end if;
207
+
208
+ perform agent_board._lock_idempotency_key(
209
+ 'send_direct_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
210
+ v_idempotency_key
211
+ );
212
+
213
+ if v_idempotency_key is not null then
214
+ select *
215
+ into v_message
216
+ from agent_board.messages
217
+ where thread_id = p_thread_id
218
+ and author_agent_id = v_actor_agent_id
219
+ and metadata ->> 'rpcName' = 'send_direct_message'
220
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
221
+ order by created_at desc
222
+ limit 1;
223
+ end if;
224
+
225
+ if v_message.id is null then
226
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
227
+ || jsonb_build_object(
228
+ 'rpcName', 'send_direct_message',
229
+ 'subject', trim(p_subject),
230
+ 'priority', p_priority
231
+ );
232
+ if v_idempotency_key is not null then
233
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
234
+ end if;
235
+
236
+ select *
237
+ into v_created_message
238
+ from agent_board.post_message(
239
+ p_thread_id,
240
+ v_actor_agent_id,
241
+ p_body,
242
+ p_message_type,
243
+ null,
244
+ v_metadata
245
+ );
246
+
247
+ select *
248
+ into v_message
249
+ from agent_board.messages
250
+ where id = v_created_message.message_id;
251
+ end if;
252
+
253
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
254
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
255
+ 'thread',
256
+ p_thread_id,
257
+ 'agent_board.v_thread_feed',
258
+ 'direct delivery',
259
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', p_recipient_agent_id::text)
260
+ );
261
+
262
+ v_recipient := agent_board._upsert_delivery_recipient(
263
+ 'direct',
264
+ p_recipient_agent_id,
265
+ 'message',
266
+ v_message.id,
267
+ null,
268
+ null,
269
+ null,
270
+ null,
271
+ jsonb_build_object('priority', p_priority)
272
+ );
273
+
274
+ v_inbox := agent_board._upsert_inbox_item(
275
+ v_recipient.id,
276
+ p_recipient_agent_id,
277
+ null,
278
+ v_recipient.delivery_mode,
279
+ 'message',
280
+ v_message.id,
281
+ null,
282
+ null,
283
+ trim(p_subject),
284
+ v_preview,
285
+ p_priority,
286
+ true,
287
+ false,
288
+ v_fetch_descriptor,
289
+ jsonb_build_object('threadId', p_thread_id::text)
290
+ );
291
+
292
+ v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
293
+
294
+ return query
295
+ select v_message.id, v_recipient.id, v_inbox.id, v_callback_count, v_message.created_at;
296
+ end;
297
+ $$;
298
+
299
+ create or replace function agent_board.send_multicast_message(
300
+ p_thread_id uuid,
301
+ p_author_agent_id uuid,
302
+ p_recipient_agent_ids uuid[],
303
+ p_subject text,
304
+ p_body text,
305
+ p_message_type text default 'comment',
306
+ p_priority text default 'medium',
307
+ p_idempotency_key text default null,
308
+ p_metadata jsonb default '{}'::jsonb
309
+ )
310
+ returns table(message_id uuid, delivered_recipient_count integer, inbox_item_count integer, callback_outbox_count integer, created_at timestamptz)
311
+ language plpgsql
312
+ security definer
313
+ set search_path = agent_board, public
314
+ as $$
315
+ declare
316
+ v_message agent_board.messages%rowtype;
317
+ v_created_message record;
318
+ v_recipient_ids uuid[];
319
+ v_recipient_id uuid;
320
+ v_recipient agent_board.delivery_recipients%rowtype;
321
+ v_inbox agent_board.inbox_items%rowtype;
322
+ v_delivered_count integer := 0;
323
+ v_inbox_count integer := 0;
324
+ v_callback_count integer := 0;
325
+ v_metadata jsonb;
326
+ v_preview text;
327
+ v_fetch_descriptor jsonb;
328
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
329
+ v_actor_agent_id uuid;
330
+ begin
331
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
332
+
333
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
334
+ raise exception 'subject and body are required' using errcode = '22023';
335
+ end if;
336
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
337
+ raise exception 'invalid priority' using errcode = '22023';
338
+ end if;
339
+ if p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0 then
340
+ raise exception 'recipient_agent_ids must not be empty' using errcode = '22023';
341
+ end if;
342
+ if array_position(p_recipient_agent_ids, null) is not null then
343
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
344
+ end if;
345
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
346
+ raise exception 'thread not found' using errcode = 'P0002';
347
+ end if;
348
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
349
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
350
+ end if;
351
+
352
+ select array_agg(distinct recipient_id)
353
+ into v_recipient_ids
354
+ from unnest(p_recipient_agent_ids) as recipient_id;
355
+
356
+ if (
357
+ select count(*)
358
+ from agent_board.agents
359
+ where id = any(v_recipient_ids)
360
+ ) <> cardinality(v_recipient_ids) then
361
+ raise exception 'recipient agent not found' using errcode = 'P0002';
362
+ end if;
363
+
364
+ perform agent_board._lock_idempotency_key(
365
+ 'send_multicast_message:' || p_thread_id::text || ':' || v_actor_agent_id::text,
366
+ v_idempotency_key
367
+ );
368
+
369
+ if v_idempotency_key is not null then
370
+ select *
371
+ into v_message
372
+ from agent_board.messages
373
+ where thread_id = p_thread_id
374
+ and author_agent_id = v_actor_agent_id
375
+ and metadata ->> 'rpcName' = 'send_multicast_message'
376
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
377
+ order by created_at desc
378
+ limit 1;
379
+ end if;
380
+
381
+ if v_message.id is null then
382
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
383
+ || jsonb_build_object(
384
+ 'rpcName', 'send_multicast_message',
385
+ 'subject', trim(p_subject),
386
+ 'priority', p_priority
387
+ );
388
+ if v_idempotency_key is not null then
389
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
390
+ end if;
391
+
392
+ select *
393
+ into v_created_message
394
+ from agent_board.post_message(
395
+ p_thread_id,
396
+ v_actor_agent_id,
397
+ p_body,
398
+ p_message_type,
399
+ null,
400
+ v_metadata
401
+ );
402
+
403
+ select *
404
+ into v_message
405
+ from agent_board.messages
406
+ where id = v_created_message.message_id;
407
+ end if;
408
+
409
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
410
+
411
+ foreach v_recipient_id in array v_recipient_ids loop
412
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
413
+ 'thread',
414
+ p_thread_id,
415
+ 'agent_board.v_thread_feed',
416
+ 'multicast delivery',
417
+ jsonb_build_object('messageId', v_message.id::text, 'recipientAgentId', v_recipient_id::text)
418
+ );
419
+
420
+ v_recipient := agent_board._upsert_delivery_recipient(
421
+ 'multicast',
422
+ v_recipient_id,
423
+ 'message',
424
+ v_message.id,
425
+ null,
426
+ null,
427
+ null,
428
+ null,
429
+ jsonb_build_object('priority', p_priority)
430
+ );
431
+
432
+ v_inbox := agent_board._upsert_inbox_item(
433
+ v_recipient.id,
434
+ v_recipient_id,
435
+ null,
436
+ v_recipient.delivery_mode,
437
+ 'message',
438
+ v_message.id,
439
+ null,
440
+ null,
441
+ trim(p_subject),
442
+ v_preview,
443
+ p_priority,
444
+ true,
445
+ false,
446
+ v_fetch_descriptor,
447
+ jsonb_build_object('threadId', p_thread_id::text)
448
+ );
449
+
450
+ v_delivered_count := v_delivered_count + 1;
451
+ v_inbox_count := v_inbox_count + 1;
452
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
453
+ end loop;
454
+
455
+ return query
456
+ select v_message.id, v_delivered_count, v_inbox_count, v_callback_count, v_message.created_at;
457
+ end;
458
+ $$;
459
+
460
+ create or replace function agent_board.post_board_message(
461
+ p_thread_id uuid,
462
+ p_author_agent_id uuid,
463
+ p_board_id uuid,
464
+ p_subject text,
465
+ p_body text,
466
+ p_message_type text default 'comment',
467
+ p_priority text default 'medium',
468
+ p_idempotency_key text default null,
469
+ p_metadata jsonb default '{}'::jsonb
470
+ )
471
+ returns table(message_id uuid, board_id uuid, delivered_recipient_count integer, callback_outbox_count integer, created_at timestamptz)
472
+ language plpgsql
473
+ security definer
474
+ set search_path = agent_board, public
475
+ as $$
476
+ declare
477
+ v_message agent_board.messages%rowtype;
478
+ v_created_message record;
479
+ v_membership record;
480
+ v_recipient agent_board.delivery_recipients%rowtype;
481
+ v_inbox agent_board.inbox_items%rowtype;
482
+ v_delivered_count integer := 0;
483
+ v_callback_count integer := 0;
484
+ v_metadata jsonb;
485
+ v_preview text;
486
+ v_fetch_descriptor jsonb;
487
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
488
+ v_actor_agent_id uuid;
489
+ begin
490
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
491
+
492
+ if nullif(trim(p_subject), '') is null or nullif(trim(p_body), '') is null then
493
+ raise exception 'subject and body are required' using errcode = '22023';
494
+ end if;
495
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
496
+ raise exception 'invalid priority' using errcode = '22023';
497
+ end if;
498
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
499
+ raise exception 'thread not found' using errcode = 'P0002';
500
+ end if;
501
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
502
+ raise exception 'board not found' using errcode = 'P0002';
503
+ end if;
504
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
505
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
506
+ end if;
507
+ if not agent_board._current_agent_has_board_access(p_board_id) then
508
+ raise exception 'board not accessible to current agent' using errcode = '42501';
509
+ end if;
510
+
511
+ perform agent_board._lock_idempotency_key(
512
+ 'post_board_message:' || p_thread_id::text || ':' || v_actor_agent_id::text || ':' || p_board_id::text,
513
+ v_idempotency_key
514
+ );
515
+
516
+ if v_idempotency_key is not null then
517
+ select *
518
+ into v_message
519
+ from agent_board.messages
520
+ where thread_id = p_thread_id
521
+ and author_agent_id = v_actor_agent_id
522
+ and metadata ->> 'rpcName' = 'post_board_message'
523
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
524
+ order by created_at desc
525
+ limit 1;
526
+ end if;
527
+
528
+ if v_message.id is null then
529
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
530
+ || jsonb_build_object(
531
+ 'rpcName', 'post_board_message',
532
+ 'subject', trim(p_subject),
533
+ 'priority', p_priority,
534
+ 'boardId', p_board_id::text
535
+ );
536
+ if v_idempotency_key is not null then
537
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
538
+ end if;
539
+
540
+ select *
541
+ into v_created_message
542
+ from agent_board.post_message(
543
+ p_thread_id,
544
+ v_actor_agent_id,
545
+ p_body,
546
+ p_message_type,
547
+ null,
548
+ v_metadata
549
+ );
550
+
551
+ select *
552
+ into v_message
553
+ from agent_board.messages
554
+ where id = v_created_message.message_id;
555
+ end if;
556
+
557
+ v_preview := agent_board._trim_inbox_text(p_body, 280);
558
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
559
+ 'board',
560
+ p_board_id,
561
+ 'agent_board.v_board_feed',
562
+ 'board delivery',
563
+ jsonb_build_object('messageId', v_message.id::text)
564
+ );
565
+
566
+ for v_membership in
567
+ select bm.id, bm.agent_id
568
+ from agent_board.board_memberships bm
569
+ where bm.board_id = p_board_id
570
+ and bm.membership_state in ('joined', 'subscribed')
571
+ order by bm.created_at
572
+ loop
573
+ v_recipient := agent_board._upsert_delivery_recipient(
574
+ 'board',
575
+ v_membership.agent_id,
576
+ 'message',
577
+ v_message.id,
578
+ null,
579
+ null,
580
+ p_board_id,
581
+ v_membership.id,
582
+ jsonb_build_object('priority', p_priority)
583
+ );
584
+
585
+ v_inbox := agent_board._upsert_inbox_item(
586
+ v_recipient.id,
587
+ v_membership.agent_id,
588
+ p_board_id,
589
+ v_recipient.delivery_mode,
590
+ 'message',
591
+ v_message.id,
592
+ null,
593
+ null,
594
+ trim(p_subject),
595
+ v_preview,
596
+ p_priority,
597
+ true,
598
+ true,
599
+ v_fetch_descriptor,
600
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
601
+ );
602
+
603
+ v_delivered_count := v_delivered_count + 1;
604
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
605
+ end loop;
606
+
607
+ v_callback_count := v_callback_count + agent_board._queue_board_post_callbacks(
608
+ p_board_id,
609
+ v_message.id,
610
+ trim(p_subject),
611
+ v_preview,
612
+ p_priority,
613
+ v_fetch_descriptor
614
+ );
615
+
616
+ return query
617
+ select v_message.id, p_board_id, v_delivered_count, v_callback_count, v_message.created_at;
618
+ end;
619
+ $$;
620
+
621
+ create or replace function agent_board.create_tasking_delivery(
622
+ p_thread_id uuid,
623
+ p_created_by_agent_id uuid,
624
+ p_title text,
625
+ p_details text default null,
626
+ p_recipient_agent_ids uuid[] default null,
627
+ p_board_id uuid default null,
628
+ p_priority text default 'medium',
629
+ p_orchestration_mode text default 'distributed',
630
+ p_idempotency_key text default null,
631
+ p_metadata jsonb default '{}'::jsonb
632
+ )
633
+ 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)
634
+ language plpgsql
635
+ security definer
636
+ set search_path = agent_board, public
637
+ as $$
638
+ declare
639
+ v_task agent_board.tasks%rowtype;
640
+ v_created_event_id uuid;
641
+ v_created_task record;
642
+ v_recipient_ids uuid[];
643
+ v_recipient_id uuid;
644
+ v_membership record;
645
+ v_recipient agent_board.delivery_recipients%rowtype;
646
+ v_inbox agent_board.inbox_items%rowtype;
647
+ v_delivered_count integer := 0;
648
+ v_inbox_count integer := 0;
649
+ v_callback_count integer := 0;
650
+ v_metadata jsonb;
651
+ v_preview text;
652
+ v_fetch_descriptor jsonb;
653
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
654
+ v_actor_agent_id uuid;
655
+ begin
656
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
657
+
658
+ if nullif(trim(p_title), '') is null then
659
+ raise exception 'task title is required' using errcode = '22023';
660
+ end if;
661
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
662
+ raise exception 'invalid priority' using errcode = '22023';
663
+ end if;
664
+ if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
665
+ raise exception 'invalid orchestration mode' using errcode = '22023';
666
+ end if;
667
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
668
+ raise exception 'thread not found' using errcode = 'P0002';
669
+ end if;
670
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
671
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
672
+ end if;
673
+ if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
674
+ raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
675
+ end if;
676
+ if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
677
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
678
+ end if;
679
+ if p_board_id is not null then
680
+ if not exists (select 1 from agent_board.boards where id = p_board_id) then
681
+ raise exception 'board not found' using errcode = 'P0002';
682
+ end if;
683
+ if not agent_board._current_agent_has_board_access(p_board_id) then
684
+ raise exception 'board not accessible to current agent' using errcode = '42501';
685
+ end if;
686
+ end if;
687
+
688
+ select array_agg(distinct recipient_id)
689
+ into v_recipient_ids
690
+ from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
691
+
692
+ if v_recipient_ids is not null and (
693
+ select count(*)
694
+ from agent_board.agents
695
+ where id = any(v_recipient_ids)
696
+ ) <> cardinality(v_recipient_ids) then
697
+ raise exception 'recipient agent not found' using errcode = 'P0002';
698
+ end if;
699
+
700
+ perform agent_board._lock_idempotency_key(
701
+ 'create_tasking_delivery:' || p_thread_id::text || ':' || v_actor_agent_id::text,
702
+ v_idempotency_key
703
+ );
704
+
705
+ if v_idempotency_key is not null then
706
+ select *
707
+ into v_task
708
+ from agent_board.tasks
709
+ where thread_id = p_thread_id
710
+ and created_by_agent_id = v_actor_agent_id
711
+ and metadata ->> 'rpcName' = 'create_tasking_delivery'
712
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
713
+ order by created_at desc
714
+ limit 1;
715
+ end if;
716
+
717
+ if v_task.id is null then
718
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
719
+ || jsonb_build_object(
720
+ 'rpcName', 'create_tasking_delivery',
721
+ 'priority', p_priority
722
+ );
723
+ if p_board_id is not null then
724
+ v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
725
+ end if;
726
+ if v_idempotency_key is not null then
727
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
728
+ end if;
729
+
730
+ select *
731
+ into v_created_task
732
+ from agent_board.create_task(
733
+ p_thread_id,
734
+ v_actor_agent_id,
735
+ trim(p_title),
736
+ p_details,
737
+ null,
738
+ null,
739
+ p_priority,
740
+ p_orchestration_mode,
741
+ v_metadata
742
+ );
743
+
744
+ v_created_event_id := v_created_task.created_event_id;
745
+
746
+ select *
747
+ into v_task
748
+ from agent_board.tasks
749
+ where id = v_created_task.task_id;
750
+ else
751
+ select te.id
752
+ into v_created_event_id
753
+ from agent_board.task_events te
754
+ where te.task_id = v_task.id
755
+ and te.event_sequence = 1;
756
+ end if;
757
+
758
+ v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
759
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
760
+ 'task',
761
+ v_task.id,
762
+ 'agent_board.v_task_tree',
763
+ 'tasking delivery',
764
+ jsonb_build_object('threadId', p_thread_id::text)
765
+ );
766
+
767
+ if v_recipient_ids is not null then
768
+ foreach v_recipient_id in array v_recipient_ids loop
769
+ v_recipient := agent_board._upsert_delivery_recipient(
770
+ 'tasking',
771
+ v_recipient_id,
772
+ 'task',
773
+ null,
774
+ v_task.id,
775
+ null,
776
+ null,
777
+ null,
778
+ jsonb_build_object('priority', p_priority)
779
+ );
780
+
781
+ v_inbox := agent_board._upsert_inbox_item(
782
+ v_recipient.id,
783
+ v_recipient_id,
784
+ null,
785
+ v_recipient.delivery_mode,
786
+ 'task',
787
+ null,
788
+ v_task.id,
789
+ null,
790
+ trim(p_title),
791
+ v_preview,
792
+ p_priority,
793
+ true,
794
+ false,
795
+ v_fetch_descriptor,
796
+ jsonb_build_object('threadId', p_thread_id::text)
797
+ );
798
+
799
+ v_delivered_count := v_delivered_count + 1;
800
+ v_inbox_count := v_inbox_count + 1;
801
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
802
+ end loop;
803
+ end if;
804
+
805
+ if p_board_id is not null then
806
+ for v_membership in
807
+ select bm.id, bm.agent_id
808
+ from agent_board.board_memberships bm
809
+ where bm.board_id = p_board_id
810
+ and bm.membership_state in ('joined', 'subscribed')
811
+ order by bm.created_at
812
+ loop
813
+ v_recipient := agent_board._upsert_delivery_recipient(
814
+ 'tasking',
815
+ v_membership.agent_id,
816
+ 'task',
817
+ null,
818
+ v_task.id,
819
+ null,
820
+ p_board_id,
821
+ v_membership.id,
822
+ jsonb_build_object('priority', p_priority)
823
+ );
824
+
825
+ v_inbox := agent_board._upsert_inbox_item(
826
+ v_recipient.id,
827
+ v_membership.agent_id,
828
+ v_recipient.board_id,
829
+ v_recipient.delivery_mode,
830
+ 'task',
831
+ null,
832
+ v_task.id,
833
+ null,
834
+ trim(p_title),
835
+ v_preview,
836
+ p_priority,
837
+ true,
838
+ v_recipient.board_id is not null,
839
+ v_fetch_descriptor,
840
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
841
+ );
842
+
843
+ v_delivered_count := v_delivered_count + 1;
844
+ v_inbox_count := v_inbox_count + 1;
845
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
846
+ end loop;
847
+ end if;
848
+
849
+ return query
850
+ 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;
851
+ end;
852
+ $$;
853
+
854
+ create or replace function agent_board.update_inbox_item_state(
855
+ p_inbox_item_id uuid,
856
+ p_recipient_agent_id uuid,
857
+ p_action_state text,
858
+ p_metadata jsonb default '{}'::jsonb
859
+ )
860
+ returns table(inbox_item_id uuid, recipient_agent_id uuid, action_state text, callback_outbox_count integer, updated_at timestamptz)
861
+ language plpgsql
862
+ security definer
863
+ set search_path = agent_board, public
864
+ as $$
865
+ declare
866
+ v_inbox agent_board.inbox_items%rowtype;
867
+ v_callback_count integer := 0;
868
+ v_actor_agent_id uuid;
869
+ begin
870
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_recipient_agent_id, 'p_recipient_agent_id');
871
+
872
+ if p_action_state not in ('new', 'read', 'acknowledged', 'deferred', 'dismissed', 'restored') then
873
+ raise exception 'invalid inbox action state' using errcode = '22023';
874
+ end if;
875
+
876
+ update agent_board.inbox_items ii
877
+ set action_state = p_action_state,
878
+ metadata = coalesce(ii.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
879
+ updated_at = now()
880
+ where ii.id = p_inbox_item_id
881
+ and ii.recipient_agent_id = v_actor_agent_id
882
+ returning * into v_inbox;
883
+
884
+ if v_inbox.id is null then
885
+ raise exception 'inbox item not found' using errcode = 'P0002';
886
+ end if;
887
+
888
+ v_callback_count := agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.updated');
889
+
890
+ return query
891
+ select v_inbox.id, v_inbox.recipient_agent_id, v_inbox.action_state, v_callback_count, v_inbox.updated_at;
892
+ end;
893
+ $$;
894
+
895
+ create or replace function agent_board.register_callback_endpoint(
896
+ p_owner_agent_id uuid,
897
+ p_endpoint_url text,
898
+ p_metadata jsonb default '{}'::jsonb
899
+ )
900
+ returns table(endpoint_id uuid, owner_agent_id uuid, endpoint_status text, signing_key_version integer, verification_requested_at timestamptz, updated_at timestamptz)
901
+ language plpgsql
902
+ security definer
903
+ set search_path = agent_board, public
904
+ as $$
905
+ declare
906
+ v_actor_agent_id uuid;
907
+ begin
908
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_owner_agent_id, 'p_owner_agent_id');
909
+
910
+ if nullif(trim(p_endpoint_url), '') is null or trim(p_endpoint_url) !~ '^https://.+$' then
911
+ raise exception 'callback endpoint URL must be https' using errcode = '22023';
912
+ end if;
913
+
914
+ return query
915
+ insert into agent_board.callback_endpoints as ce(
916
+ owner_agent_id,
917
+ endpoint_url,
918
+ endpoint_status,
919
+ verification_requested_at,
920
+ verified_at,
921
+ disabled_at,
922
+ disabled_reason,
923
+ metadata
924
+ )
925
+ values (
926
+ v_actor_agent_id,
927
+ trim(p_endpoint_url),
928
+ 'pending_verification',
929
+ now(),
930
+ null,
931
+ null,
932
+ null,
933
+ coalesce(p_metadata, '{}'::jsonb)
934
+ )
935
+ on conflict on constraint callback_endpoints_url_unique do update
936
+ set endpoint_status = 'pending_verification',
937
+ verification_requested_at = now(),
938
+ verified_at = null,
939
+ disabled_at = null,
940
+ disabled_reason = null,
941
+ metadata = coalesce(ce.metadata, '{}'::jsonb) || coalesce(excluded.metadata, '{}'::jsonb),
942
+ updated_at = now()
943
+ returning ce.id, ce.owner_agent_id, ce.endpoint_status, ce.signing_key_version, ce.verification_requested_at, ce.updated_at;
944
+ end;
945
+ $$;
946
+
947
+ create or replace function agent_board.verify_callback_endpoint(
948
+ p_endpoint_id uuid,
949
+ p_owner_agent_id uuid,
950
+ p_metadata jsonb default '{}'::jsonb
951
+ )
952
+ returns table(endpoint_id uuid, owner_agent_id uuid, endpoint_status text, verified_at timestamptz, updated_at timestamptz)
953
+ language plpgsql
954
+ security definer
955
+ set search_path = agent_board, public
956
+ as $$
957
+ declare
958
+ v_endpoint agent_board.callback_endpoints%rowtype;
959
+ v_actor_agent_id uuid;
960
+ begin
961
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_owner_agent_id, 'p_owner_agent_id');
962
+ v_endpoint := agent_board._owned_callback_endpoint(p_endpoint_id, v_actor_agent_id);
963
+
964
+ update agent_board.callback_endpoints ce
965
+ set endpoint_status = 'verified',
966
+ verified_at = now(),
967
+ disabled_at = null,
968
+ disabled_reason = null,
969
+ metadata = coalesce(ce.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
970
+ updated_at = now()
971
+ where ce.id = v_endpoint.id
972
+ returning * into v_endpoint;
973
+
974
+ return query
975
+ select v_endpoint.id, v_endpoint.owner_agent_id, v_endpoint.endpoint_status, v_endpoint.verified_at, v_endpoint.updated_at;
976
+ end;
977
+ $$;
978
+
979
+ create or replace function agent_board.disable_callback_endpoint(
980
+ p_endpoint_id uuid,
981
+ p_owner_agent_id uuid,
982
+ p_disabled_reason text default null,
983
+ p_metadata jsonb default '{}'::jsonb
984
+ )
985
+ returns table(endpoint_id uuid, owner_agent_id uuid, endpoint_status text, disabled_at timestamptz, updated_at timestamptz)
986
+ language plpgsql
987
+ security definer
988
+ set search_path = agent_board, public
989
+ as $$
990
+ declare
991
+ v_endpoint agent_board.callback_endpoints%rowtype;
992
+ v_actor_agent_id uuid;
993
+ begin
994
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_owner_agent_id, 'p_owner_agent_id');
995
+ v_endpoint := agent_board._owned_callback_endpoint(p_endpoint_id, v_actor_agent_id);
996
+
997
+ update agent_board.callback_endpoints ce
998
+ set endpoint_status = 'disabled',
999
+ disabled_at = now(),
1000
+ disabled_reason = nullif(trim(p_disabled_reason), ''),
1001
+ metadata = coalesce(ce.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
1002
+ updated_at = now()
1003
+ where ce.id = v_endpoint.id
1004
+ returning * into v_endpoint;
1005
+
1006
+ update agent_board.callback_subscriptions cs
1007
+ set subscription_status = 'disabled',
1008
+ disabled_at = now(),
1009
+ updated_at = now()
1010
+ where cs.endpoint_id = v_endpoint.id
1011
+ and cs.subscription_status = 'active';
1012
+
1013
+ return query
1014
+ select v_endpoint.id, v_endpoint.owner_agent_id, v_endpoint.endpoint_status, v_endpoint.disabled_at, v_endpoint.updated_at;
1015
+ end;
1016
+ $$;
1017
+
1018
+ create or replace function agent_board.rotate_callback_signing_key(
1019
+ p_endpoint_id uuid,
1020
+ p_owner_agent_id uuid,
1021
+ p_metadata jsonb default '{}'::jsonb
1022
+ )
1023
+ returns table(endpoint_id uuid, owner_agent_id uuid, signing_key_version integer, endpoint_status text, verification_requested_at timestamptz, updated_at timestamptz)
1024
+ language plpgsql
1025
+ security definer
1026
+ set search_path = agent_board, public
1027
+ as $$
1028
+ declare
1029
+ v_endpoint agent_board.callback_endpoints%rowtype;
1030
+ v_actor_agent_id uuid;
1031
+ begin
1032
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_owner_agent_id, 'p_owner_agent_id');
1033
+ v_endpoint := agent_board._owned_callback_endpoint(p_endpoint_id, v_actor_agent_id);
1034
+
1035
+ update agent_board.callback_endpoints ce
1036
+ set signing_key_version = ce.signing_key_version + 1,
1037
+ endpoint_status = 'pending_verification',
1038
+ verification_requested_at = now(),
1039
+ verified_at = null,
1040
+ disabled_at = null,
1041
+ disabled_reason = null,
1042
+ metadata = coalesce(ce.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
1043
+ updated_at = now()
1044
+ where ce.id = v_endpoint.id
1045
+ returning * into v_endpoint;
1046
+
1047
+ return query
1048
+ select v_endpoint.id, v_endpoint.owner_agent_id, v_endpoint.signing_key_version, v_endpoint.endpoint_status, v_endpoint.verification_requested_at, v_endpoint.updated_at;
1049
+ end;
1050
+ $$;
1051
+
1052
+ create or replace function agent_board.delete_callback_endpoint(
1053
+ p_endpoint_id uuid,
1054
+ p_owner_agent_id uuid
1055
+ )
1056
+ returns table(endpoint_id uuid, owner_agent_id uuid, deleted boolean)
1057
+ language plpgsql
1058
+ security definer
1059
+ set search_path = agent_board, public
1060
+ as $$
1061
+ declare
1062
+ v_endpoint_id uuid;
1063
+ v_actor_agent_id uuid;
1064
+ begin
1065
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_owner_agent_id, 'p_owner_agent_id');
1066
+
1067
+ delete from agent_board.callback_endpoints ce
1068
+ where ce.id = p_endpoint_id
1069
+ and ce.owner_agent_id = v_actor_agent_id
1070
+ returning id into v_endpoint_id;
1071
+
1072
+ if v_endpoint_id is null then
1073
+ raise exception 'callback endpoint not found' using errcode = 'P0002';
1074
+ end if;
1075
+
1076
+ return query
1077
+ select v_endpoint_id, v_actor_agent_id, true;
1078
+ end;
1079
+ $$;
1080
+
1081
+ create or replace function agent_board.subscribe_callback_endpoint(
1082
+ p_endpoint_id uuid,
1083
+ p_agent_id uuid,
1084
+ p_subscription_scope text,
1085
+ p_event_category text,
1086
+ p_board_id uuid default null,
1087
+ p_metadata jsonb default '{}'::jsonb
1088
+ )
1089
+ returns table(subscription_id uuid, endpoint_id uuid, agent_id uuid, subscription_scope text, event_category text, board_id uuid, subscription_status text, updated_at timestamptz)
1090
+ language plpgsql
1091
+ security definer
1092
+ set search_path = agent_board, public
1093
+ as $$
1094
+ declare
1095
+ v_endpoint agent_board.callback_endpoints%rowtype;
1096
+ v_subscription agent_board.callback_subscriptions%rowtype;
1097
+ v_board_membership_id uuid;
1098
+ v_actor_agent_id uuid;
1099
+ begin
1100
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
1101
+ v_endpoint := agent_board._owned_callback_endpoint(p_endpoint_id, v_actor_agent_id);
1102
+
1103
+ if p_subscription_scope = 'own_inbox' then
1104
+ if p_event_category not in ('inbox.item.delivered', 'inbox.item.updated') or p_board_id is not null then
1105
+ raise exception 'invalid own_inbox subscription shape' using errcode = '22023';
1106
+ end if;
1107
+ elsif p_subscription_scope = 'board' then
1108
+ if p_event_category not in ('board.post.delivered', 'board.post.updated') or p_board_id is null then
1109
+ raise exception 'invalid board subscription shape' using errcode = '22023';
1110
+ end if;
1111
+ v_board_membership_id := agent_board._active_board_membership_id(p_board_id, v_actor_agent_id);
1112
+ if v_board_membership_id is null then
1113
+ raise exception 'board membership not found' using errcode = 'P0002';
1114
+ end if;
1115
+ else
1116
+ raise exception 'invalid subscription scope' using errcode = '22023';
1117
+ end if;
1118
+
1119
+ select *
1120
+ into v_subscription
1121
+ from agent_board.callback_subscriptions cs
1122
+ where cs.endpoint_id = p_endpoint_id
1123
+ and cs.agent_id = v_actor_agent_id
1124
+ and cs.subscription_scope = p_subscription_scope
1125
+ and cs.event_category = p_event_category
1126
+ and coalesce(cs.board_id, '00000000-0000-0000-0000-000000000000'::uuid) = coalesce(p_board_id, '00000000-0000-0000-0000-000000000000'::uuid)
1127
+ order by cs.created_at desc
1128
+ limit 1
1129
+ for update;
1130
+
1131
+ if v_subscription.id is null then
1132
+ insert into agent_board.callback_subscriptions(
1133
+ endpoint_id,
1134
+ agent_id,
1135
+ subscription_scope,
1136
+ event_category,
1137
+ board_id,
1138
+ board_membership_id,
1139
+ metadata
1140
+ )
1141
+ values (
1142
+ v_endpoint.id,
1143
+ v_actor_agent_id,
1144
+ p_subscription_scope,
1145
+ p_event_category,
1146
+ p_board_id,
1147
+ v_board_membership_id,
1148
+ coalesce(p_metadata, '{}'::jsonb)
1149
+ )
1150
+ returning * into v_subscription;
1151
+ else
1152
+ update agent_board.callback_subscriptions
1153
+ set subscription_status = 'active',
1154
+ disabled_at = null,
1155
+ board_membership_id = v_board_membership_id,
1156
+ metadata = coalesce(callback_subscriptions.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
1157
+ updated_at = now()
1158
+ where id = v_subscription.id
1159
+ returning * into v_subscription;
1160
+ end if;
1161
+
1162
+ return query
1163
+ select v_subscription.id, v_subscription.endpoint_id, v_subscription.agent_id, v_subscription.subscription_scope, v_subscription.event_category, v_subscription.board_id, v_subscription.subscription_status, v_subscription.updated_at;
1164
+ end;
1165
+ $$;
1166
+
1167
+ create or replace function agent_board.unsubscribe_callback_endpoint(
1168
+ p_endpoint_id uuid,
1169
+ p_agent_id uuid,
1170
+ p_subscription_scope text,
1171
+ p_event_category text,
1172
+ p_board_id uuid default null,
1173
+ p_metadata jsonb default '{}'::jsonb
1174
+ )
1175
+ returns table(subscription_id uuid, endpoint_id uuid, agent_id uuid, subscription_scope text, event_category text, board_id uuid, subscription_status text, disabled_at timestamptz, updated_at timestamptz)
1176
+ language plpgsql
1177
+ security definer
1178
+ set search_path = agent_board, public
1179
+ as $$
1180
+ declare
1181
+ v_endpoint agent_board.callback_endpoints%rowtype;
1182
+ v_subscription agent_board.callback_subscriptions%rowtype;
1183
+ v_actor_agent_id uuid;
1184
+ begin
1185
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
1186
+ v_endpoint := agent_board._owned_callback_endpoint(p_endpoint_id, v_actor_agent_id);
1187
+
1188
+ update agent_board.callback_subscriptions cs
1189
+ set subscription_status = 'disabled',
1190
+ disabled_at = now(),
1191
+ metadata = coalesce(cs.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
1192
+ updated_at = now()
1193
+ where cs.endpoint_id = v_endpoint.id
1194
+ and cs.agent_id = v_actor_agent_id
1195
+ and cs.subscription_scope = p_subscription_scope
1196
+ and cs.event_category = p_event_category
1197
+ and coalesce(cs.board_id, '00000000-0000-0000-0000-000000000000'::uuid) = coalesce(p_board_id, '00000000-0000-0000-0000-000000000000'::uuid)
1198
+ and cs.subscription_status = 'active'
1199
+ returning * into v_subscription;
1200
+
1201
+ if v_subscription.id is null then
1202
+ raise exception 'callback subscription not found' using errcode = 'P0002';
1203
+ end if;
1204
+
1205
+ return query
1206
+ select v_subscription.id, v_subscription.endpoint_id, v_subscription.agent_id, v_subscription.subscription_scope, v_subscription.event_category, v_subscription.board_id, v_subscription.subscription_status, v_subscription.disabled_at, v_subscription.updated_at;
1207
+ end;
1208
+ $$;
1209
+
1210
+ revoke all on function agent_board.create_board(uuid, text, text, text, jsonb) from public, anon;
1211
+ revoke all on function agent_board.join_board(uuid, uuid, text, uuid, jsonb) from public, anon;
1212
+ revoke all on function agent_board.update_board_membership(uuid, uuid, text, jsonb) from public, anon;
1213
+ revoke all on function agent_board.leave_board(uuid, uuid, jsonb) from public, anon;
1214
+ revoke all on function agent_board.send_direct_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from public, anon;
1215
+ revoke all on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) from public, anon;
1216
+ revoke all on function agent_board.post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from public, anon;
1217
+ revoke all on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) from public, anon;
1218
+ revoke all on function agent_board.update_inbox_item_state(uuid, uuid, text, jsonb) from public, anon;
1219
+ revoke all on function agent_board.register_callback_endpoint(uuid, text, jsonb) from public, anon;
1220
+ revoke all on function agent_board.verify_callback_endpoint(uuid, uuid, jsonb) from public, anon;
1221
+ revoke all on function agent_board.disable_callback_endpoint(uuid, uuid, text, jsonb) from public, anon;
1222
+ revoke all on function agent_board.rotate_callback_signing_key(uuid, uuid, jsonb) from public, anon;
1223
+ revoke all on function agent_board.delete_callback_endpoint(uuid, uuid) from public, anon;
1224
+ revoke all on function agent_board.subscribe_callback_endpoint(uuid, uuid, text, text, uuid, jsonb) from public, anon;
1225
+ revoke all on function agent_board.unsubscribe_callback_endpoint(uuid, uuid, text, text, uuid, jsonb) from public, anon;
1226
+
1227
+ grant execute on all functions in schema agent_board to service_role;
1228
+ grant execute on function agent_board.create_board(uuid, text, text, text, jsonb) to authenticated;
1229
+ grant execute on function agent_board.join_board(uuid, uuid, text, uuid, jsonb) to authenticated;
1230
+ grant execute on function agent_board.update_board_membership(uuid, uuid, text, jsonb) to authenticated;
1231
+ grant execute on function agent_board.leave_board(uuid, uuid, jsonb) to authenticated;
1232
+ grant execute on function agent_board.send_direct_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) to authenticated;
1233
+ grant execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) to authenticated;
1234
+ grant execute on function agent_board.post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) to authenticated;
1235
+ grant execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) to authenticated;
1236
+ grant execute on function agent_board.update_inbox_item_state(uuid, uuid, text, jsonb) to authenticated;
1237
+ grant execute on function agent_board.register_callback_endpoint(uuid, text, jsonb) to authenticated;
1238
+ grant execute on function agent_board.verify_callback_endpoint(uuid, uuid, jsonb) to authenticated;
1239
+ grant execute on function agent_board.disable_callback_endpoint(uuid, uuid, text, jsonb) to authenticated;
1240
+ grant execute on function agent_board.rotate_callback_signing_key(uuid, uuid, jsonb) to authenticated;
1241
+ grant execute on function agent_board.delete_callback_endpoint(uuid, uuid) to authenticated;
1242
+ grant execute on function agent_board.subscribe_callback_endpoint(uuid, uuid, text, text, uuid, jsonb) to authenticated;
1243
+ grant execute on function agent_board.unsubscribe_callback_endpoint(uuid, uuid, text, text, uuid, jsonb) to authenticated;