@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,413 @@
1
+ create or replace function agent_board._next_task_event_sequence(p_task_id uuid)
2
+ returns bigint
3
+ language plpgsql
4
+ security definer
5
+ set search_path = agent_board, public
6
+ as $$
7
+ declare
8
+ v_sequence bigint;
9
+ begin
10
+ perform 1
11
+ from agent_board.tasks t
12
+ where t.id = p_task_id
13
+ for update;
14
+
15
+ select coalesce(max(te.event_sequence), 0) + 1
16
+ into v_sequence
17
+ from agent_board.task_events te
18
+ where te.task_id = p_task_id;
19
+
20
+ return v_sequence;
21
+ end;
22
+ $$;
23
+
24
+ create or replace function agent_board.heartbeat(
25
+ p_agent_id uuid,
26
+ p_thread_id uuid default null,
27
+ p_availability text default 'ready',
28
+ p_activity_summary text default null,
29
+ p_expires_in interval default interval '5 minutes',
30
+ p_metadata jsonb default '{}'::jsonb
31
+ )
32
+ returns table(agent_id uuid, availability text, thread_id uuid, last_heartbeat_at timestamptz, expires_at timestamptz, updated_at timestamptz)
33
+ language plpgsql
34
+ security definer
35
+ set search_path = agent_board, public
36
+ as $$
37
+ begin
38
+ if not exists (select 1 from agent_board.agents where id = p_agent_id) then
39
+ raise exception 'agent not found' using errcode = 'P0002';
40
+ end if;
41
+ if p_thread_id is not null and not exists (select 1 from agent_board.threads where id = p_thread_id) then
42
+ raise exception 'thread not found' using errcode = 'P0002';
43
+ end if;
44
+
45
+ return query
46
+ insert into agent_board.presence as p(agent_id, thread_id, availability, activity_summary, last_heartbeat_at, expires_at, metadata, updated_at)
47
+ values (p_agent_id, p_thread_id, coalesce(p_availability, 'ready'), p_activity_summary, now(), now() + coalesce(p_expires_in, interval '5 minutes'), coalesce(p_metadata, '{}'::jsonb), now())
48
+ on conflict on constraint presence_pkey do update
49
+ set thread_id = excluded.thread_id,
50
+ availability = excluded.availability,
51
+ activity_summary = excluded.activity_summary,
52
+ last_heartbeat_at = excluded.last_heartbeat_at,
53
+ expires_at = excluded.expires_at,
54
+ metadata = excluded.metadata,
55
+ updated_at = excluded.updated_at
56
+ returning p.agent_id, p.availability, p.thread_id, p.last_heartbeat_at, p.expires_at, p.updated_at;
57
+ end;
58
+ $$;
59
+
60
+ create or replace function agent_board.mark_read(p_thread_id uuid, p_agent_id uuid, p_last_read_message_sequence bigint)
61
+ returns table(thread_id uuid, agent_id uuid, last_read_message_sequence bigint, last_read_at timestamptz)
62
+ language plpgsql
63
+ security definer
64
+ set search_path = agent_board, public
65
+ as $$
66
+ declare
67
+ v_sequence bigint;
68
+ begin
69
+ if p_last_read_message_sequence < 0 then
70
+ raise exception 'negative read sequence' using errcode = '22023';
71
+ end if;
72
+
73
+ select least(p_last_read_message_sequence, t.last_message_sequence)
74
+ into v_sequence
75
+ from agent_board.threads t
76
+ where t.id = p_thread_id;
77
+
78
+ if v_sequence is null then
79
+ raise exception 'thread not found' using errcode = 'P0002';
80
+ end if;
81
+
82
+ return query
83
+ insert into agent_board.read_receipts as rr(thread_id, agent_id, last_read_message_sequence, last_read_at)
84
+ values (p_thread_id, p_agent_id, v_sequence, now())
85
+ on conflict on constraint read_receipts_pkey do update
86
+ set last_read_message_sequence = greatest(rr.last_read_message_sequence, excluded.last_read_message_sequence),
87
+ last_read_at = now()
88
+ returning rr.thread_id, rr.agent_id, rr.last_read_message_sequence, rr.last_read_at;
89
+ end;
90
+ $$;
91
+
92
+ create or replace function agent_board.resolve_conflict(
93
+ p_conflict_id uuid,
94
+ p_resolved_by_agent_id uuid,
95
+ p_details jsonb default '{}'::jsonb
96
+ )
97
+ returns table(conflict_id uuid, status text, resolved_at timestamptz, updated_at timestamptz, task_event_sequence bigint)
98
+ language plpgsql
99
+ security definer
100
+ set search_path = agent_board, public
101
+ as $$
102
+ declare
103
+ v_conflict agent_board.conflicts%rowtype;
104
+ v_event record;
105
+ begin
106
+ select *
107
+ into v_conflict
108
+ from agent_board.conflicts c
109
+ where c.id = p_conflict_id
110
+ and c.status <> 'resolved';
111
+ if v_conflict.id is null then
112
+ raise exception 'conflict not found' using errcode = 'P0002';
113
+ end if;
114
+
115
+ update agent_board.conflicts c
116
+ set status = 'resolved',
117
+ resolved_by_agent_id = p_resolved_by_agent_id,
118
+ resolved_at = now(),
119
+ details = c.details || coalesce(p_details, '{}'::jsonb),
120
+ updated_at = now()
121
+ where c.id = p_conflict_id
122
+ returning * into v_conflict;
123
+
124
+ if v_conflict.task_id is not null then
125
+ select * into v_event
126
+ from agent_board.append_task_event(v_conflict.task_id, p_resolved_by_agent_id, 'status_changed', jsonb_build_object('kind', 'conflict_resolved', 'conflict_id', p_conflict_id));
127
+ end if;
128
+
129
+ return query
130
+ select v_conflict.id, v_conflict.status, v_conflict.resolved_at, v_conflict.updated_at, v_event.event_sequence;
131
+ end;
132
+ $$;
133
+
134
+ create or replace function agent_board.resolve_escalation(
135
+ p_escalation_id uuid,
136
+ p_resolved_by_agent_id uuid,
137
+ p_details jsonb default '{}'::jsonb
138
+ )
139
+ returns table(escalation_id uuid, status text, resolved_at timestamptz, updated_at timestamptz, task_event_sequence bigint)
140
+ language plpgsql
141
+ security definer
142
+ set search_path = agent_board, public
143
+ as $$
144
+ declare
145
+ v_escalation agent_board.escalations%rowtype;
146
+ v_event record;
147
+ begin
148
+ select *
149
+ into v_escalation
150
+ from agent_board.escalations e
151
+ where e.id = p_escalation_id
152
+ and e.status <> 'resolved';
153
+ if v_escalation.id is null then
154
+ raise exception 'escalation not found' using errcode = 'P0002';
155
+ end if;
156
+
157
+ update agent_board.escalations e
158
+ set status = 'resolved',
159
+ resolved_by_agent_id = p_resolved_by_agent_id,
160
+ resolved_at = now(),
161
+ details = e.details || coalesce(p_details, '{}'::jsonb),
162
+ updated_at = now()
163
+ where e.id = p_escalation_id
164
+ returning * into v_escalation;
165
+
166
+ if v_escalation.task_id is not null then
167
+ select * into v_event
168
+ from agent_board.append_task_event(v_escalation.task_id, p_resolved_by_agent_id, 'status_changed', jsonb_build_object('kind', 'escalation_resolved', 'escalation_id', p_escalation_id));
169
+ end if;
170
+
171
+ return query
172
+ select v_escalation.id, v_escalation.status, v_escalation.resolved_at, v_escalation.updated_at, v_event.event_sequence;
173
+ end;
174
+ $$;
175
+
176
+ create or replace function agent_board.create_tasking_delivery(
177
+ p_thread_id uuid,
178
+ p_created_by_agent_id uuid,
179
+ p_title text,
180
+ p_details text default null,
181
+ p_recipient_agent_ids uuid[] default null,
182
+ p_board_id uuid default null,
183
+ p_priority text default 'medium',
184
+ p_orchestration_mode text default 'distributed',
185
+ p_idempotency_key text default null,
186
+ p_metadata jsonb default '{}'::jsonb
187
+ )
188
+ 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)
189
+ language plpgsql
190
+ security definer
191
+ set search_path = agent_board, public
192
+ as $$
193
+ declare
194
+ v_task agent_board.tasks%rowtype;
195
+ v_created_event_id uuid;
196
+ v_created_task record;
197
+ v_recipient_ids uuid[];
198
+ v_recipient_id uuid;
199
+ v_membership record;
200
+ v_recipient agent_board.delivery_recipients%rowtype;
201
+ v_inbox agent_board.inbox_items%rowtype;
202
+ v_delivered_count integer := 0;
203
+ v_inbox_count integer := 0;
204
+ v_callback_count integer := 0;
205
+ v_metadata jsonb;
206
+ v_preview text;
207
+ v_fetch_descriptor jsonb;
208
+ v_idempotency_key text := nullif(trim(p_idempotency_key), '');
209
+ begin
210
+ if nullif(trim(p_title), '') is null then
211
+ raise exception 'task title is required' using errcode = '22023';
212
+ end if;
213
+ if p_priority not in ('low', 'medium', 'high', 'urgent') then
214
+ raise exception 'invalid priority' using errcode = '22023';
215
+ end if;
216
+ if p_orchestration_mode not in ('distributed', 'hierarchical', 'hybrid') then
217
+ raise exception 'invalid orchestration mode' using errcode = '22023';
218
+ end if;
219
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
220
+ raise exception 'thread not found' using errcode = 'P0002';
221
+ end if;
222
+ if not exists (select 1 from agent_board.agents where id = p_created_by_agent_id) then
223
+ raise exception 'creator agent not found' using errcode = 'P0002';
224
+ end if;
225
+ if (p_recipient_agent_ids is null or cardinality(p_recipient_agent_ids) = 0) and p_board_id is null then
226
+ raise exception 'tasking delivery requires explicit recipients or a board target' using errcode = '22023';
227
+ end if;
228
+ if p_recipient_agent_ids is not null and array_position(p_recipient_agent_ids, null) is not null then
229
+ raise exception 'recipient_agent_ids must not contain nulls' using errcode = '22023';
230
+ end if;
231
+ if p_board_id is not null and not exists (select 1 from agent_board.boards where id = p_board_id) then
232
+ raise exception 'board not found' using errcode = 'P0002';
233
+ end if;
234
+
235
+ select array_agg(distinct recipient_id)
236
+ into v_recipient_ids
237
+ from unnest(coalesce(p_recipient_agent_ids, '{}'::uuid[])) as recipient_id;
238
+
239
+ if v_recipient_ids is not null and (
240
+ select count(*)
241
+ from agent_board.agents
242
+ where id = any(v_recipient_ids)
243
+ ) <> cardinality(v_recipient_ids) then
244
+ raise exception 'recipient agent not found' using errcode = 'P0002';
245
+ end if;
246
+
247
+ perform agent_board._lock_idempotency_key(
248
+ 'create_tasking_delivery:' || p_thread_id::text || ':' || p_created_by_agent_id::text,
249
+ v_idempotency_key
250
+ );
251
+
252
+ if v_idempotency_key is not null then
253
+ select *
254
+ into v_task
255
+ from agent_board.tasks
256
+ where thread_id = p_thread_id
257
+ and created_by_agent_id = p_created_by_agent_id
258
+ and metadata ->> 'rpcName' = 'create_tasking_delivery'
259
+ and metadata ->> 'idempotencyKey' = v_idempotency_key
260
+ order by created_at desc
261
+ limit 1;
262
+ end if;
263
+
264
+ if v_task.id is null then
265
+ v_metadata := coalesce(p_metadata, '{}'::jsonb)
266
+ || jsonb_build_object(
267
+ 'rpcName', 'create_tasking_delivery',
268
+ 'priority', p_priority
269
+ );
270
+ if p_board_id is not null then
271
+ v_metadata := v_metadata || jsonb_build_object('boardId', p_board_id::text);
272
+ end if;
273
+ if v_idempotency_key is not null then
274
+ v_metadata := v_metadata || jsonb_build_object('idempotencyKey', v_idempotency_key);
275
+ end if;
276
+
277
+ select *
278
+ into v_created_task
279
+ from agent_board.create_task(
280
+ p_thread_id,
281
+ p_created_by_agent_id,
282
+ trim(p_title),
283
+ p_details,
284
+ null,
285
+ null,
286
+ p_priority,
287
+ p_orchestration_mode,
288
+ v_metadata
289
+ );
290
+
291
+ v_created_event_id := v_created_task.created_event_id;
292
+
293
+ select *
294
+ into v_task
295
+ from agent_board.tasks
296
+ where id = v_created_task.task_id;
297
+ else
298
+ select te.id
299
+ into v_created_event_id
300
+ from agent_board.task_events te
301
+ where te.task_id = v_task.id
302
+ and te.event_sequence = 1;
303
+ end if;
304
+
305
+ v_preview := agent_board._trim_inbox_text(coalesce(p_details, p_title), 280);
306
+ v_fetch_descriptor := agent_board._build_fetch_descriptor(
307
+ 'task',
308
+ v_task.id,
309
+ 'agent_board.v_task_tree',
310
+ 'tasking delivery',
311
+ jsonb_build_object('threadId', p_thread_id::text)
312
+ );
313
+
314
+ if v_recipient_ids is not null then
315
+ foreach v_recipient_id in array v_recipient_ids loop
316
+ v_recipient := agent_board._upsert_delivery_recipient(
317
+ 'tasking',
318
+ v_recipient_id,
319
+ 'task',
320
+ null,
321
+ v_task.id,
322
+ null,
323
+ null,
324
+ null,
325
+ jsonb_build_object('priority', p_priority)
326
+ );
327
+
328
+ v_inbox := agent_board._upsert_inbox_item(
329
+ v_recipient.id,
330
+ v_recipient_id,
331
+ null,
332
+ v_recipient.delivery_mode,
333
+ 'task',
334
+ null,
335
+ v_task.id,
336
+ null,
337
+ trim(p_title),
338
+ v_preview,
339
+ p_priority,
340
+ true,
341
+ false,
342
+ v_fetch_descriptor,
343
+ jsonb_build_object('threadId', p_thread_id::text)
344
+ );
345
+
346
+ v_delivered_count := v_delivered_count + 1;
347
+ v_inbox_count := v_inbox_count + 1;
348
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
349
+ end loop;
350
+ end if;
351
+
352
+ if p_board_id is not null then
353
+ for v_membership in
354
+ select bm.id, bm.agent_id
355
+ from agent_board.board_memberships bm
356
+ where bm.board_id = p_board_id
357
+ and bm.membership_state in ('joined', 'subscribed')
358
+ order by bm.created_at
359
+ loop
360
+ v_recipient := agent_board._upsert_delivery_recipient(
361
+ 'tasking',
362
+ v_membership.agent_id,
363
+ 'task',
364
+ null,
365
+ v_task.id,
366
+ null,
367
+ p_board_id,
368
+ v_membership.id,
369
+ jsonb_build_object('priority', p_priority)
370
+ );
371
+
372
+ v_inbox := agent_board._upsert_inbox_item(
373
+ v_recipient.id,
374
+ v_membership.agent_id,
375
+ v_recipient.board_id,
376
+ v_recipient.delivery_mode,
377
+ 'task',
378
+ null,
379
+ v_task.id,
380
+ null,
381
+ trim(p_title),
382
+ v_preview,
383
+ p_priority,
384
+ true,
385
+ v_recipient.board_id is not null,
386
+ v_fetch_descriptor,
387
+ jsonb_build_object('threadId', p_thread_id::text, 'boardId', p_board_id::text)
388
+ );
389
+
390
+ v_delivered_count := v_delivered_count + 1;
391
+ v_inbox_count := v_inbox_count + 1;
392
+ v_callback_count := v_callback_count + agent_board._queue_inbox_callbacks(v_inbox.id, 'inbox.item.delivered');
393
+ end loop;
394
+ end if;
395
+
396
+ return query
397
+ 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;
398
+ end;
399
+ $$;
400
+
401
+ revoke all on function agent_board._next_task_event_sequence(uuid) from public, anon, authenticated;
402
+ revoke all on function agent_board.heartbeat(uuid, uuid, text, text, interval, jsonb) from public, anon, authenticated;
403
+ revoke all on function agent_board.mark_read(uuid, uuid, bigint) from public, anon, authenticated;
404
+ revoke all on function agent_board.resolve_conflict(uuid, uuid, jsonb) from public, anon, authenticated;
405
+ revoke all on function agent_board.resolve_escalation(uuid, uuid, jsonb) from public, anon, authenticated;
406
+ revoke all on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) from public, anon, authenticated;
407
+
408
+ grant execute on function agent_board._next_task_event_sequence(uuid) to service_role;
409
+ grant execute on function agent_board.heartbeat(uuid, uuid, text, text, interval, jsonb) to service_role;
410
+ grant execute on function agent_board.mark_read(uuid, uuid, bigint) to service_role;
411
+ grant execute on function agent_board.resolve_conflict(uuid, uuid, jsonb) to service_role;
412
+ grant execute on function agent_board.resolve_escalation(uuid, uuid, jsonb) to service_role;
413
+ grant execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) to service_role;
@@ -0,0 +1,146 @@
1
+ do $$
2
+ begin
3
+ if not exists (
4
+ select 1
5
+ from pg_type t
6
+ join pg_namespace n on n.oid = t.typnamespace
7
+ where n.nspname = 'agent_board'
8
+ and t.typname = 'agent_kind'
9
+ ) then
10
+ create type agent_board.agent_kind as enum ('human', 'agent', 'system');
11
+ end if;
12
+ end
13
+ $$;
14
+
15
+ alter table agent_board.agents
16
+ add column if not exists auth_user_id uuid references auth.users(id) on delete set null,
17
+ add column if not exists owner_user_id uuid references auth.users(id) on delete set null,
18
+ add column if not exists kind agent_board.agent_kind;
19
+
20
+ update agent_board.agents
21
+ set kind = agent_type::agent_board.agent_kind
22
+ where kind is null;
23
+
24
+ create or replace function agent_board.sync_agent_principal_columns()
25
+ returns trigger
26
+ language plpgsql
27
+ set search_path = ''
28
+ as $$
29
+ begin
30
+ if new.kind is null
31
+ or (
32
+ tg_op = 'UPDATE'
33
+ and new.kind is not distinct from old.kind
34
+ and new.agent_type is distinct from old.agent_type
35
+ and new.agent_type is not null
36
+ ) then
37
+ new.kind := new.agent_type::agent_board.agent_kind;
38
+ end if;
39
+
40
+ if new.agent_type is null
41
+ or (
42
+ tg_op = 'UPDATE'
43
+ and new.agent_type is not distinct from old.agent_type
44
+ and new.kind is distinct from old.kind
45
+ and new.kind is not null
46
+ ) then
47
+ new.agent_type := new.kind::text;
48
+ end if;
49
+
50
+ if new.kind is null or new.agent_type is null then
51
+ raise exception 'kind and agent_type are both required' using errcode = '23502';
52
+ end if;
53
+
54
+ if new.kind::text <> new.agent_type then
55
+ raise exception 'kind and agent_type must match' using errcode = '22000';
56
+ end if;
57
+
58
+ if new.kind = 'system'::agent_board.agent_kind then
59
+ new.owner_user_id := null;
60
+ new.auth_user_id := null;
61
+ end if;
62
+
63
+ return new;
64
+ end;
65
+ $$;
66
+
67
+ drop trigger if exists trg_sync_agent_principal_columns on agent_board.agents;
68
+
69
+ create trigger trg_sync_agent_principal_columns
70
+ before insert or update on agent_board.agents
71
+ for each row execute function agent_board.sync_agent_principal_columns();
72
+
73
+ do $$
74
+ declare
75
+ v_provisioning_user_id uuid;
76
+ begin
77
+ select uor.user_id
78
+ into v_provisioning_user_id
79
+ from public.user_org_roles uor
80
+ where uor.role = 'platform_admin'
81
+ order by uor.granted_at, uor.user_id
82
+ limit 1;
83
+
84
+ if v_provisioning_user_id is not null then
85
+ update agent_board.agents
86
+ set owner_user_id = coalesce(owner_user_id, case when agent_type = 'system' then null else v_provisioning_user_id end),
87
+ auth_user_id = coalesce(auth_user_id, case when agent_type = 'human' then v_provisioning_user_id else null end),
88
+ kind = coalesce(kind, agent_type::agent_board.agent_kind),
89
+ updated_at = now()
90
+ where agent_type in ('human', 'agent', 'system');
91
+ end if;
92
+
93
+ update agent_board.agents
94
+ set owner_user_id = null,
95
+ auth_user_id = null,
96
+ kind = 'system'::agent_board.agent_kind,
97
+ updated_at = now()
98
+ where agent_type = 'system'
99
+ and (
100
+ owner_user_id is not null
101
+ or auth_user_id is not null
102
+ or kind is distinct from 'system'::agent_board.agent_kind
103
+ );
104
+ end
105
+ $$;
106
+
107
+ alter table agent_board.agents
108
+ drop constraint if exists agents_kind_agent_type_match,
109
+ add constraint agents_kind_agent_type_match check (kind::text = agent_type),
110
+ drop constraint if exists agents_system_owner_shape,
111
+ add constraint agents_system_owner_shape check (
112
+ (kind = 'system'::agent_board.agent_kind and owner_user_id is null and auth_user_id is null)
113
+ or kind <> 'system'::agent_board.agent_kind
114
+ );
115
+
116
+ create index if not exists idx_agent_board_agents_owner_user_id
117
+ on agent_board.agents(owner_user_id)
118
+ where owner_user_id is not null;
119
+
120
+ create index if not exists idx_agent_board_agents_auth_user_id
121
+ on agent_board.agents(auth_user_id)
122
+ where auth_user_id is not null;
123
+
124
+ create unique index if not exists idx_agent_board_agents_auth_user_id_active_unique
125
+ on agent_board.agents(auth_user_id)
126
+ where auth_user_id is not null and status = 'active';
127
+
128
+ create or replace function agent_board.current_agent_id()
129
+ returns uuid
130
+ language sql
131
+ stable
132
+ security definer
133
+ set search_path = ''
134
+ as $$
135
+ select a.id
136
+ from agent_board.agents a
137
+ where a.auth_user_id = auth.uid()
138
+ and a.status = 'active'
139
+ limit 1;
140
+ $$;
141
+
142
+ revoke all on function agent_board.current_agent_id() from public, anon, authenticated;
143
+ grant execute on function agent_board.current_agent_id() to authenticated;
144
+ grant execute on function agent_board.current_agent_id() to service_role;
145
+
146
+ comment on function agent_board.current_agent_id() is 'Resolves auth.uid() to the active agent_board.agents.id or returns null.';
@@ -0,0 +1,39 @@
1
+ create table if not exists agent_board.agent_credentials (
2
+ id uuid primary key default gen_random_uuid(),
3
+ agent_id uuid not null references agent_board.agents(id) on delete cascade,
4
+ refresh_token_hash bytea not null,
5
+ hash_version integer not null check (hash_version > 0),
6
+ credential_label text not null,
7
+ expires_at timestamptz not null,
8
+ last_used_at timestamptz,
9
+ revoked_at timestamptz,
10
+ rotated_at timestamptz,
11
+ replaced_by_credential_id uuid references agent_board.agent_credentials(id) on delete set null,
12
+ replaces_credential_id uuid references agent_board.agent_credentials(id) on delete set null,
13
+ metadata jsonb not null default '{}'::jsonb,
14
+ created_at timestamptz not null default now(),
15
+ updated_at timestamptz not null default now(),
16
+ constraint agent_credentials_not_self_replaced_by check (replaced_by_credential_id is null or replaced_by_credential_id <> id),
17
+ constraint agent_credentials_not_self_replaces check (replaces_credential_id is null or replaces_credential_id <> id),
18
+ constraint agent_credentials_revocation_rotation_shape check (
19
+ revoked_at is null
20
+ or rotated_at is null
21
+ or revoked_at <= rotated_at
22
+ )
23
+ );
24
+
25
+ comment on table agent_board.agent_credentials is 'Durable hashed refresh-token references plus lifecycle metadata for one agent principal.';
26
+
27
+ create index if not exists idx_agent_board_agent_credentials_agent
28
+ on agent_board.agent_credentials(agent_id, created_at desc);
29
+
30
+ create unique index if not exists idx_agent_board_agent_credentials_refresh_token_hash
31
+ on agent_board.agent_credentials(refresh_token_hash);
32
+
33
+ create index if not exists idx_agent_board_agent_credentials_active
34
+ on agent_board.agent_credentials(agent_id, expires_at, created_at desc)
35
+ where revoked_at is null;
36
+
37
+ insert into agent_board.schema_version (version)
38
+ values (2)
39
+ on conflict ((true)) do update set version = excluded.version;