@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,622 @@
1
+ create or replace function agent_board._is_service_role()
2
+ returns boolean
3
+ language sql
4
+ stable
5
+ security definer
6
+ set search_path = ''
7
+ as $$
8
+ select coalesce(auth.role(), '') = 'service_role'
9
+ or (
10
+ coalesce(auth.role(), '') = ''
11
+ and session_user in ('postgres', 'supabase_admin')
12
+ )
13
+ $$;
14
+
15
+ create or replace function agent_board._resolved_actor_agent_id(
16
+ p_supplied_agent_id uuid,
17
+ p_param_name text
18
+ )
19
+ returns uuid
20
+ language plpgsql
21
+ stable
22
+ security definer
23
+ set search_path = ''
24
+ as $$
25
+ declare
26
+ v_current_agent_id uuid;
27
+ begin
28
+ if agent_board._is_service_role() then
29
+ if p_supplied_agent_id is null then
30
+ raise exception '% is required for service_role callers', p_param_name using errcode = '22023';
31
+ end if;
32
+ return p_supplied_agent_id;
33
+ end if;
34
+
35
+ v_current_agent_id := agent_board.current_agent_id();
36
+ if v_current_agent_id is null then
37
+ raise exception 'current agent principal not resolved' using errcode = '42501';
38
+ end if;
39
+
40
+ if p_supplied_agent_id is not null and p_supplied_agent_id <> v_current_agent_id then
41
+ raise exception '% must match current_agent_id()', p_param_name using errcode = '42501';
42
+ end if;
43
+
44
+ return v_current_agent_id;
45
+ end;
46
+ $$;
47
+
48
+ create or replace function agent_board._require_platform_admin_user_id()
49
+ returns uuid
50
+ language plpgsql
51
+ stable
52
+ security definer
53
+ set search_path = ''
54
+ as $$
55
+ declare
56
+ v_user_id uuid;
57
+ begin
58
+ if agent_board._is_service_role() then
59
+ return null;
60
+ end if;
61
+
62
+ v_user_id := auth.uid();
63
+ if v_user_id is null then
64
+ raise exception 'authenticated caller required' using errcode = '42501';
65
+ end if;
66
+
67
+ if not exists (
68
+ select 1
69
+ from public.user_org_roles uor
70
+ where uor.user_id = v_user_id
71
+ and uor.role = 'platform_admin'
72
+ ) then
73
+ raise exception 'platform_admin role required' using errcode = '42501';
74
+ end if;
75
+
76
+ return v_user_id;
77
+ end;
78
+ $$;
79
+
80
+ create or replace function agent_board._normalize_provision_slug(p_slug text)
81
+ returns text
82
+ language plpgsql
83
+ immutable
84
+ security definer
85
+ set search_path = ''
86
+ as $$
87
+ declare
88
+ v_slug text := lower(coalesce(trim(p_slug), ''));
89
+ begin
90
+ if v_slug = '' or v_slug !~ '^[a-z0-9]+(?:[-_][a-z0-9]+)*$' then
91
+ raise exception 'slug must match ^[a-z0-9]+(?:[-_][a-z0-9]+)*$' using errcode = '22023';
92
+ end if;
93
+
94
+ return v_slug;
95
+ end;
96
+ $$;
97
+
98
+ create or replace function agent_board._default_provision_display_name(p_slug text)
99
+ returns text
100
+ language sql
101
+ immutable
102
+ security definer
103
+ set search_path = ''
104
+ as $$
105
+ select replace(initcap(replace(p_slug, '-', ' ')), '_', ' ')
106
+ $$;
107
+
108
+ create or replace function agent_board._ensure_agent_auth_user(
109
+ p_slug text
110
+ )
111
+ returns uuid
112
+ language plpgsql
113
+ security definer
114
+ set search_path = ''
115
+ as $$
116
+ declare
117
+ v_email text := format('agent+%s@agent-board.local', p_slug);
118
+ v_user_id uuid;
119
+ begin
120
+ select u.id
121
+ into v_user_id
122
+ from auth.users u
123
+ where lower(u.email) = v_email
124
+ limit 1;
125
+
126
+ if v_user_id is not null then
127
+ return v_user_id;
128
+ end if;
129
+
130
+ v_user_id := gen_random_uuid();
131
+
132
+ insert into auth.users (
133
+ id,
134
+ instance_id,
135
+ email,
136
+ email_confirmed_at,
137
+ encrypted_password,
138
+ created_at,
139
+ updated_at,
140
+ raw_app_meta_data,
141
+ raw_user_meta_data,
142
+ aud,
143
+ role
144
+ )
145
+ values (
146
+ v_user_id,
147
+ '00000000-0000-0000-0000-000000000000',
148
+ v_email,
149
+ now(),
150
+ '',
151
+ now(),
152
+ now(),
153
+ jsonb_build_object('provider', 'agent_board', 'providers', jsonb_build_array('agent_board')),
154
+ jsonb_build_object('slug', p_slug, 'issuer', 'agent_board.provision_agent'),
155
+ 'authenticated',
156
+ 'authenticated'
157
+ );
158
+
159
+ insert into auth.identities (
160
+ id,
161
+ user_id,
162
+ provider_id,
163
+ provider,
164
+ identity_data,
165
+ last_sign_in_at,
166
+ created_at,
167
+ updated_at
168
+ )
169
+ values (
170
+ gen_random_uuid(),
171
+ v_user_id,
172
+ p_slug,
173
+ 'agent_board',
174
+ jsonb_build_object('sub', p_slug, 'slug', p_slug),
175
+ now(),
176
+ now(),
177
+ now()
178
+ )
179
+ on conflict do nothing;
180
+
181
+ return v_user_id;
182
+ end;
183
+ $$;
184
+
185
+ create or replace function agent_board._current_agent_has_board_access(
186
+ p_board_id uuid
187
+ )
188
+ returns boolean
189
+ language sql
190
+ stable
191
+ security definer
192
+ set search_path = ''
193
+ as $$
194
+ select
195
+ agent_board._is_service_role()
196
+ or exists (
197
+ select 1
198
+ from agent_board.board_memberships bm
199
+ where bm.board_id = p_board_id
200
+ and bm.agent_id = agent_board.current_agent_id()
201
+ and bm.membership_state in ('joined', 'subscribed')
202
+ )
203
+ $$;
204
+
205
+ create or replace function agent_board._current_agent_can_access_thread(
206
+ p_thread_id uuid
207
+ )
208
+ returns boolean
209
+ language sql
210
+ stable
211
+ security definer
212
+ set search_path = ''
213
+ as $$
214
+ select
215
+ agent_board._is_service_role()
216
+ or exists (
217
+ select 1
218
+ from agent_board.threads t
219
+ where t.id = p_thread_id
220
+ and (
221
+ t.created_by_agent_id = agent_board.current_agent_id()
222
+ or exists (
223
+ select 1
224
+ from agent_board.messages m
225
+ where m.thread_id = t.id
226
+ and m.author_agent_id = agent_board.current_agent_id()
227
+ )
228
+ or exists (
229
+ select 1
230
+ from agent_board.tasks task
231
+ where task.thread_id = t.id
232
+ and (
233
+ task.created_by_agent_id = agent_board.current_agent_id()
234
+ or task.assigned_agent_id = agent_board.current_agent_id()
235
+ )
236
+ )
237
+ or exists (
238
+ select 1
239
+ from agent_board.read_receipts rr
240
+ where rr.thread_id = t.id
241
+ and rr.agent_id = agent_board.current_agent_id()
242
+ )
243
+ or exists (
244
+ select 1
245
+ from agent_board.claims c
246
+ where c.thread_id = t.id
247
+ and c.agent_id = agent_board.current_agent_id()
248
+ )
249
+ or exists (
250
+ select 1
251
+ from agent_board.inbox_items ii
252
+ left join agent_board.messages im on im.id = ii.source_message_id
253
+ left join agent_board.tasks it on it.id = ii.source_task_id
254
+ left join agent_board.delegations ide on ide.id = ii.source_delegation_id
255
+ left join agent_board.tasks idt on idt.id = ide.child_task_id
256
+ where ii.recipient_agent_id = agent_board.current_agent_id()
257
+ and (
258
+ im.thread_id = t.id
259
+ or it.thread_id = t.id
260
+ or idt.thread_id = t.id
261
+ )
262
+ )
263
+ or exists (
264
+ select 1
265
+ from agent_board.delivery_recipients dr
266
+ join agent_board.messages bm on bm.id = dr.source_message_id
267
+ where dr.board_id is not null
268
+ and bm.thread_id = t.id
269
+ and agent_board._current_agent_has_board_access(dr.board_id)
270
+ )
271
+ )
272
+ )
273
+ $$;
274
+
275
+ create or replace function agent_board._current_agent_can_access_task(
276
+ p_task_id uuid
277
+ )
278
+ returns boolean
279
+ language sql
280
+ stable
281
+ security definer
282
+ set search_path = ''
283
+ as $$
284
+ select
285
+ agent_board._is_service_role()
286
+ or exists (
287
+ select 1
288
+ from agent_board.tasks t
289
+ where t.id = p_task_id
290
+ and (
291
+ t.created_by_agent_id = agent_board.current_agent_id()
292
+ or t.assigned_agent_id = agent_board.current_agent_id()
293
+ or agent_board._current_agent_can_access_thread(t.thread_id)
294
+ )
295
+ )
296
+ $$;
297
+
298
+ create or replace function agent_board._mint_agent_refresh_token()
299
+ returns text
300
+ language sql
301
+ volatile
302
+ security definer
303
+ set search_path = ''
304
+ as $$
305
+ select encode(extensions.gen_random_bytes(32), 'hex')
306
+ $$;
307
+
308
+ create or replace function agent_board._issue_agent_credential(
309
+ p_agent_id uuid,
310
+ p_credential_label text,
311
+ p_replaces_credential_id uuid default null,
312
+ p_metadata jsonb default '{}'::jsonb
313
+ )
314
+ returns table(
315
+ credential_id uuid,
316
+ refresh_token text,
317
+ expires_at timestamptz,
318
+ created_at timestamptz,
319
+ replaces_credential_id uuid
320
+ )
321
+ language plpgsql
322
+ security definer
323
+ set search_path = ''
324
+ as $$
325
+ declare
326
+ v_refresh_token text;
327
+ v_expires_at timestamptz := now() + interval '30 days';
328
+ v_created_at timestamptz := now();
329
+ v_credential_id uuid;
330
+ begin
331
+ v_refresh_token := agent_board._mint_agent_refresh_token();
332
+
333
+ insert into agent_board.agent_credentials(
334
+ agent_id,
335
+ refresh_token_hash,
336
+ hash_version,
337
+ credential_label,
338
+ expires_at,
339
+ replaces_credential_id,
340
+ metadata,
341
+ created_at,
342
+ updated_at
343
+ )
344
+ values (
345
+ p_agent_id,
346
+ extensions.digest(v_refresh_token, 'sha256'),
347
+ 1,
348
+ p_credential_label,
349
+ v_expires_at,
350
+ p_replaces_credential_id,
351
+ coalesce(p_metadata, '{}'::jsonb),
352
+ v_created_at,
353
+ v_created_at
354
+ )
355
+ returning id into v_credential_id;
356
+
357
+ if p_replaces_credential_id is not null then
358
+ update agent_board.agent_credentials
359
+ set revoked_at = coalesce(revoked_at, v_created_at),
360
+ rotated_at = coalesce(rotated_at, v_created_at),
361
+ replaced_by_credential_id = v_credential_id,
362
+ updated_at = v_created_at
363
+ where id = p_replaces_credential_id;
364
+ end if;
365
+
366
+ return query
367
+ select v_credential_id, v_refresh_token, v_expires_at, v_created_at, p_replaces_credential_id;
368
+ end;
369
+ $$;
370
+
371
+ create or replace function agent_board.provision_agent(
372
+ p_slug text,
373
+ p_kind text,
374
+ p_scopes jsonb default '[]'::jsonb,
375
+ p_owner_user_id uuid default null
376
+ )
377
+ returns table(
378
+ agent_id uuid,
379
+ auth_user_id uuid,
380
+ credential_id uuid,
381
+ refresh_token text,
382
+ expires_at timestamptz,
383
+ created_at timestamptz
384
+ )
385
+ language plpgsql
386
+ security definer
387
+ set search_path = agent_board, public
388
+ as $$
389
+ declare
390
+ v_slug text;
391
+ v_kind text;
392
+ v_owner_user_id uuid;
393
+ v_admin_user_id uuid;
394
+ v_auth_user_id uuid;
395
+ v_agent agent_board.agents%rowtype;
396
+ v_credential record;
397
+ begin
398
+ v_admin_user_id := agent_board._require_platform_admin_user_id();
399
+ v_slug := agent_board._normalize_provision_slug(p_slug);
400
+ v_kind := lower(coalesce(trim(p_kind), ''));
401
+ v_owner_user_id := p_owner_user_id;
402
+
403
+ if v_kind not in ('human', 'agent', 'system') then
404
+ raise exception 'kind must be one of human, agent, or system' using errcode = '22023';
405
+ end if;
406
+
407
+ if v_kind = 'system' then
408
+ v_owner_user_id := null;
409
+ elsif v_owner_user_id is null then
410
+ raise exception 'owner_user_id is required for human and agent principals' using errcode = '22023';
411
+ elsif not exists (select 1 from auth.users where id = v_owner_user_id) then
412
+ raise exception 'owner user not found' using errcode = 'P0002';
413
+ end if;
414
+
415
+ select *
416
+ into v_agent
417
+ from agent_board.agents
418
+ where handle = v_slug
419
+ limit 1
420
+ for update;
421
+
422
+ v_auth_user_id := coalesce(v_agent.auth_user_id, agent_board._ensure_agent_auth_user(v_slug));
423
+
424
+ if v_agent.id is null then
425
+ insert into agent_board.agents(
426
+ agent_type,
427
+ kind,
428
+ handle,
429
+ display_name,
430
+ status,
431
+ auth_user_id,
432
+ owner_user_id,
433
+ metadata
434
+ )
435
+ values (
436
+ v_kind,
437
+ v_kind::agent_board.agent_kind,
438
+ v_slug,
439
+ agent_board._default_provision_display_name(v_slug),
440
+ 'active',
441
+ v_auth_user_id,
442
+ v_owner_user_id,
443
+ jsonb_build_object(
444
+ 'scopes', coalesce(p_scopes, '[]'::jsonb),
445
+ 'provisionedByUserId', v_admin_user_id,
446
+ 'provisionedAt', now()
447
+ )
448
+ )
449
+ returning * into v_agent;
450
+ else
451
+ update agent_board.agents a
452
+ set agent_type = v_kind,
453
+ kind = v_kind::agent_board.agent_kind,
454
+ status = 'active',
455
+ auth_user_id = v_auth_user_id,
456
+ owner_user_id = v_owner_user_id,
457
+ metadata = coalesce(a.metadata, '{}'::jsonb)
458
+ || jsonb_build_object(
459
+ 'scopes', coalesce(p_scopes, '[]'::jsonb),
460
+ 'provisionedByUserId', v_admin_user_id,
461
+ 'provisionedAt', now()
462
+ ),
463
+ updated_at = now()
464
+ where a.id = v_agent.id
465
+ returning * into v_agent;
466
+ end if;
467
+
468
+ select *
469
+ into v_credential
470
+ from agent_board._issue_agent_credential(
471
+ v_agent.id,
472
+ 'provisioned',
473
+ (
474
+ select ac.id
475
+ from agent_board.agent_credentials ac
476
+ where ac.agent_id = v_agent.id
477
+ and ac.revoked_at is null
478
+ order by ac.created_at desc
479
+ limit 1
480
+ ),
481
+ jsonb_build_object('issuedByUserId', v_admin_user_id, 'issuedAt', now(), 'scopes', coalesce(p_scopes, '[]'::jsonb))
482
+ );
483
+
484
+ return query
485
+ select v_agent.id, v_auth_user_id, v_credential.credential_id, v_credential.refresh_token, v_credential.expires_at, v_credential.created_at;
486
+ end;
487
+ $$;
488
+
489
+ create or replace function agent_board.rotate_agent_token(
490
+ p_agent_id uuid
491
+ )
492
+ returns table(
493
+ agent_id uuid,
494
+ credential_id uuid,
495
+ refresh_token text,
496
+ expires_at timestamptz,
497
+ rotated_at timestamptz,
498
+ replaces_credential_id uuid
499
+ )
500
+ language plpgsql
501
+ security definer
502
+ set search_path = agent_board, public
503
+ as $$
504
+ declare
505
+ v_admin_user_id uuid;
506
+ v_agent agent_board.agents%rowtype;
507
+ v_previous_credential_id uuid;
508
+ v_credential record;
509
+ begin
510
+ v_admin_user_id := agent_board._require_platform_admin_user_id();
511
+
512
+ select *
513
+ into v_agent
514
+ from agent_board.agents
515
+ where id = p_agent_id
516
+ limit 1
517
+ for update;
518
+
519
+ if v_agent.id is null then
520
+ raise exception 'agent not found' using errcode = 'P0002';
521
+ end if;
522
+
523
+ select ac.id
524
+ into v_previous_credential_id
525
+ from agent_board.agent_credentials ac
526
+ where ac.agent_id = p_agent_id
527
+ and ac.revoked_at is null
528
+ order by ac.created_at desc
529
+ limit 1
530
+ for update;
531
+
532
+ select *
533
+ into v_credential
534
+ from agent_board._issue_agent_credential(
535
+ p_agent_id,
536
+ 'rotated',
537
+ v_previous_credential_id,
538
+ jsonb_build_object('rotatedByUserId', v_admin_user_id, 'rotatedAt', now())
539
+ );
540
+
541
+ return query
542
+ select p_agent_id, v_credential.credential_id, v_credential.refresh_token, v_credential.expires_at, v_credential.created_at, v_credential.replaces_credential_id;
543
+ end;
544
+ $$;
545
+
546
+ create or replace function agent_board.revoke_agent(
547
+ p_agent_id uuid,
548
+ p_reason text default null
549
+ )
550
+ returns table(
551
+ agent_id uuid,
552
+ status text,
553
+ revoked_credential_count integer,
554
+ revoked_at timestamptz
555
+ )
556
+ language plpgsql
557
+ security definer
558
+ set search_path = agent_board, public
559
+ as $$
560
+ declare
561
+ v_admin_user_id uuid;
562
+ v_agent agent_board.agents%rowtype;
563
+ v_revoked_at timestamptz := now();
564
+ v_revoked_count integer := 0;
565
+ begin
566
+ v_admin_user_id := agent_board._require_platform_admin_user_id();
567
+
568
+ update agent_board.agents a
569
+ set status = 'suspended',
570
+ updated_at = v_revoked_at,
571
+ metadata = coalesce(a.metadata, '{}'::jsonb)
572
+ || jsonb_build_object(
573
+ 'revokedByUserId', v_admin_user_id,
574
+ 'revokedAt', v_revoked_at,
575
+ 'revocationReason', nullif(trim(p_reason), '')
576
+ )
577
+ where a.id = p_agent_id
578
+ returning * into v_agent;
579
+
580
+ if v_agent.id is null then
581
+ raise exception 'agent not found' using errcode = 'P0002';
582
+ end if;
583
+
584
+ update agent_board.agent_credentials ac
585
+ set revoked_at = coalesce(ac.revoked_at, v_revoked_at),
586
+ updated_at = v_revoked_at,
587
+ metadata = coalesce(ac.metadata, '{}'::jsonb)
588
+ || jsonb_build_object(
589
+ 'revokedByUserId', v_admin_user_id,
590
+ 'revokedAt', v_revoked_at,
591
+ 'revocationReason', nullif(trim(p_reason), '')
592
+ )
593
+ where ac.agent_id = p_agent_id
594
+ and ac.revoked_at is null;
595
+
596
+ get diagnostics v_revoked_count = row_count;
597
+
598
+ return query
599
+ select v_agent.id, v_agent.status, v_revoked_count, v_revoked_at;
600
+ end;
601
+ $$;
602
+
603
+ revoke all on function agent_board._is_service_role() from public, anon, authenticated;
604
+ revoke all on function agent_board._resolved_actor_agent_id(uuid, text) from public, anon, authenticated;
605
+ revoke all on function agent_board._require_platform_admin_user_id() from public, anon, authenticated;
606
+ revoke all on function agent_board._normalize_provision_slug(text) from public, anon, authenticated;
607
+ revoke all on function agent_board._default_provision_display_name(text) from public, anon, authenticated;
608
+ revoke all on function agent_board._ensure_agent_auth_user(text) from public, anon, authenticated;
609
+ revoke all on function agent_board._current_agent_has_board_access(uuid) from public, anon, authenticated;
610
+ revoke all on function agent_board._current_agent_can_access_thread(uuid) from public, anon, authenticated;
611
+ revoke all on function agent_board._current_agent_can_access_task(uuid) from public, anon, authenticated;
612
+ revoke all on function agent_board._mint_agent_refresh_token() from public, anon, authenticated;
613
+ revoke all on function agent_board._issue_agent_credential(uuid, text, uuid, jsonb) from public, anon, authenticated;
614
+
615
+ revoke all on function agent_board.provision_agent(text, text, jsonb, uuid) from public, anon;
616
+ revoke all on function agent_board.rotate_agent_token(uuid) from public, anon;
617
+ revoke all on function agent_board.revoke_agent(uuid, text) from public, anon;
618
+
619
+ grant execute on all functions in schema agent_board to service_role;
620
+ grant execute on function agent_board.provision_agent(text, text, jsonb, uuid) to authenticated;
621
+ grant execute on function agent_board.rotate_agent_token(uuid) to authenticated;
622
+ grant execute on function agent_board.revoke_agent(uuid, text) to authenticated;