@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,411 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(20);
7
+
8
+ select has_column('agent_board', 'agents', 'auth_user_id', 'agents exposes auth_user_id');
9
+ select has_column('agent_board', 'agents', 'owner_user_id', 'agents exposes owner_user_id');
10
+ select col_type_is('agent_board', 'agents', 'kind', 'agent_board.agent_kind', 'agents.kind uses the frozen enum type');
11
+
12
+ select has_table('agent_board', 'agent_credentials', 'agent_credentials table exists');
13
+ select has_column('agent_board', 'agent_credentials', 'refresh_token_hash', 'agent_credentials stores hashed refresh-token references');
14
+ select has_column('agent_board', 'agent_credentials', 'hash_version', 'agent_credentials stores hash version metadata');
15
+ select has_column('agent_board', 'agent_credentials', 'credential_label', 'agent_credentials stores a human-readable label');
16
+ select has_column('agent_board', 'agent_credentials', 'expires_at', 'agent_credentials stores expiry metadata');
17
+ select has_column('agent_board', 'agent_credentials', 'last_used_at', 'agent_credentials stores last-used metadata');
18
+ select has_column('agent_board', 'agent_credentials', 'replaced_by_credential_id', 'agent_credentials stores replacement lineage');
19
+ select has_column('agent_board', 'agent_credentials', 'replaces_credential_id', 'agent_credentials stores reverse rotation lineage');
20
+
21
+ select has_function('agent_board', 'current_agent_id', array[]::text[], 'current_agent_id resolver exists');
22
+
23
+ select ok(
24
+ (
25
+ select p.prosecdef and p.provolatile = 's'
26
+ from pg_proc p
27
+ join pg_namespace n on n.oid = p.pronamespace
28
+ where n.nspname = 'agent_board'
29
+ and p.proname = 'current_agent_id'
30
+ ),
31
+ 'current_agent_id is security definer and stable'
32
+ );
33
+
34
+ select ok(
35
+ (
36
+ select count(distinct grantee) = 2
37
+ from information_schema.routine_privileges
38
+ where specific_schema = 'agent_board'
39
+ and routine_name = 'current_agent_id'
40
+ and privilege_type = 'EXECUTE'
41
+ and grantee in ('authenticated', 'service_role')
42
+ ),
43
+ 'current_agent_id is executable by authenticated and service_role'
44
+ );
45
+
46
+ do $$
47
+ declare
48
+ v_owner_user uuid;
49
+ v_rotated_user uuid;
50
+ v_duplicate_user uuid;
51
+ v_human_agent uuid;
52
+ v_agent_principal uuid;
53
+ v_thread uuid := gen_random_uuid();
54
+ v_task uuid;
55
+ v_message uuid;
56
+ v_conflict uuid;
57
+ v_escalation uuid;
58
+ v_delivery_recipient uuid;
59
+ v_inbox_item uuid;
60
+ v_endpoint uuid;
61
+ v_subscription uuid;
62
+ v_credential_old uuid;
63
+ v_credential_new uuid;
64
+ begin
65
+ v_owner_user := public.ensure_github_user(
66
+ 'agent-board-auth-owner',
67
+ 'agent-board-auth-owner@example.test',
68
+ 'agent-board-auth-owner',
69
+ 'Agent Board Auth Owner'
70
+ );
71
+ v_rotated_user := public.ensure_github_user(
72
+ 'agent-board-auth-rotated',
73
+ 'agent-board-auth-rotated@example.test',
74
+ 'agent-board-auth-rotated',
75
+ 'Agent Board Auth Rotated'
76
+ );
77
+ v_duplicate_user := public.ensure_github_user(
78
+ 'agent-board-auth-duplicate',
79
+ 'agent-board-auth-duplicate@example.test',
80
+ 'agent-board-auth-duplicate',
81
+ 'Agent Board Auth Duplicate'
82
+ );
83
+
84
+ insert into agent_board.agents(id, agent_type, handle, display_name, owner_user_id, auth_user_id)
85
+ values (
86
+ gen_random_uuid(),
87
+ 'human',
88
+ 'test_authschema_human',
89
+ 'Auth Human',
90
+ v_owner_user,
91
+ v_owner_user
92
+ )
93
+ returning id into v_human_agent;
94
+
95
+ insert into agent_board.agents(id, agent_type, handle, display_name, owner_user_id)
96
+ values (
97
+ gen_random_uuid(),
98
+ 'agent',
99
+ 'test_authschema_agent',
100
+ 'Auth Agent',
101
+ v_owner_user
102
+ )
103
+ returning id into v_agent_principal;
104
+
105
+ if (select kind::text from agent_board.agents where id = v_human_agent) is distinct from 'human' then
106
+ raise exception 'legacy human insert did not backfill kind';
107
+ end if;
108
+
109
+ if (select kind::text from agent_board.agents where id = v_agent_principal) is distinct from 'agent' then
110
+ raise exception 'legacy agent insert did not backfill kind';
111
+ end if;
112
+
113
+ update agent_board.agents
114
+ set agent_type = 'human'
115
+ where id = v_agent_principal;
116
+
117
+ if (select kind::text from agent_board.agents where id = v_agent_principal) is distinct from 'human' then
118
+ raise exception 'agent_type update did not keep kind in sync';
119
+ end if;
120
+
121
+ update agent_board.agents
122
+ set auth_user_id = null
123
+ where id = v_agent_principal;
124
+
125
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
126
+ values (v_thread, 'coordination', v_human_agent, 'Auth schema thread');
127
+
128
+ select task_id
129
+ into v_task
130
+ from agent_board.create_task(
131
+ v_thread,
132
+ v_human_agent,
133
+ 'Auth schema task',
134
+ 'Auth schema task details',
135
+ v_human_agent,
136
+ null,
137
+ 'high',
138
+ 'distributed',
139
+ '{}'::jsonb
140
+ );
141
+
142
+ insert into agent_board.messages(id, thread_id, author_agent_id, task_id, message_sequence, message_type, body)
143
+ values (gen_random_uuid(), v_thread, v_human_agent, v_task, 1, 'comment', 'Auth schema message')
144
+ returning id into v_message;
145
+
146
+ insert into agent_board.claims(task_id, thread_id, agent_id, resource_kind, resource_key, lease_expires_at)
147
+ values (v_task, v_thread, v_human_agent, 'repo_file', 'authschema.sql', now() + interval '10 minutes');
148
+
149
+ insert into agent_board.conflicts(thread_id, task_id, raised_by_agent_id, summary)
150
+ values (v_thread, v_task, v_human_agent, 'Auth conflict')
151
+ returning id into v_conflict;
152
+
153
+ insert into agent_board.escalations(thread_id, task_id, raised_by_agent_id, summary)
154
+ values (v_thread, v_task, v_human_agent, 'Auth escalation')
155
+ returning id into v_escalation;
156
+
157
+ insert into agent_board.delivery_recipients(delivery_mode, recipient_agent_id, source_kind, source_message_id)
158
+ values ('direct', v_human_agent, 'message', v_message)
159
+ returning id into v_delivery_recipient;
160
+
161
+ insert into agent_board.inbox_items(
162
+ delivery_recipient_id,
163
+ recipient_agent_id,
164
+ delivery_mode,
165
+ source_kind,
166
+ source_message_id,
167
+ subject,
168
+ preview,
169
+ priority,
170
+ action_state,
171
+ visible_in_inbox,
172
+ visible_in_board_feed,
173
+ fetch_descriptor
174
+ )
175
+ values (
176
+ v_delivery_recipient,
177
+ v_human_agent,
178
+ 'direct',
179
+ 'message',
180
+ v_message,
181
+ 'Auth schema subject',
182
+ 'Auth schema preview',
183
+ 'high',
184
+ 'new',
185
+ true,
186
+ false,
187
+ jsonb_build_object('surface', 'thread', 'targetId', v_thread::text, 'view', 'agent_board.v_thread_feed', 'filters', jsonb_build_object(), 'reason', 'auth schema')
188
+ )
189
+ returning id into v_inbox_item;
190
+
191
+ insert into agent_board.callback_endpoints(owner_agent_id, endpoint_url, endpoint_status, verification_requested_at, verified_at)
192
+ values (v_human_agent, 'https://example.test/authschema', 'verified', now(), now())
193
+ returning id into v_endpoint;
194
+
195
+ insert into agent_board.callback_subscriptions(endpoint_id, agent_id, subscription_scope, event_category, subscription_status)
196
+ values (v_endpoint, v_human_agent, 'own_inbox', 'inbox.item.delivered', 'active')
197
+ returning id into v_subscription;
198
+
199
+ insert into agent_board.agent_credentials(
200
+ agent_id,
201
+ refresh_token_hash,
202
+ hash_version,
203
+ credential_label,
204
+ expires_at
205
+ )
206
+ values (
207
+ v_human_agent,
208
+ decode(repeat('01', 32), 'hex'),
209
+ 1,
210
+ 'primary',
211
+ now() + interval '30 days'
212
+ )
213
+ returning id into v_credential_old;
214
+
215
+ insert into agent_board.agent_credentials(
216
+ agent_id,
217
+ refresh_token_hash,
218
+ hash_version,
219
+ credential_label,
220
+ expires_at,
221
+ replaces_credential_id
222
+ )
223
+ values (
224
+ v_human_agent,
225
+ decode(repeat('02', 32), 'hex'),
226
+ 1,
227
+ 'rotated',
228
+ now() + interval '60 days',
229
+ v_credential_old
230
+ )
231
+ returning id into v_credential_new;
232
+
233
+ update agent_board.agent_credentials
234
+ set replaced_by_credential_id = v_credential_new,
235
+ revoked_at = now(),
236
+ rotated_at = now()
237
+ where id = v_credential_old;
238
+
239
+ perform set_config('request.jwt.claim.sub', gen_random_uuid()::text, true);
240
+ if agent_board.current_agent_id() is not null then
241
+ raise exception 'unknown JWT unexpectedly resolved an agent principal';
242
+ end if;
243
+
244
+ perform set_config('request.jwt.claim.sub', v_owner_user::text, true);
245
+ if agent_board.current_agent_id() is distinct from v_human_agent then
246
+ raise exception 'known JWT did not resolve the expected agent principal';
247
+ end if;
248
+
249
+ update agent_board.agents
250
+ set auth_user_id = v_rotated_user
251
+ where id = v_human_agent;
252
+
253
+ perform set_config('request.jwt.claim.sub', v_owner_user::text, true);
254
+ if agent_board.current_agent_id() is not null then
255
+ raise exception 'rotated auth principal still resolved from the old user id';
256
+ end if;
257
+
258
+ perform set_config('request.jwt.claim.sub', v_rotated_user::text, true);
259
+ if agent_board.current_agent_id() is distinct from v_human_agent then
260
+ raise exception 'rotated auth principal did not resolve from the new user id';
261
+ end if;
262
+
263
+ if (select count(*) from agent_board.messages where author_agent_id = v_human_agent) <> 1 then
264
+ raise exception 'message history was orphaned by auth rotation';
265
+ end if;
266
+
267
+ if (select count(*) from agent_board.tasks where created_by_agent_id = v_human_agent or assigned_agent_id = v_human_agent) <> 1 then
268
+ raise exception 'task history was orphaned by auth rotation';
269
+ end if;
270
+
271
+ if (select count(*) from agent_board.claims where agent_id = v_human_agent) <> 1 then
272
+ raise exception 'claim history was orphaned by auth rotation';
273
+ end if;
274
+
275
+ if (select count(*) from agent_board.conflicts where id = v_conflict and raised_by_agent_id = v_human_agent) <> 1 then
276
+ raise exception 'conflict history was orphaned by auth rotation';
277
+ end if;
278
+
279
+ if (select count(*) from agent_board.escalations where id = v_escalation and raised_by_agent_id = v_human_agent) <> 1 then
280
+ raise exception 'escalation history was orphaned by auth rotation';
281
+ end if;
282
+
283
+ if (select count(*) from agent_board.inbox_items where id = v_inbox_item and recipient_agent_id = v_human_agent) <> 1 then
284
+ raise exception 'inbox history was orphaned by auth rotation';
285
+ end if;
286
+
287
+ if (select count(*) from agent_board.callback_endpoints where id = v_endpoint and owner_agent_id = v_human_agent) <> 1 then
288
+ raise exception 'callback endpoint history was orphaned by auth rotation';
289
+ end if;
290
+
291
+ if (select count(*) from agent_board.callback_subscriptions where id = v_subscription and agent_id = v_human_agent) <> 1 then
292
+ raise exception 'callback subscription history was orphaned by auth rotation';
293
+ end if;
294
+
295
+ begin
296
+ insert into agent_board.agents(id, agent_type, handle, display_name, owner_user_id, auth_user_id)
297
+ values (
298
+ gen_random_uuid(),
299
+ 'human',
300
+ 'test_authschema_duplicate_auth',
301
+ 'Duplicate Auth User',
302
+ v_duplicate_user,
303
+ v_rotated_user
304
+ );
305
+ raise exception 'duplicate active auth_user_id insert unexpectedly succeeded';
306
+ exception
307
+ when unique_violation then
308
+ null;
309
+ end;
310
+
311
+ begin
312
+ insert into agent_board.agent_credentials(
313
+ agent_id,
314
+ refresh_token_hash,
315
+ hash_version,
316
+ credential_label,
317
+ expires_at
318
+ )
319
+ values (
320
+ v_human_agent,
321
+ decode(repeat('02', 32), 'hex'),
322
+ 1,
323
+ 'duplicate-hash',
324
+ now() + interval '1 day'
325
+ );
326
+ raise exception 'duplicate refresh_token_hash unexpectedly succeeded';
327
+ exception
328
+ when unique_violation then
329
+ null;
330
+ end;
331
+ end
332
+ $$;
333
+
334
+ select ok(
335
+ exists (
336
+ select 1
337
+ from pg_indexes
338
+ where schemaname = 'agent_board'
339
+ and tablename = 'agents'
340
+ and indexname = 'idx_agent_board_agents_auth_user_id_active_unique'
341
+ ),
342
+ 'agents has a single-active-principal unique index on auth_user_id'
343
+ );
344
+
345
+ select ok(
346
+ exists (
347
+ select 1
348
+ from pg_indexes
349
+ where schemaname = 'agent_board'
350
+ and tablename = 'agent_credentials'
351
+ and indexname = 'idx_agent_board_agent_credentials_refresh_token_hash'
352
+ ),
353
+ 'agent_credentials has a unique index on refresh_token_hash'
354
+ );
355
+
356
+ select is(
357
+ (
358
+ select kind::text
359
+ from agent_board.agents
360
+ where handle = 'test_authschema_human'
361
+ ),
362
+ 'human',
363
+ 'legacy agent_type writers continue to produce a valid kind value'
364
+ );
365
+
366
+ select is(
367
+ (
368
+ select count(*)::int
369
+ from agent_board.agents
370
+ where auth_user_id = (
371
+ select auth_user_id
372
+ from agent_board.agents
373
+ where handle = 'test_authschema_human'
374
+ )
375
+ and status = 'active'
376
+ ),
377
+ 1,
378
+ 'one auth_user_id resolves to at most one active agent principal'
379
+ );
380
+
381
+ select is(
382
+ (
383
+ select count(*)::int
384
+ from agent_board.agent_credentials
385
+ where agent_id = (
386
+ select id
387
+ from agent_board.agents
388
+ where handle = 'test_authschema_human'
389
+ )
390
+ and replaced_by_credential_id is not null
391
+ and revoked_at is not null
392
+ ),
393
+ 1,
394
+ 'credential rotation preserves durable replacement lineage'
395
+ );
396
+
397
+ select throws_ok(
398
+ $$insert into agent_board.agent_credentials(agent_id, refresh_token_hash, hash_version, credential_label)
399
+ values (
400
+ (select id from agent_board.agents where handle = 'test_authschema_human'),
401
+ decode(repeat('03', 32), 'hex'),
402
+ 1,
403
+ 'missing-expiry'
404
+ )$$,
405
+ '23502',
406
+ null,
407
+ 'agent_credentials requires expiry metadata'
408
+ );
409
+
410
+ select finish();
411
+ rollback;
@@ -0,0 +1,148 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(10);
7
+
8
+ select has_function('agent_board', 'current_agent_id', array[]::text[], 'current_agent_id exists');
9
+ select has_function('agent_board', '_signed_write_actor_id', array['jsonb', 'jsonb', 'text'], 'AUTHBOUND actor assertion helper exists');
10
+ select table_privs_are('agent_board', 'agent_credentials', 'service_role', array['SELECT'], 'service_role can perform server-only credential exchange lookup');
11
+ select ok(has_column_privilege('service_role', 'agent_board.agent_credentials', 'last_used_at', 'UPDATE'), 'service_role can update credential last_used_at');
12
+ select ok(has_column_privilege('service_role', 'agent_board.agent_credentials', 'updated_at', 'UPDATE'), 'service_role can update credential updated_at');
13
+
14
+ create temp table _agent_board_authbound_fixture (
15
+ user_id uuid,
16
+ agent_id uuid,
17
+ credential_id uuid,
18
+ signing_key_id uuid
19
+ ) on commit drop;
20
+
21
+ grant all on table _agent_board_authbound_fixture to authenticated, service_role;
22
+
23
+ do $$
24
+ declare
25
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
26
+ v_user uuid;
27
+ v_agent uuid := gen_random_uuid();
28
+ v_credential uuid := gen_random_uuid();
29
+ v_key uuid;
30
+ begin
31
+ v_user := public.ensure_github_user(
32
+ 'agent-board-authbound-' || v_suffix,
33
+ 'agent-board-authbound-' || v_suffix || '@example.test',
34
+ 'agent-board-authbound-' || v_suffix,
35
+ 'Agent Board AUTHBOUND ' || v_suffix
36
+ );
37
+
38
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
39
+ values (v_agent, 'agent', 'agent', 'authbound-' || v_suffix, 'AUTHBOUND Agent', 'active', v_user, v_user);
40
+
41
+ insert into agent_board.agent_credentials(id, agent_id, refresh_token_hash, hash_version, credential_label, expires_at)
42
+ values (v_credential, v_agent, extensions.digest('authbound-refresh-' || v_suffix, 'sha256'), 1, 'authbound', now() + interval '1 day');
43
+
44
+ insert into agent_board.agent_signing_keys(agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
45
+ values (v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url', 'sha256:' || repeat('A', 43), 'active', 'proven')
46
+ returning id into v_key;
47
+
48
+ insert into _agent_board_authbound_fixture(user_id, agent_id, credential_id, signing_key_id)
49
+ values (v_user, v_agent, v_credential, v_key);
50
+ end
51
+ $$;
52
+
53
+ set local role authenticated;
54
+ select set_config('request.jwt.claim.role', 'authenticated', true);
55
+
56
+ do $$
57
+ declare
58
+ v_fixture record;
59
+ begin
60
+ select * into v_fixture from _agent_board_authbound_fixture;
61
+ perform set_config('request.jwt.claim.sub', v_fixture.user_id::text, true);
62
+ perform set_config('request.jwt.claim.agent_credential_id', v_fixture.credential_id::text, true);
63
+
64
+ if agent_board.current_agent_id() is distinct from v_fixture.agent_id then
65
+ raise exception 'credential-bound current_agent_id did not resolve active agent';
66
+ end if;
67
+ end
68
+ $$;
69
+
70
+ reset role;
71
+
72
+ do $$
73
+ declare
74
+ v_fixture record;
75
+ begin
76
+ select * into v_fixture from _agent_board_authbound_fixture;
77
+ update agent_board.agent_credentials
78
+ set revoked_at = now()
79
+ where id = v_fixture.credential_id;
80
+ end
81
+ $$;
82
+
83
+ set local role authenticated;
84
+ select set_config('request.jwt.claim.role', 'authenticated', true);
85
+
86
+ do $$
87
+ declare
88
+ v_fixture record;
89
+ begin
90
+ select * into v_fixture from _agent_board_authbound_fixture;
91
+ perform set_config('request.jwt.claim.sub', v_fixture.user_id::text, true);
92
+ perform set_config('request.jwt.claim.agent_credential_id', v_fixture.credential_id::text, true);
93
+ if agent_board.current_agent_id() is not null then
94
+ raise exception 'revoked credential still resolved before JWT expiry';
95
+ end if;
96
+ end
97
+ $$;
98
+
99
+ select pass('current_agent_id honors agent_credential_id revocation before JWT expiry');
100
+
101
+ select throws_like(
102
+ $$select agent_board._signed_write_actor_id(
103
+ '{}'::jsonb,
104
+ '{}'::jsonb,
105
+ 'p_actor_agent_id'
106
+ )$$,
107
+ '%actor_mismatch%',
108
+ 'missing gateway actor fails closed'
109
+ );
110
+
111
+ select throws_like(
112
+ $$select agent_board._signed_write_actor_id(
113
+ jsonb_build_object('p_actor_agent_id', (select agent_id::text from _agent_board_authbound_fixture)),
114
+ jsonb_build_object('agent_id', gen_random_uuid()::text),
115
+ 'p_actor_agent_id'
116
+ )$$,
117
+ '%actor_mismatch%',
118
+ 'payload actor assertion mismatch fails closed'
119
+ );
120
+
121
+ select ok(
122
+ exists (
123
+ select 1
124
+ from pg_proc p
125
+ join pg_namespace n on n.oid = p.pronamespace
126
+ where n.nspname = 'agent_board'
127
+ and p.proname = '_record_signed_write_evidence'
128
+ and pg_get_functiondef(p.oid) like '%authbound%'
129
+ and pg_get_functiondef(p.oid) like '%metadata-safe%'
130
+ ),
131
+ 'signed-write evidence records AUTHBOUND metadata-safe posture'
132
+ );
133
+
134
+ select ok(
135
+ exists (
136
+ select 1
137
+ from pg_proc p
138
+ join pg_namespace n on n.oid = p.pronamespace
139
+ where n.nspname = 'agent_board'
140
+ and p.proname = 'current_agent_id'
141
+ and pg_get_functiondef(p.oid) like '%agent_credential_id%'
142
+ ),
143
+ 'current_agent_id records AUTHBOUND credential-claim boundary'
144
+ );
145
+
146
+ select finish();
147
+
148
+ rollback;