@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,798 @@
1
+ create or replace function agent_board.register_agent(
2
+ p_agent_type text,
3
+ p_handle text,
4
+ p_display_name text,
5
+ p_status text default 'active',
6
+ p_metadata jsonb default '{}'::jsonb
7
+ )
8
+ returns table(agent_id uuid, handle 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
+ v_existing agent_board.agents%rowtype;
16
+ begin
17
+ if nullif(trim(p_handle), '') is null or nullif(trim(p_display_name), '') is null then
18
+ raise exception 'handle and display name are required' using errcode = '22023';
19
+ end if;
20
+
21
+ if agent_board._is_service_role() then
22
+ return query
23
+ insert into agent_board.agents as a(agent_type, handle, display_name, status, metadata)
24
+ values (p_agent_type, trim(p_handle), trim(p_display_name), coalesce(p_status, 'active'), coalesce(p_metadata, '{}'::jsonb))
25
+ on conflict on constraint agents_handle_key do update
26
+ set agent_type = excluded.agent_type,
27
+ display_name = excluded.display_name,
28
+ status = excluded.status,
29
+ metadata = excluded.metadata,
30
+ updated_at = now()
31
+ returning a.id, a.handle, a.status, a.created_at, a.updated_at;
32
+ return;
33
+ end if;
34
+
35
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(null, 'current_agent_id');
36
+
37
+ select *
38
+ into v_existing
39
+ from agent_board.agents
40
+ where id = v_actor_agent_id
41
+ limit 1
42
+ for update;
43
+
44
+ if v_existing.id is null then
45
+ raise exception 'agent not found' using errcode = 'P0002';
46
+ end if;
47
+
48
+ if p_agent_type is distinct from v_existing.agent_type then
49
+ raise exception 'register_agent may not change agent_type after provisioning' using errcode = '42501';
50
+ end if;
51
+
52
+ return query
53
+ update agent_board.agents a
54
+ set handle = trim(p_handle),
55
+ display_name = trim(p_display_name),
56
+ status = coalesce(p_status, a.status),
57
+ metadata = coalesce(a.metadata, '{}'::jsonb) || coalesce(p_metadata, '{}'::jsonb),
58
+ updated_at = now()
59
+ where a.id = v_actor_agent_id
60
+ returning a.id, a.handle, a.status, a.created_at, a.updated_at;
61
+ end;
62
+ $$;
63
+
64
+ create or replace function agent_board.heartbeat(
65
+ p_agent_id uuid,
66
+ p_thread_id uuid default null,
67
+ p_availability text default 'ready',
68
+ p_activity_summary text default null,
69
+ p_expires_in interval default interval '5 minutes',
70
+ p_metadata jsonb default '{}'::jsonb
71
+ )
72
+ returns table(agent_id uuid, availability text, thread_id uuid, last_heartbeat_at timestamptz, expires_at timestamptz, updated_at timestamptz)
73
+ language plpgsql
74
+ security definer
75
+ set search_path = agent_board, public
76
+ as $$
77
+ declare
78
+ v_actor_agent_id uuid;
79
+ begin
80
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
81
+
82
+ if p_thread_id is not null then
83
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
84
+ raise exception 'thread not found' using errcode = 'P0002';
85
+ end if;
86
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
87
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
88
+ end if;
89
+ end if;
90
+
91
+ return query
92
+ insert into agent_board.presence(agent_id, thread_id, availability, activity_summary, last_heartbeat_at, expires_at, metadata, updated_at)
93
+ values (v_actor_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())
94
+ on conflict on constraint presence_pkey do update
95
+ set thread_id = excluded.thread_id,
96
+ availability = excluded.availability,
97
+ activity_summary = excluded.activity_summary,
98
+ last_heartbeat_at = excluded.last_heartbeat_at,
99
+ expires_at = excluded.expires_at,
100
+ metadata = excluded.metadata,
101
+ updated_at = excluded.updated_at
102
+ returning presence.agent_id, presence.availability, presence.thread_id, presence.last_heartbeat_at, presence.expires_at, presence.updated_at;
103
+ end;
104
+ $$;
105
+
106
+ create or replace function agent_board.create_thread(
107
+ p_created_by_agent_id uuid,
108
+ p_subject text,
109
+ p_thread_type text default 'coordination',
110
+ p_metadata jsonb default '{}'::jsonb
111
+ )
112
+ returns table(thread_id uuid, thread_type text, subject text, status text, last_message_sequence bigint, created_at timestamptz, updated_at timestamptz)
113
+ language plpgsql
114
+ security definer
115
+ set search_path = agent_board, public
116
+ as $$
117
+ declare
118
+ v_actor_agent_id uuid;
119
+ begin
120
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
121
+
122
+ if nullif(trim(p_subject), '') is null then
123
+ raise exception 'subject is required' using errcode = '22023';
124
+ end if;
125
+
126
+ return query
127
+ insert into agent_board.threads(created_by_agent_id, subject, thread_type, metadata)
128
+ values (v_actor_agent_id, trim(p_subject), coalesce(p_thread_type, 'coordination'), coalesce(p_metadata, '{}'::jsonb))
129
+ returning id, threads.thread_type, threads.subject, threads.status, threads.last_message_sequence, threads.created_at, threads.updated_at;
130
+ end;
131
+ $$;
132
+
133
+ create or replace function agent_board.post_message(
134
+ p_thread_id uuid,
135
+ p_author_agent_id uuid,
136
+ p_body text,
137
+ p_message_type text default 'comment',
138
+ p_task_id uuid default null,
139
+ p_metadata jsonb default '{}'::jsonb
140
+ )
141
+ returns table(message_id uuid, thread_id uuid, task_id uuid, message_sequence bigint, created_at timestamptz)
142
+ language plpgsql
143
+ security definer
144
+ set search_path = agent_board, public
145
+ as $$
146
+ declare
147
+ v_sequence bigint;
148
+ v_actor_agent_id uuid;
149
+ begin
150
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_author_agent_id, 'p_author_agent_id');
151
+
152
+ if nullif(trim(p_body), '') is null then
153
+ raise exception 'message body is required' using errcode = '22023';
154
+ end if;
155
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
156
+ raise exception 'thread not found' using errcode = 'P0002';
157
+ end if;
158
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
159
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
160
+ end if;
161
+ if p_task_id is not null and not exists (select 1 from agent_board.tasks where id = p_task_id and tasks.thread_id = p_thread_id) then
162
+ raise exception 'task not found in thread' using errcode = '22023';
163
+ end if;
164
+
165
+ v_sequence := agent_board._next_thread_message_sequence(p_thread_id);
166
+
167
+ return query
168
+ insert into agent_board.messages(thread_id, author_agent_id, task_id, message_sequence, message_type, body, metadata)
169
+ values (p_thread_id, v_actor_agent_id, p_task_id, v_sequence, coalesce(p_message_type, 'comment'), p_body, coalesce(p_metadata, '{}'::jsonb))
170
+ returning id, messages.thread_id, messages.task_id, messages.message_sequence, messages.created_at;
171
+ end;
172
+ $$;
173
+
174
+ create or replace function agent_board.create_task(
175
+ p_thread_id uuid,
176
+ p_created_by_agent_id uuid,
177
+ p_title text,
178
+ p_details text default null,
179
+ p_assigned_agent_id uuid default null,
180
+ p_parent_task_id uuid default null,
181
+ p_priority text default 'medium',
182
+ p_orchestration_mode text default 'distributed',
183
+ p_metadata jsonb default '{}'::jsonb
184
+ )
185
+ returns table(task_id uuid, parent_task_id uuid, root_task_id uuid, created_event_id uuid, created_event_sequence bigint, status text, created_at timestamptz, updated_at timestamptz)
186
+ language plpgsql
187
+ security definer
188
+ set search_path = agent_board, public
189
+ as $$
190
+ declare
191
+ v_task_id uuid;
192
+ v_root_task_id uuid;
193
+ v_event_id uuid;
194
+ v_actor_agent_id uuid;
195
+ begin
196
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
197
+
198
+ if nullif(trim(p_title), '') is null then
199
+ raise exception 'task title is required' using errcode = '22023';
200
+ end if;
201
+ if not exists (select 1 from agent_board.threads where id = p_thread_id) then
202
+ raise exception 'thread not found' using errcode = 'P0002';
203
+ end if;
204
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
205
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
206
+ end if;
207
+ if p_assigned_agent_id is not null and not exists (select 1 from agent_board.agents where id = p_assigned_agent_id) then
208
+ raise exception 'assigned agent not found' using errcode = 'P0002';
209
+ end if;
210
+
211
+ if p_parent_task_id is not null then
212
+ select coalesce(tasks.root_task_id, tasks.id)
213
+ into v_root_task_id
214
+ from agent_board.tasks
215
+ where id = p_parent_task_id and tasks.thread_id = p_thread_id;
216
+ if v_root_task_id is null then
217
+ raise exception 'parent task not found in thread' using errcode = '22023';
218
+ end if;
219
+ if not agent_board._current_agent_can_access_task(p_parent_task_id) then
220
+ raise exception 'parent task not accessible to current agent' using errcode = '42501';
221
+ end if;
222
+ end if;
223
+
224
+ v_task_id := gen_random_uuid();
225
+ if p_parent_task_id is null then
226
+ v_root_task_id := v_task_id;
227
+ end if;
228
+
229
+ insert into agent_board.tasks(id, thread_id, created_by_agent_id, assigned_agent_id, parent_task_id, root_task_id, title, details, priority, orchestration_mode, metadata)
230
+ values (v_task_id, p_thread_id, v_actor_agent_id, p_assigned_agent_id, p_parent_task_id, v_root_task_id, trim(p_title), p_details, coalesce(p_priority, 'medium'), coalesce(p_orchestration_mode, 'distributed'), coalesce(p_metadata, '{}'::jsonb));
231
+
232
+ insert into agent_board.task_events(task_id, thread_id, actor_agent_id, event_sequence, event_type, payload)
233
+ values (v_task_id, p_thread_id, v_actor_agent_id, 1, 'created', jsonb_build_object('title', trim(p_title)))
234
+ returning id into v_event_id;
235
+
236
+ return query
237
+ select t.id, t.parent_task_id, t.root_task_id, v_event_id, 1::bigint, t.status, t.created_at, t.updated_at
238
+ from agent_board.tasks t
239
+ where t.id = v_task_id;
240
+ end;
241
+ $$;
242
+
243
+ create or replace function agent_board.append_task_event(
244
+ p_task_id uuid,
245
+ p_actor_agent_id uuid,
246
+ p_event_type text,
247
+ p_payload jsonb default '{}'::jsonb,
248
+ p_status text default null,
249
+ p_assigned_agent_id uuid default null,
250
+ p_completed_at timestamptz default null
251
+ )
252
+ returns table(task_id uuid, thread_id uuid, event_id uuid, event_sequence bigint, task_status text, assigned_agent_id uuid, updated_at timestamptz, completed_at timestamptz)
253
+ language plpgsql
254
+ security definer
255
+ set search_path = agent_board, public
256
+ as $$
257
+ declare
258
+ v_thread_id uuid;
259
+ v_sequence bigint;
260
+ v_event_id uuid;
261
+ v_actor_agent_id uuid;
262
+ begin
263
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_actor_agent_id, 'p_actor_agent_id');
264
+
265
+ select tasks.thread_id into v_thread_id from agent_board.tasks where id = p_task_id for update;
266
+ if v_thread_id is null then
267
+ raise exception 'task not found' using errcode = 'P0002';
268
+ end if;
269
+ if not agent_board._current_agent_can_access_task(p_task_id) then
270
+ raise exception 'task not accessible to current agent' using errcode = '42501';
271
+ end if;
272
+
273
+ select coalesce(max(te.event_sequence), 0) + 1 into v_sequence from agent_board.task_events te where te.task_id = p_task_id;
274
+ insert into agent_board.task_events(task_id, thread_id, actor_agent_id, event_sequence, event_type, payload)
275
+ values (p_task_id, v_thread_id, v_actor_agent_id, v_sequence, p_event_type, coalesce(p_payload, '{}'::jsonb))
276
+ returning id into v_event_id;
277
+
278
+ update agent_board.tasks
279
+ set status = coalesce(p_status, tasks.status),
280
+ assigned_agent_id = coalesce(p_assigned_agent_id, tasks.assigned_agent_id),
281
+ completed_at = coalesce(p_completed_at, tasks.completed_at),
282
+ updated_at = now()
283
+ where id = p_task_id;
284
+
285
+ return query
286
+ select t.id, t.thread_id, v_event_id, v_sequence, t.status, t.assigned_agent_id, t.updated_at, t.completed_at
287
+ from agent_board.tasks t
288
+ where t.id = p_task_id;
289
+ end;
290
+ $$;
291
+
292
+ create or replace function agent_board.acquire_claim(
293
+ p_agent_id uuid,
294
+ p_resource_kind text,
295
+ p_resource_key text,
296
+ p_lease_expires_at timestamptz,
297
+ p_task_id uuid default null,
298
+ p_thread_id uuid default null,
299
+ p_metadata jsonb default '{}'::jsonb
300
+ )
301
+ returns table(claim_id uuid, task_id uuid, thread_id uuid, agent_id uuid, resource_kind text, resource_key text, status text, lease_token uuid, lease_expires_at timestamptz, released_at timestamptz, created_at timestamptz, updated_at timestamptz)
302
+ language plpgsql
303
+ security definer
304
+ set search_path = agent_board, public
305
+ as $$
306
+ declare
307
+ v_thread_id uuid;
308
+ v_claim_id uuid;
309
+ v_actor_agent_id uuid;
310
+ begin
311
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
312
+
313
+ if nullif(trim(p_resource_key), '') is null then
314
+ raise exception 'resource key is required' using errcode = '22023';
315
+ end if;
316
+
317
+ if p_task_id is not null then
318
+ select tasks.thread_id into v_thread_id from agent_board.tasks where id = p_task_id;
319
+ if v_thread_id is null then
320
+ raise exception 'task not found' using errcode = 'P0002';
321
+ end if;
322
+ if not agent_board._current_agent_can_access_task(p_task_id) then
323
+ raise exception 'task not accessible to current agent' using errcode = '42501';
324
+ end if;
325
+ else
326
+ v_thread_id := p_thread_id;
327
+ end if;
328
+
329
+ if v_thread_id is not null and not agent_board._current_agent_can_access_thread(v_thread_id) then
330
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
331
+ end if;
332
+
333
+ update agent_board.claims c
334
+ set status = 'expired',
335
+ updated_at = now()
336
+ where c.resource_kind = p_resource_kind
337
+ and c.resource_key = p_resource_key
338
+ and c.status = 'active'
339
+ and c.released_at is null
340
+ and c.lease_expires_at <= now();
341
+
342
+ if exists (
343
+ select 1 from agent_board.claims
344
+ where claims.resource_kind = p_resource_kind
345
+ and claims.resource_key = p_resource_key
346
+ and claims.status = 'active'
347
+ and claims.released_at is null
348
+ and claims.lease_expires_at > now()
349
+ ) then
350
+ raise exception 'conflicting active claim' using errcode = '23505';
351
+ end if;
352
+
353
+ insert into agent_board.claims(task_id, thread_id, agent_id, resource_kind, resource_key, lease_expires_at, metadata)
354
+ values (p_task_id, v_thread_id, v_actor_agent_id, p_resource_kind, p_resource_key, p_lease_expires_at, coalesce(p_metadata, '{}'::jsonb))
355
+ returning id into v_claim_id;
356
+
357
+ if p_task_id is not null then
358
+ perform agent_board.append_task_event(p_task_id, v_actor_agent_id, 'claimed', jsonb_build_object('claim_id', v_claim_id, 'resource_kind', p_resource_kind, 'resource_key', p_resource_key));
359
+ end if;
360
+
361
+ return query
362
+ select c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at
363
+ from agent_board.claims c
364
+ where c.id = v_claim_id;
365
+ end;
366
+ $$;
367
+
368
+ create or replace function agent_board.renew_claim(p_lease_token uuid, p_lease_expires_at timestamptz)
369
+ returns table(claim_id uuid, task_id uuid, thread_id uuid, agent_id uuid, resource_kind text, resource_key text, status text, lease_token uuid, lease_expires_at timestamptz, released_at timestamptz, created_at timestamptz, updated_at timestamptz)
370
+ language plpgsql
371
+ security definer
372
+ set search_path = agent_board, public
373
+ as $$
374
+ declare
375
+ v_actor_agent_id uuid;
376
+ begin
377
+ if not agent_board._is_service_role() then
378
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(null, 'current_agent_id');
379
+ if not exists (
380
+ select 1
381
+ from agent_board.claims c
382
+ where c.lease_token = p_lease_token
383
+ and c.agent_id = v_actor_agent_id
384
+ ) then
385
+ raise exception 'claim not accessible to current agent' using errcode = '42501';
386
+ end if;
387
+ end if;
388
+
389
+ return query
390
+ update agent_board.claims c
391
+ set lease_expires_at = p_lease_expires_at,
392
+ updated_at = now()
393
+ where c.lease_token = p_lease_token
394
+ and c.status = 'active'
395
+ and c.released_at is null
396
+ and c.lease_expires_at > now()
397
+ returning c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at;
398
+ end;
399
+ $$;
400
+
401
+ create or replace function agent_board.release_claim(p_lease_token uuid)
402
+ returns table(claim_id uuid, task_id uuid, thread_id uuid, agent_id uuid, resource_kind text, resource_key text, status text, lease_token uuid, lease_expires_at timestamptz, released_at timestamptz, created_at timestamptz, updated_at timestamptz)
403
+ language plpgsql
404
+ security definer
405
+ set search_path = agent_board, public
406
+ as $$
407
+ declare
408
+ v_actor_agent_id uuid;
409
+ begin
410
+ if not agent_board._is_service_role() then
411
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(null, 'current_agent_id');
412
+ if not exists (
413
+ select 1
414
+ from agent_board.claims c
415
+ where c.lease_token = p_lease_token
416
+ and c.agent_id = v_actor_agent_id
417
+ ) then
418
+ raise exception 'claim not accessible to current agent' using errcode = '42501';
419
+ end if;
420
+ end if;
421
+
422
+ return query
423
+ update agent_board.claims c
424
+ set status = 'released',
425
+ released_at = now(),
426
+ updated_at = now()
427
+ where c.lease_token = p_lease_token
428
+ and c.status = 'active'
429
+ and c.released_at is null
430
+ returning c.id, c.task_id, c.thread_id, c.agent_id, c.resource_kind, c.resource_key, c.status, c.lease_token, c.lease_expires_at, c.released_at, c.created_at, c.updated_at;
431
+ end;
432
+ $$;
433
+
434
+ create or replace function agent_board.delegate_task(
435
+ p_parent_task_id uuid,
436
+ p_actor_agent_id uuid,
437
+ p_title text,
438
+ p_details text default null,
439
+ p_delegated_to_agent_id uuid default null,
440
+ p_priority text default 'medium',
441
+ p_note text default null,
442
+ p_metadata jsonb default '{}'::jsonb
443
+ )
444
+ returns table(delegation_id uuid, child_task_id uuid, parent_task_id uuid, root_task_id uuid, delegation_status text, child_event_sequence bigint, parent_event_sequence bigint, created_at timestamptz)
445
+ language plpgsql
446
+ security definer
447
+ set search_path = agent_board, public
448
+ as $$
449
+ declare
450
+ v_parent agent_board.tasks%rowtype;
451
+ v_child record;
452
+ v_delegation_id uuid;
453
+ v_parent_event record;
454
+ v_actor_agent_id uuid;
455
+ begin
456
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_actor_agent_id, 'p_actor_agent_id');
457
+
458
+ select * into v_parent from agent_board.tasks where id = p_parent_task_id;
459
+ if v_parent.id is null then
460
+ raise exception 'parent task not found' using errcode = 'P0002';
461
+ end if;
462
+ if not agent_board._current_agent_can_access_task(p_parent_task_id) then
463
+ raise exception 'parent task not accessible to current agent' using errcode = '42501';
464
+ end if;
465
+
466
+ select * into v_child
467
+ from agent_board.create_task(v_parent.thread_id, v_actor_agent_id, p_title, p_details, p_delegated_to_agent_id, p_parent_task_id, p_priority, 'hierarchical', p_metadata);
468
+
469
+ insert into agent_board.delegations(parent_task_id, child_task_id, delegated_by_agent_id, delegated_to_agent_id, note, metadata)
470
+ values (p_parent_task_id, v_child.task_id, v_actor_agent_id, p_delegated_to_agent_id, p_note, coalesce(p_metadata, '{}'::jsonb))
471
+ returning id into v_delegation_id;
472
+
473
+ select * into v_parent_event
474
+ from agent_board.append_task_event(p_parent_task_id, v_actor_agent_id, 'delegated', jsonb_build_object('delegation_id', v_delegation_id, 'child_task_id', v_child.task_id));
475
+
476
+ return query
477
+ select v_delegation_id, v_child.task_id, p_parent_task_id, v_child.root_task_id, 'requested'::text, v_child.created_event_sequence, v_parent_event.event_sequence, now();
478
+ end;
479
+ $$;
480
+
481
+ create or replace function agent_board.add_artifact(
482
+ p_created_by_agent_id uuid,
483
+ p_uri text,
484
+ p_artifact_type text default 'url',
485
+ p_storage_kind text default 'external_url',
486
+ p_thread_id uuid default null,
487
+ p_task_id uuid default null,
488
+ p_message_id uuid default null,
489
+ p_task_event_id uuid default null,
490
+ p_metadata jsonb default '{}'::jsonb
491
+ )
492
+ returns table(artifact_id uuid, task_event_sequence bigint, created_at timestamptz)
493
+ language plpgsql
494
+ security definer
495
+ set search_path = agent_board, public
496
+ as $$
497
+ declare
498
+ v_artifact_id uuid;
499
+ v_event record;
500
+ v_actor_agent_id uuid;
501
+ v_thread_id uuid;
502
+ begin
503
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_created_by_agent_id, 'p_created_by_agent_id');
504
+
505
+ if p_thread_id is null and p_task_id is null and p_message_id is null and p_task_event_id is null then
506
+ raise exception 'artifact target is required' using errcode = '22023';
507
+ end if;
508
+
509
+ if p_task_id is not null then
510
+ if not agent_board._current_agent_can_access_task(p_task_id) then
511
+ raise exception 'task not accessible to current agent' using errcode = '42501';
512
+ end if;
513
+ end if;
514
+
515
+ if p_thread_id is not null and not agent_board._current_agent_can_access_thread(p_thread_id) then
516
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
517
+ end if;
518
+
519
+ if p_message_id is not null then
520
+ select m.thread_id into v_thread_id from agent_board.messages m where m.id = p_message_id;
521
+ if v_thread_id is null or not agent_board._current_agent_can_access_thread(v_thread_id) then
522
+ raise exception 'message not accessible to current agent' using errcode = '42501';
523
+ end if;
524
+ end if;
525
+
526
+ if p_task_event_id is not null then
527
+ select te.thread_id into v_thread_id from agent_board.task_events te where te.id = p_task_event_id;
528
+ if v_thread_id is null or not agent_board._current_agent_can_access_thread(v_thread_id) then
529
+ raise exception 'task event not accessible to current agent' using errcode = '42501';
530
+ end if;
531
+ end if;
532
+
533
+ insert into agent_board.artifacts(thread_id, task_id, message_id, task_event_id, created_by_agent_id, artifact_type, storage_kind, uri, metadata)
534
+ values (p_thread_id, p_task_id, p_message_id, p_task_event_id, v_actor_agent_id, coalesce(p_artifact_type, 'url'), coalesce(p_storage_kind, 'external_url'), p_uri, coalesce(p_metadata, '{}'::jsonb))
535
+ returning id into v_artifact_id;
536
+
537
+ if p_task_id is not null then
538
+ select * into v_event
539
+ from agent_board.append_task_event(p_task_id, v_actor_agent_id, 'artifact_added', jsonb_build_object('artifact_id', v_artifact_id));
540
+ end if;
541
+
542
+ return query
543
+ select v_artifact_id, v_event.event_sequence, now();
544
+ end;
545
+ $$;
546
+
547
+ create or replace function agent_board.raise_conflict(
548
+ p_thread_id uuid,
549
+ p_raised_by_agent_id uuid,
550
+ p_summary text,
551
+ p_conflict_type text default 'other',
552
+ p_severity text default 'medium',
553
+ p_task_id uuid default null,
554
+ p_details jsonb default '{}'::jsonb
555
+ )
556
+ returns table(conflict_id uuid, status text, task_event_sequence bigint, created_at timestamptz, updated_at timestamptz)
557
+ language plpgsql
558
+ security definer
559
+ set search_path = agent_board, public
560
+ as $$
561
+ declare
562
+ v_conflict_id uuid;
563
+ v_event record;
564
+ v_actor_agent_id uuid;
565
+ begin
566
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_raised_by_agent_id, 'p_raised_by_agent_id');
567
+
568
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
569
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
570
+ end if;
571
+
572
+ insert into agent_board.conflicts(thread_id, task_id, raised_by_agent_id, summary, conflict_type, severity, details)
573
+ values (p_thread_id, p_task_id, v_actor_agent_id, p_summary, coalesce(p_conflict_type, 'other'), coalesce(p_severity, 'medium'), coalesce(p_details, '{}'::jsonb))
574
+ returning id into v_conflict_id;
575
+
576
+ if p_task_id is not null then
577
+ select * into v_event
578
+ from agent_board.append_task_event(p_task_id, v_actor_agent_id, 'conflict_raised', jsonb_build_object('conflict_id', v_conflict_id));
579
+ end if;
580
+
581
+ return query
582
+ select c.id, c.status, v_event.event_sequence, c.created_at, c.updated_at
583
+ from agent_board.conflicts c
584
+ where c.id = v_conflict_id;
585
+ end;
586
+ $$;
587
+
588
+ create or replace function agent_board.resolve_conflict(
589
+ p_conflict_id uuid,
590
+ p_resolved_by_agent_id uuid,
591
+ p_details jsonb default '{}'::jsonb
592
+ )
593
+ returns table(conflict_id uuid, status text, resolved_at timestamptz, updated_at timestamptz, task_event_sequence bigint)
594
+ language plpgsql
595
+ security definer
596
+ set search_path = agent_board, public
597
+ as $$
598
+ declare
599
+ v_conflict agent_board.conflicts%rowtype;
600
+ v_event record;
601
+ v_actor_agent_id uuid;
602
+ begin
603
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_resolved_by_agent_id, 'p_resolved_by_agent_id');
604
+
605
+ select *
606
+ into v_conflict
607
+ from agent_board.conflicts c
608
+ where c.id = p_conflict_id
609
+ and c.status <> 'resolved';
610
+ if v_conflict.id is null then
611
+ raise exception 'conflict not found' using errcode = 'P0002';
612
+ end if;
613
+ if not agent_board._current_agent_can_access_thread(v_conflict.thread_id) then
614
+ raise exception 'conflict not accessible to current agent' using errcode = '42501';
615
+ end if;
616
+
617
+ update agent_board.conflicts
618
+ set status = 'resolved',
619
+ resolved_by_agent_id = v_actor_agent_id,
620
+ resolved_at = now(),
621
+ details = details || coalesce(p_details, '{}'::jsonb),
622
+ updated_at = now()
623
+ where id = p_conflict_id
624
+ returning * into v_conflict;
625
+
626
+ if v_conflict.task_id is not null then
627
+ select * into v_event
628
+ from agent_board.append_task_event(v_conflict.task_id, v_actor_agent_id, 'status_changed', jsonb_build_object('kind', 'conflict_resolved', 'conflict_id', p_conflict_id));
629
+ end if;
630
+
631
+ return query
632
+ select v_conflict.id, v_conflict.status, v_conflict.resolved_at, v_conflict.updated_at, v_event.event_sequence;
633
+ end;
634
+ $$;
635
+
636
+ create or replace function agent_board.raise_escalation(
637
+ p_thread_id uuid,
638
+ p_raised_by_agent_id uuid,
639
+ p_summary text,
640
+ p_escalation_type text default 'other',
641
+ p_severity text default 'medium',
642
+ p_task_id uuid default null,
643
+ p_assigned_agent_id uuid default null,
644
+ p_details jsonb default '{}'::jsonb
645
+ )
646
+ returns table(escalation_id uuid, status text, task_event_sequence bigint, created_at timestamptz, updated_at timestamptz)
647
+ language plpgsql
648
+ security definer
649
+ set search_path = agent_board, public
650
+ as $$
651
+ declare
652
+ v_escalation_id uuid;
653
+ v_event record;
654
+ v_actor_agent_id uuid;
655
+ begin
656
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_raised_by_agent_id, 'p_raised_by_agent_id');
657
+
658
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
659
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
660
+ end if;
661
+
662
+ insert into agent_board.escalations(thread_id, task_id, raised_by_agent_id, assigned_agent_id, summary, escalation_type, severity, details)
663
+ values (p_thread_id, p_task_id, v_actor_agent_id, p_assigned_agent_id, p_summary, coalesce(p_escalation_type, 'other'), coalesce(p_severity, 'medium'), coalesce(p_details, '{}'::jsonb))
664
+ returning id into v_escalation_id;
665
+
666
+ if p_task_id is not null then
667
+ select * into v_event
668
+ from agent_board.append_task_event(p_task_id, v_actor_agent_id, 'escalated', jsonb_build_object('escalation_id', v_escalation_id));
669
+ end if;
670
+
671
+ return query
672
+ select e.id, e.status, v_event.event_sequence, e.created_at, e.updated_at
673
+ from agent_board.escalations e
674
+ where e.id = v_escalation_id;
675
+ end;
676
+ $$;
677
+
678
+ create or replace function agent_board.resolve_escalation(
679
+ p_escalation_id uuid,
680
+ p_resolved_by_agent_id uuid,
681
+ p_details jsonb default '{}'::jsonb
682
+ )
683
+ returns table(escalation_id uuid, status text, resolved_at timestamptz, updated_at timestamptz, task_event_sequence bigint)
684
+ language plpgsql
685
+ security definer
686
+ set search_path = agent_board, public
687
+ as $$
688
+ declare
689
+ v_escalation agent_board.escalations%rowtype;
690
+ v_event record;
691
+ v_actor_agent_id uuid;
692
+ begin
693
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_resolved_by_agent_id, 'p_resolved_by_agent_id');
694
+
695
+ select *
696
+ into v_escalation
697
+ from agent_board.escalations e
698
+ where e.id = p_escalation_id
699
+ and e.status <> 'resolved';
700
+ if v_escalation.id is null then
701
+ raise exception 'escalation not found' using errcode = 'P0002';
702
+ end if;
703
+ if not agent_board._current_agent_can_access_thread(v_escalation.thread_id) then
704
+ raise exception 'escalation not accessible to current agent' using errcode = '42501';
705
+ end if;
706
+
707
+ update agent_board.escalations
708
+ set status = 'resolved',
709
+ resolved_by_agent_id = v_actor_agent_id,
710
+ resolved_at = now(),
711
+ details = details || coalesce(p_details, '{}'::jsonb),
712
+ updated_at = now()
713
+ where id = p_escalation_id
714
+ returning * into v_escalation;
715
+
716
+ if v_escalation.task_id is not null then
717
+ select * into v_event
718
+ from agent_board.append_task_event(v_escalation.task_id, v_actor_agent_id, 'status_changed', jsonb_build_object('kind', 'escalation_resolved', 'escalation_id', p_escalation_id));
719
+ end if;
720
+
721
+ return query
722
+ select v_escalation.id, v_escalation.status, v_escalation.resolved_at, v_escalation.updated_at, v_event.event_sequence;
723
+ end;
724
+ $$;
725
+
726
+ create or replace function agent_board.mark_read(p_thread_id uuid, p_agent_id uuid, p_last_read_message_sequence bigint)
727
+ returns table(thread_id uuid, agent_id uuid, last_read_message_sequence bigint, last_read_at timestamptz)
728
+ language plpgsql
729
+ security definer
730
+ set search_path = agent_board, public
731
+ as $$
732
+ declare
733
+ v_sequence bigint;
734
+ v_actor_agent_id uuid;
735
+ begin
736
+ v_actor_agent_id := agent_board._resolved_actor_agent_id(p_agent_id, 'p_agent_id');
737
+
738
+ if p_last_read_message_sequence < 0 then
739
+ raise exception 'negative read sequence' using errcode = '22023';
740
+ end if;
741
+
742
+ if not agent_board._current_agent_can_access_thread(p_thread_id) then
743
+ raise exception 'thread not accessible to current agent' using errcode = '42501';
744
+ end if;
745
+
746
+ select least(p_last_read_message_sequence, t.last_message_sequence)
747
+ into v_sequence
748
+ from agent_board.threads t
749
+ where t.id = p_thread_id;
750
+
751
+ if v_sequence is null then
752
+ raise exception 'thread not found' using errcode = 'P0002';
753
+ end if;
754
+
755
+ return query
756
+ insert into agent_board.read_receipts(thread_id, agent_id, last_read_message_sequence, last_read_at)
757
+ values (p_thread_id, v_actor_agent_id, v_sequence, now())
758
+ on conflict on constraint read_receipts_pkey do update
759
+ set last_read_message_sequence = greatest(read_receipts.last_read_message_sequence, excluded.last_read_message_sequence),
760
+ last_read_at = now()
761
+ returning read_receipts.thread_id, read_receipts.agent_id, read_receipts.last_read_message_sequence, read_receipts.last_read_at;
762
+ end;
763
+ $$;
764
+
765
+ revoke all on function agent_board.register_agent(text, text, text, text, jsonb) from public, anon;
766
+ revoke all on function agent_board.heartbeat(uuid, uuid, text, text, interval, jsonb) from public, anon;
767
+ revoke all on function agent_board.create_thread(uuid, text, text, jsonb) from public, anon;
768
+ revoke all on function agent_board.post_message(uuid, uuid, text, text, uuid, jsonb) from public, anon;
769
+ revoke all on function agent_board.create_task(uuid, uuid, text, text, uuid, uuid, text, text, jsonb) from public, anon;
770
+ revoke all on function agent_board.append_task_event(uuid, uuid, text, jsonb, text, uuid, timestamptz) from public, anon;
771
+ revoke all on function agent_board.acquire_claim(uuid, text, text, timestamptz, uuid, uuid, jsonb) from public, anon;
772
+ revoke all on function agent_board.renew_claim(uuid, timestamptz) from public, anon;
773
+ revoke all on function agent_board.release_claim(uuid) from public, anon;
774
+ revoke all on function agent_board.delegate_task(uuid, uuid, text, text, uuid, text, text, jsonb) from public, anon;
775
+ revoke all on function agent_board.add_artifact(uuid, text, text, text, uuid, uuid, uuid, uuid, jsonb) from public, anon;
776
+ revoke all on function agent_board.raise_conflict(uuid, uuid, text, text, text, uuid, jsonb) from public, anon;
777
+ revoke all on function agent_board.resolve_conflict(uuid, uuid, jsonb) from public, anon;
778
+ revoke all on function agent_board.raise_escalation(uuid, uuid, text, text, text, uuid, uuid, jsonb) from public, anon;
779
+ revoke all on function agent_board.resolve_escalation(uuid, uuid, jsonb) from public, anon;
780
+ revoke all on function agent_board.mark_read(uuid, uuid, bigint) from public, anon;
781
+
782
+ grant execute on all functions in schema agent_board to service_role;
783
+ grant execute on function agent_board.register_agent(text, text, text, text, jsonb) to authenticated;
784
+ grant execute on function agent_board.heartbeat(uuid, uuid, text, text, interval, jsonb) to authenticated;
785
+ grant execute on function agent_board.create_thread(uuid, text, text, jsonb) to authenticated;
786
+ grant execute on function agent_board.post_message(uuid, uuid, text, text, uuid, jsonb) to authenticated;
787
+ grant execute on function agent_board.create_task(uuid, uuid, text, text, uuid, uuid, text, text, jsonb) to authenticated;
788
+ grant execute on function agent_board.append_task_event(uuid, uuid, text, jsonb, text, uuid, timestamptz) to authenticated;
789
+ grant execute on function agent_board.acquire_claim(uuid, text, text, timestamptz, uuid, uuid, jsonb) to authenticated;
790
+ grant execute on function agent_board.renew_claim(uuid, timestamptz) to authenticated;
791
+ grant execute on function agent_board.release_claim(uuid) to authenticated;
792
+ grant execute on function agent_board.delegate_task(uuid, uuid, text, text, uuid, text, text, jsonb) to authenticated;
793
+ grant execute on function agent_board.add_artifact(uuid, text, text, text, uuid, uuid, uuid, uuid, jsonb) to authenticated;
794
+ grant execute on function agent_board.raise_conflict(uuid, uuid, text, text, text, uuid, jsonb) to authenticated;
795
+ grant execute on function agent_board.resolve_conflict(uuid, uuid, jsonb) to authenticated;
796
+ grant execute on function agent_board.raise_escalation(uuid, uuid, text, text, text, uuid, uuid, jsonb) to authenticated;
797
+ grant execute on function agent_board.resolve_escalation(uuid, uuid, jsonb) to authenticated;
798
+ grant execute on function agent_board.mark_read(uuid, uuid, bigint) to authenticated;