@consiliency/agent-board-schema 1.23.0 → 1.25.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 (32) hide show
  1. package/README.md +75 -1
  2. package/fixtures/already-applied.json +12 -0
  3. package/migrations/20260905000100_agent_board_consent_gating_reconcile.sql +151 -0
  4. package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
  5. package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
  6. package/package.json +1 -1
  7. package/schema-manifest.json +30 -3
  8. package/src/check.js +23 -0
  9. package/src/index.js +20 -0
  10. package/src/plan.js +79 -5
  11. package/src/tooling.test.mjs +237 -4
  12. package/tests/agent_board_atomic_fleet_onboarding.test.sql +124 -112
  13. package/tests/agent_board_claimkey.test.sql +25 -3
  14. package/tests/agent_board_consent_emission.test.sql +17 -13
  15. package/tests/agent_board_consent_gating.test.sql +55 -11
  16. package/tests/agent_board_delegation_policies.test.sql +3 -1
  17. package/tests/agent_board_enroll_resumption.test.sql +412 -0
  18. package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
  19. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
  20. package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
  21. package/tests/agent_board_mint_harden.test.sql +39 -4
  22. package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
  23. package/tests/agent_board_rejectsem.test.sql +1 -1
  24. package/tests/agent_board_rolereset.test.sql +23 -2
  25. package/tests/agent_board_rowid.test.sql +24 -13
  26. package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
  27. package/tests/agent_board_signed_authority_provenance.test.sql +454 -0
  28. package/tests/agent_board_source_strip_helper.test.sql +285 -0
  29. package/tests/agent_board_wake_recovery.test.sql +58 -40
  30. package/tests/catalog/p_metadata_parameters.json +689 -0
  31. package/tests/fixtures/test-prereqs.sql +313 -0
  32. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
@@ -16,7 +16,7 @@ set search_path = extensions, public, agent_board;
16
16
 
17
17
  begin;
18
18
 
19
- select plan(25);
19
+ select plan(26);
20
20
 
21
21
  select set_config('request.jwt.claim.role', 'service_role', true);
22
22
  select set_config('request.jwt.claim.sub', '', true);
@@ -219,11 +219,33 @@ select is(
219
219
  join pg_namespace n on n.oid = p.pronamespace
220
220
  where n.nspname = 'agent_board'
221
221
  and p.prokind = 'f'
222
- and pg_get_functiondef(p.oid) ~* '(insert\s+into|update)\s+(agent_board\.)?claims\M'
223
- and pg_get_functiondef(p.oid) !~ '_canonical_claim_key'),
222
+ and agent_board_test.stripped_body(p.oid) ~* '(insert\s+into|update)\s+(agent_board\.)?claims\M'
223
+ -- message-board#196 Bucket B: read RAW, one comment naming _canonical_claim_key
224
+ -- dropped a bypass writer out of this set and the count went to 0 green.
225
+ -- anchored to the CALL shape: a bare-substring `!~` is satisfied by any
226
+ -- identifier containing the name, e.g. an argument `p_canonical_claim_key`.
227
+ -- SCHEMA-QUALIFIED, because a bare-name anchor proves call SYNTAX, not callee
228
+ -- IDENTITY: `\m_canonical_claim_key\s*\(` also matches
229
+ -- `zz_other._canonical_claim_key(...)` (measured true). Every live call
230
+ -- site in agent_board is `agent_board.`-qualified, so requiring the
231
+ -- qualification costs nothing today and is fail-CLOSED if someone later writes
232
+ -- an unqualified call (it reads as NOT calling). Overload count is pinned
233
+ -- separately, so schema + name is the identity.
234
+ and agent_board_test.stripped_body(p.oid) !~* '\magent_board\._canonical_claim_key\s*\('),
224
235
  0,
225
236
  'every catalog-enumerated claims writer routes its resource_key through _canonical_claim_key');
226
237
 
238
+
239
+ -- The schema-qualified call anchor above treats `agent_board._canonical_claim_key` as an
240
+ -- IDENTITY. That is only true while the name resolves to exactly one function:
241
+ -- a second overload would be a different implementation the same anchor accepts.
242
+ -- Catalog-derived, so a new overload fails here and the count names itself.
243
+ select is(
244
+ (select count(*)::int from pg_proc p join pg_namespace n on n.oid = p.pronamespace
245
+ where n.nspname = 'agent_board' and p.proname = '_canonical_claim_key'),
246
+ 1,
247
+ 'agent_board._canonical_claim_key has exactly ONE overload, so the schema-qualified call anchor identifies it uniquely');
248
+
227
249
  select * from finish();
228
250
 
229
251
  rollback;
@@ -222,21 +222,18 @@ select is(
222
222
  -- anywhere in the body is fail-open twice over: the name in a comment or a dead branch would
223
223
  -- satisfy it, and calling the emitter TWICE would too -- while this suite claims exactly one
224
224
  -- event per transition.
225
- -- Strips BOTH comment forms. The first version stripped only `--`, so a /* */ commented-out
226
- -- call still satisfied the completeness assertion.
227
- create function pg_temp.code(p_oid oid) returns text language sql stable as $$
228
- select regexp_replace(
229
- regexp_replace(pg_get_functiondef(p_oid), '/\*.*?\*/', '', 'gs'),
230
- '--[^\n]*', '', 'g');
231
- $$;
225
+ -- The stripped reader is agent_board_test.stripped_body(), shared by the whole
226
+ -- suite from tests/fixtures/test-prereqs.sql. The local version this replaces
227
+ -- stripped both comment forms with a regex -- which still could not see a NESTED
228
+ -- block comment (message-board#199) or a string literal.
232
229
 
233
230
  select is(
234
231
  (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
235
232
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
236
233
  where n.nspname = 'agent_board' and p.prokind = 'f'
237
234
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
238
- and pg_temp.code(p.oid) ~* '(insert into|update)[[:space:]]+agent_board\.board_memberships'
239
- and pg_temp.code(p.oid) !~ 'perform agent_board\._emit_membership_event'
235
+ and agent_board_test.stripped_body(p.oid) ~* '(insert into|update)[[:space:]]+agent_board\.board_memberships'
236
+ and agent_board_test.stripped_body(p.oid) !~* 'perform agent_board\._emit_membership_event'
240
237
  and p.proname <> 'grant_board_role'),
241
238
  '',
242
239
  'COMPLETENESS: every function writing board_memberships emits, or is a stated exemption');
@@ -244,14 +241,21 @@ select is(
244
241
  -- ...and none of them emits MORE than once, which would break one-event-per-transition.
245
242
  select is(
246
243
  (select coalesce(string_agg(p.proname || '=' ||
247
- (length(pg_temp.code(p.oid)) - length(replace(pg_temp.code(p.oid),
244
+ (length(agent_board_test.stripped_body(p.oid)) - length(replace(lower(agent_board_test.stripped_body(p.oid)),
248
245
  'perform agent_board._emit_membership_event', '')))
249
246
  / length('perform agent_board._emit_membership_event'), ', ' order by p.proname), '')
250
247
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
251
248
  where n.nspname = 'agent_board' and p.prokind = 'f'
252
249
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
253
- and pg_temp.code(p.oid) ~ 'perform agent_board\._emit_membership_event'
254
- and (length(pg_temp.code(p.oid)) - length(replace(pg_temp.code(p.oid),
250
+ -- CASE-INSENSITIVE, and that is load-bearing rather than tidiness. SQL folds
251
+ -- unquoted identifiers, but `prosrc` is stored VERBATIM, so a writer spelling
252
+ -- the same call `AGENT_BOARD.…` or the same column `FINGERPRINT` is identical
253
+ -- to PostgreSQL and invisible to a case-sensitive scan. For a MEMBERSHIP
254
+ -- filter that is the fail-OPEN direction: the offender simply leaves the
255
+ -- class and the expected-empty assertion stays green. (Same mechanism
256
+ -- agent_board_wake_recovery.test.sql records for its own WR-7 filter.)
257
+ and agent_board_test.stripped_body(p.oid) ~* 'perform agent_board\._emit_membership_event'
258
+ and (length(agent_board_test.stripped_body(p.oid)) - length(replace(lower(agent_board_test.stripped_body(p.oid)),
255
259
  'perform agent_board._emit_membership_event', '')))
256
260
  / length('perform agent_board._emit_membership_event') <> 1),
257
261
  '',
@@ -259,7 +263,7 @@ select is(
259
263
 
260
264
  -- ...and the one exemption is genuinely state-blind: membership_state never appears in a SET.
261
265
  select ok(
262
- (select pg_get_functiondef(p.oid) !~* 'set[^;]*membership_state[[:space:]]*='
266
+ (select agent_board_test.stripped_body(p.oid) !~* 'set[^;]*membership_state[[:space:]]*='
263
267
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
264
268
  where n.nspname = 'agent_board' and p.proname = 'grant_board_role'),
265
269
  'COMPLETENESS: the exemption holds -- grant_board_role never SETs membership_state');
@@ -17,7 +17,7 @@ set search_path = extensions, public, agent_board;
17
17
 
18
18
  begin;
19
19
 
20
- select plan(53);
20
+ select plan(54);
21
21
 
22
22
  create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
23
23
  begin
@@ -298,14 +298,44 @@ select is(
298
298
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
299
299
  and has_function_privilege('authenticated', p.oid, 'EXECUTE')
300
300
  -- reaches an active membership, directly or through the shared sink
301
- and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
301
+ and agent_board_test.stripped_body(p.oid)
302
302
  -- Includes update_board_membership: a one-hop WRAPPER calling it with 'joined'
303
303
  -- reaches an active membership without ever naming the sink, and would otherwise
304
304
  -- evade this class assertion entirely.
305
- ~ '(_upsert_board_membership_state|_redeem_invite_for_agent|update_board_membership|insert into agent_board\.board_memberships)'
306
- -- gated: consults the predicate, or refuses a non-subject actor structurally
307
- and regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
308
- !~ '(_membership_consent_present|actor mismatch|requires an act by that agent)'
305
+ -- CASE-INSENSITIVE, and that is load-bearing rather than tidiness. SQL folds
306
+ -- unquoted identifiers, but `prosrc` is stored VERBATIM, so a writer spelling
307
+ -- the same call `AGENT_BOARD.…` or the same column `FINGERPRINT` is identical
308
+ -- to PostgreSQL and invisible to a case-sensitive scan. For a MEMBERSHIP
309
+ -- filter that is the fail-OPEN direction: the offender simply leaves the
310
+ -- class and the expected-empty assertion stays green. (Same mechanism
311
+ -- agent_board_wake_recovery.test.sql records for its own WR-7 filter.)
312
+ ~* '(_upsert_board_membership_state|_redeem_invite_for_agent|update_board_membership|insert into agent_board\.board_memberships)'
313
+ -- gated: consults the predicate, or refuses a non-subject actor structurally.
314
+ --
315
+ -- SPLIT IN TWO, by what kind of thing each marker IS. The single `!~` over a
316
+ -- three-way alternation could not be read correctly in either mode: the
317
+ -- helper name is CODE and the other two are raise MESSAGES, so
318
+ -- literal-stripping deleted the messages (every route then looked ungated)
319
+ -- and literal-keeping left the messages maskable by any string.
320
+ --
321
+ -- The message half is therefore ANCHORED TO THE STATEMENT SHAPE
322
+ -- (message-board#196 remedy 2) rather than to the bare message: the text has
323
+ -- to follow a `raise exception` to count. Measured before the split: a
324
+ -- `raise debug 'gate: %', '_membership_consent_present';` planted in an
325
+ -- ungated update_board_membership took this class assertion GREEN with the
326
+ -- gate genuinely removed. It does not any more.
327
+ -- anchored to the CALL shape, for the same reason the message half below is
328
+ -- anchored to the raise shape: a bare substring is satisfied by a NAME.
329
+ -- SCHEMA-QUALIFIED, because a bare-name anchor proves call SYNTAX, not callee
330
+ -- IDENTITY: `\m_membership_consent_present\s*\(` also matches
331
+ -- `zz_other._membership_consent_present(...)` (measured true). Every live call
332
+ -- site in agent_board is `agent_board.`-qualified, so requiring the
333
+ -- qualification costs nothing today and is fail-CLOSED if someone later writes
334
+ -- an unqualified call (it reads as NOT calling). Overload count is pinned
335
+ -- separately, so schema + name is the identity.
336
+ and agent_board_test.stripped_body(p.oid) !~* '\magent_board\._membership_consent_present\s*\('
337
+ and agent_board_test.stripped_body(p.oid, true)
338
+ !~ 'raise[[:space:]]+exception[^;]{0,250}(actor mismatch|requires an act by that agent)'
309
339
  -- stated exemptions, each with a reason recorded in the migration header
310
340
  and p.proname not in (
311
341
  'create_board', -- the actor IS the subject: it seeds the creator's own seat
@@ -329,7 +359,9 @@ select is(
329
359
  from pg_proc p
330
360
  join pg_namespace n on n.oid = p.pronamespace
331
361
  cross join lateral regexp_matches(
332
- regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g'),
362
+ -- LITERALS KEPT: the marker is the ARGUMENT ''joined''/''subscribed'' passed
363
+ -- to the sink -- a string literal in the code being matched.
364
+ agent_board_test.stripped_body(p.oid, true),
333
365
  '(?:_upsert_board_membership_state|update_board_membership)\s*\([^;]*?''(joined|subscribed)''',
334
366
  'g') m
335
367
  where n.nspname = 'agent_board'
@@ -343,7 +375,8 @@ select is(
343
375
  from pg_proc p
344
376
  join pg_namespace n on n.oid = p.pronamespace
345
377
  cross join lateral regexp_matches(
346
- regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g'),
378
+ -- LITERALS KEPT: same shape as above -- the state being written is a literal.
379
+ agent_board_test.stripped_body(p.oid, true),
347
380
  '(?:_upsert_board_membership_state|update_board_membership)\s*\([^;]*?''(invited|requested|left)''',
348
381
  'g') m
349
382
  where n.nspname = 'agent_board'
@@ -389,7 +422,7 @@ select is(
389
422
  -- silently reopening the race -- the classic wrong "fix" for lock-wait reports. `nowait`
390
423
  -- is excluded too (it at least fails loudly).
391
424
  select matches(
392
- (select regexp_replace(regexp_replace(pg_get_functiondef(p.oid), '/\*.*?\*/', '', 'gs'), '--[^\n]*', '', 'g')
425
+ (select agent_board_test.stripped_body(p.oid)
393
426
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
394
427
  where n.nspname = 'agent_board' and p.proname = 'resolve_board_request'),
395
428
  'from agent_board\.board_memberships[[:space:]]+bm[[:space:]]+where bm\.board_id = p_board_id[[:space:]]+and bm\.agent_id = p_agent_id[[:space:]]+order by bm\.id[[:space:]]+for update;',
@@ -399,14 +432,14 @@ select matches(
399
432
  -- a lock taken after the check protects nothing.
400
433
  select ok(
401
434
  (select position('for update' in body) < position('_membership_consent_present' in body)
402
- from (select regexp_replace(regexp_replace(pg_get_functiondef(p.oid), '/\*.*?\*/', '', 'gs'), '--[^\n]*', '', 'g') as body
435
+ from (select agent_board_test.stripped_body(p.oid) as body
403
436
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
404
437
  where n.nspname = 'agent_board' and p.proname = 'resolve_board_request') s),
405
438
  'LOCK: ...and reads consent AFTER taking it, not before');
406
439
 
407
440
  -- The sink acts on the row the CALLER named, rather than reselecting (#109).
408
441
  select matches(
409
- (select regexp_replace(pg_get_functiondef(p.oid), '--[^\n]*', '', 'g')
442
+ (select agent_board_test.stripped_body(p.oid)
410
443
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
411
444
  where n.nspname = 'agent_board' and p.proname = '_upsert_board_membership_state'),
412
445
  'if p_membership_id is not null then',
@@ -485,5 +518,16 @@ select is(
485
518
  select is((select version from agent_board.schema_version), 8,
486
519
  'schema_version stays 8 -- additive');
487
520
 
521
+
522
+ -- The schema-qualified call anchor above treats `agent_board._membership_consent_present` as an
523
+ -- IDENTITY. That is only true while the name resolves to exactly one function:
524
+ -- a second overload would be a different implementation the same anchor accepts.
525
+ -- Catalog-derived, so a new overload fails here and the count names itself.
526
+ select is(
527
+ (select count(*)::int from pg_proc p join pg_namespace n on n.oid = p.pronamespace
528
+ where n.nspname = 'agent_board' and p.proname = '_membership_consent_present'),
529
+ 1,
530
+ 'agent_board._membership_consent_present has exactly ONE overload, so the schema-qualified call anchor identifies it uniquely');
531
+
488
532
  select finish();
489
533
  rollback;
@@ -94,7 +94,9 @@ select is(
94
94
  false, 'over-ceiling grant is rejected (owner cannot grant admin when ceiling is member)');
95
95
 
96
96
  -- #13 owner-settable ceiling: set_board_authority_ceiling is owner-gated. ---------
97
- select has_function('agent_board', 'set_board_authority_ceiling', array['uuid','text'],
97
+ -- #247 added the delegable p_actor_agent_id (null default => the pre-#247 path for every
98
+ -- role), so the identity signature is (uuid, text, uuid).
99
+ select has_function('agent_board', 'set_board_authority_ceiling', array['uuid','text','uuid'],
98
100
  'set_board_authority_ceiling exists');
99
101
 
100
102
  set local role authenticated;
@@ -0,0 +1,412 @@
1
+ -- Consiliency/message-board#176 pgTAP: OWNER-SCOPED ENROLL RESUMPTION.
2
+ --
3
+ -- The live failure: /enroll committed an identity, post-enroll provisioning failed, the
4
+ -- client reported total failure, and every retry was then refused forever with
5
+ -- `23505 this signing key is already enrolled`. 20260901000100's replay wrapper resumes
6
+ -- only rows carrying its own `metadata.federation.replay` STAMP -- which the trapped
7
+ -- enrollments, created before it shipped, do not have. 20260906000100 decides resumption
8
+ -- from evidence written at the ORIGINAL enrollment instead, so an unstamped row resumes
9
+ -- too.
10
+ --
11
+ -- Both directions are pinned here, and the SECURITY direction is the one that matters:
12
+ -- * same verified (issuer, subject) -> the existing identity is RETURNED (R-1, R-2);
13
+ -- * a DIFFERENT principal -> still raises, and the message names no handle (R-3, R-4).
14
+ -- Every allow-side assertion is `lives_ok(...)` followed by a separate state check, never
15
+ -- a bare `is((select ... from rpc(...)), ...)`, which aborts instead of failing (#113).
16
+ --
17
+ -- Failing calls are executed through pg_temp.try(), a BEGIN/EXCEPTION subtransaction that
18
+ -- records (sqlstate, message) into `_err` and records an explicit '00000 / (no error
19
+ -- raised)' row when the call SUCCEEDS -- so "the raise disappeared" fails an assertion
20
+ -- rather than silently matching nothing.
21
+ --
22
+ -- EC-1/EC-2 are the class assertions: the route class is enumerated from pg_proc, not
23
+ -- from a list written here, so a new fingerprint-accepting route or a new signing-key
24
+ -- creator fails and NAMES ITSELF.
25
+ create extension if not exists pgtap with schema extensions;
26
+ set search_path = extensions, public, agent_board;
27
+
28
+ begin;
29
+
30
+ select plan(35);
31
+
32
+ -- ---------------------------------------------------------------------------
33
+ -- Fixtures
34
+ -- ---------------------------------------------------------------------------
35
+ create temp table _v (name text primary key, val text) on commit drop;
36
+ create temp table _e (
37
+ label text, agent_id uuid, auth_user_id uuid, provider_id text, signing_key_id uuid,
38
+ granted text[], key_id text, key_version int, handle text, invite_redeemed boolean
39
+ ) on commit drop;
40
+ create temp table _err (label text primary key, state text, msg text) on commit drop;
41
+
42
+ -- The four resumption RESULTS, declared UP FRONT with the RETURNS TABLE shape rather
43
+ -- than created by `create temp table ... as <the call>`. If the call under test raises,
44
+ -- `lives_ok` records the failure and the CTAS form would leave the table MISSING -- so
45
+ -- every following assertion would abort the transaction instead of failing, and pgTAP
46
+ -- would emit fewer assertions than the plan with no `not ok` to read (#113). Declared
47
+ -- empty, those assertions return NULL and fail honestly.
48
+ create temp table _r1 (
49
+ agent_id uuid, auth_user_id uuid, provider_id text, signing_key_id uuid,
50
+ granted_scopes text[], key_id text, key_version int, agent_handle text,
51
+ invite_redeemed boolean
52
+ ) on commit drop;
53
+ create temp table _r2 (like _r1) on commit drop;
54
+ create temp table _r6 (like _r1) on commit drop;
55
+ create temp table _r13 (like _r1) on commit drop;
56
+
57
+
58
+ create function pg_temp.v(p_name text) returns text language sql stable as
59
+ $$ select val from _v where name = p_name $$;
60
+
61
+ -- The exact /enroll call, as SQL text. `p_actor_sub` is null and the kind is 'agent'
62
+ -- throughout, so the only inputs that vary across cases are the ones under test.
63
+ create function pg_temp.enroll_sql(
64
+ p_iss text, p_sub text, p_slug text, p_pub text, p_fp text, p_scopes text default '{board:send}'
65
+ ) returns text language sql stable as
66
+ $$
67
+ select format(
68
+ $q$select * from agent_board.enroll_federated_identity(%L,'message-board',%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent',%L,'{}')$q$,
69
+ p_iss, p_sub,
70
+ jsonb_build_object('iss', p_iss, 'aud', 'message-board', 'sub', p_sub)::text,
71
+ p_slug, p_pub, p_fp, p_scopes)
72
+ $$;
73
+
74
+ -- Run a call that is EXPECTED to raise, capturing sqlstate + message. A call that does
75
+ -- NOT raise records '00000', so a vanished raise fails loudly instead of matching an
76
+ -- absent row.
77
+ create function pg_temp.try(p_label text, p_sql text) returns void language plpgsql as
78
+ $$
79
+ begin
80
+ begin
81
+ execute p_sql;
82
+ insert into _err values (p_label, '00000', '(no error raised)');
83
+ exception when others then
84
+ insert into _err values (p_label, sqlstate, sqlerrm);
85
+ end;
86
+ end
87
+ $$;
88
+
89
+ do $$
90
+ declare
91
+ s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
92
+ v_iss1 text := 'https://res-a-' || replace(substr(gen_random_uuid()::text, 1, 8), '-', '') || '.test';
93
+ v_iss2 text;
94
+ v_pub text;
95
+ i int;
96
+ begin
97
+ v_iss1 := 'https://res-a-' || s || '.test';
98
+ v_iss2 := 'https://res-b-' || s || '.test';
99
+
100
+ insert into agent_board.identity_providers
101
+ (issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
102
+ values
103
+ (v_iss1, 'message-board', true, true, '{board:send,board:read}', '{board:x}', '{agent,human}'),
104
+ (v_iss2, 'message-board', true, true, '{board:send,board:read}', '{board:x}', '{agent,human}');
105
+
106
+ insert into _v values
107
+ ('s', s), ('iss1', v_iss1), ('iss2', v_iss2),
108
+ ('suba', 'portal|a-' || s), ('subb', 'portal|b-' || s);
109
+
110
+ -- Eight real Ed25519-shaped keys: 32 random bytes re-encoded through the SAME
111
+ -- canonical base64url form the enroll contract requires, and their in-DB fingerprints.
112
+ for i in 1..8 loop
113
+ v_pub := rtrim(translate(encode(gen_random_bytes(32), 'base64'), '+/', '-_'), '=');
114
+ insert into _v values ('pub' || i, v_pub), ('fp' || i, agent_board._raw_key_fingerprint(v_pub));
115
+ end loop;
116
+ end
117
+ $$;
118
+
119
+ -- Eight enrollments. E1/E3..E8 are then stripped of their replay stamp, which is exactly
120
+ -- the state every identity enrolled before 20260901000100 is in -- the #176 victim's
121
+ -- state. E2 keeps its stamp.
122
+ do $$
123
+ declare
124
+ i int;
125
+ v_slug text;
126
+ begin
127
+ for i in 1..8 loop
128
+ v_slug := 'res-' || i || '-' || pg_temp.v('s');
129
+ -- E2 is the STAMPED fixture and the only one that needs two scopes: the frozen
130
+ -- contract compares them as JSONB, which preserves ORDER, and R-14 pins that.
131
+ execute format('insert into _e select %L, * from (%s) t', 'E' || i,
132
+ pg_temp.enroll_sql(pg_temp.v('iss1'), pg_temp.v('suba'), v_slug,
133
+ pg_temp.v('pub' || i), pg_temp.v('fp' || i),
134
+ case when i = 2 then '{board:send,board:read}' else '{board:send}' end));
135
+ end loop;
136
+
137
+ update agent_board.agents a
138
+ set metadata = a.metadata #- '{federation,replay}'
139
+ where a.id in (select agent_id from _e where label <> 'E2');
140
+
141
+ -- E3: owned, unstamped, but the agent is revoked -> must NOT be revived by resumption.
142
+ update agent_board.agents a set status = 'revoked' where a.id = (select agent_id from _e where label = 'E3');
143
+
144
+ -- E4: evidence (i) broken -- owner_user_id repointed at an unrelated auth user.
145
+ update agent_board.agents a
146
+ set owner_user_id = agent_board._ensure_agent_auth_user('res-unrelated-owner-' || pg_temp.v('s'))
147
+ where a.id = (select agent_id from _e where label = 'E4');
148
+
149
+ -- E5: evidence (ii) broken -- the creation-time federation subject is gone.
150
+ update agent_board.agents a
151
+ set metadata = a.metadata #- '{federation,subject}'
152
+ where a.id = (select agent_id from _e where label = 'E5');
153
+
154
+ -- E6: evidence (iii) broken -- the original enrollment's audit row is gone.
155
+ delete from agent_board.enrollment_federation_audit e
156
+ where e.agent_id = (select agent_id from _e where label = 'E6');
157
+
158
+ -- E7: audit row present but recording no granted scopes -> ownership holds, the
159
+ -- identity is not resumable. This is what makes branch (b) reachable rather than
160
+ -- implied by the ownership conjunction.
161
+ update agent_board.enrollment_federation_audit e
162
+ set granted_scopes = null
163
+ where e.agent_id = (select agent_id from _e where label = 'E7');
164
+
165
+ -- E8: the ONE exemption in the ownership conjunction, modelled on the atomic-fleet
166
+ -- backfill fixture -- an agent that predates the owner model and carries NO
167
+ -- owner_user_id. Evidence (i) is unavailable there rather than contradicted.
168
+ update agent_board.agents a set owner_user_id = null
169
+ where a.id = (select agent_id from _e where label = 'E8');
170
+ end
171
+ $$;
172
+
173
+ -- ---------------------------------------------------------------------------
174
+ -- EC-1/EC-2 -- the class, enumerated from the catalog
175
+ -- ---------------------------------------------------------------------------
176
+
177
+ -- Every route a client can re-present an enrolled key to, taken from pg_proc.proargnames.
178
+ -- A fourth such route added later fails this and names itself in the diff.
179
+ select is(
180
+ (select string_agg(p.proname, ', ' order by p.proname)
181
+ from pg_proc p
182
+ join pg_namespace n on n.oid = p.pronamespace
183
+ where n.nspname = 'agent_board'
184
+ and p.proargnames && array['p_agent_public_key_fingerprint']),
185
+ '_enroll_federated_identity_once, enroll_federated_identity, onboard_federated_fleet_agent',
186
+ 'EC-1: exactly three agent_board routes accept an enrolled key fingerprint (pg_proc-derived)');
187
+
188
+ -- ...and only two functions in the whole schema can CREATE a signing key, so
189
+ -- onboard_federated_fleet_agent necessarily reaches an already-enrolled key through
190
+ -- enroll_federated_identity rather than deciding the duplicate case itself.
191
+ select is(
192
+ (select string_agg(p.proname, ', ' order by p.proname)
193
+ from pg_proc p
194
+ join pg_namespace n on n.oid = p.pronamespace
195
+ where n.nspname = 'agent_board'
196
+ and p.prokind = 'f'
197
+ and agent_board_test.strip_sql(p.prosrc) ~* 'insert into agent_board\.agent_signing_keys'),
198
+ '_enroll_federated_identity_once, mint_delegated_child',
199
+ 'EC-2: only the enroll creator and delegated-child mint insert signing keys (strip_sql-derived)');
200
+
201
+ -- ---------------------------------------------------------------------------
202
+ -- R-1 -- THE #176 FIX: an unstamped enrollment resumes for its owner
203
+ -- ---------------------------------------------------------------------------
204
+ select lives_ok(
205
+ (select 'insert into _r1 ' || pg_temp.enroll_sql(
206
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-1-' || pg_temp.v('s'),
207
+ pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send}')),
208
+ 'R-1: re-presenting an already-enrolled key as the same verified (issuer, subject) SUCCEEDS (unstamped row)');
209
+ select is(
210
+ (select agent_id from _r1),
211
+ (select agent_id from _e where label = 'E1'),
212
+ 'R-1: the resumption returns the EXISTING agent identity');
213
+ select is(
214
+ (select signing_key_id from _r1),
215
+ (select signing_key_id from _e where label = 'E1'),
216
+ 'R-1: the resumption returns the EXISTING signing key, not a new one');
217
+ select is(
218
+ (select agent_handle from _r1),
219
+ (select handle from _e where label = 'E1'),
220
+ 'R-1: the resumption returns the server-assigned handle the client lost');
221
+ select is(
222
+ (select provider_id from _r1),
223
+ (select provider_id from _e where label = 'E1'),
224
+ 'R-1: provider_id survives the unstamped path (it is read from the creation-time federation object)');
225
+ select is(
226
+ (select count(*)::int from agent_board.agent_signing_keys k where k.fingerprint = pg_temp.v('fp1')),
227
+ 1, 'R-1: the resumption enrolled no second key for that fingerprint');
228
+ select is(
229
+ (select count(*)::int from agent_board.agents a where a.id in (select agent_id from _e) or a.handle like 'res-1-%'),
230
+ 8, 'R-1: the resumption created no ninth agent');
231
+
232
+ -- ---------------------------------------------------------------------------
233
+ -- R-2 -- an unstamped resumption cannot ESCALATE
234
+ -- ---------------------------------------------------------------------------
235
+ select lives_ok(
236
+ (select 'insert into _r2 ' || pg_temp.enroll_sql(
237
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-1-different-slug',
238
+ pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send,board:read}')),
239
+ 'R-2: an unstamped resumption tolerates a different slug and a wider scope request');
240
+ select is(
241
+ (select granted_scopes from _r2),
242
+ '{board:send}'::text[],
243
+ 'R-2: it returns the ORIGINAL granted scopes -- the wider request grants nothing');
244
+ select is(
245
+ (select agent_id from _r2),
246
+ (select agent_id from _e where label = 'E1'),
247
+ 'R-2: and it still returns the original identity, not a second one');
248
+
249
+ -- ---------------------------------------------------------------------------
250
+ -- R-3 / R-4 -- THE SECURITY DIRECTION: a different principal is still refused
251
+ -- ---------------------------------------------------------------------------
252
+ select pg_temp.try('squat-subject', (select pg_temp.enroll_sql(
253
+ pg_temp.v('iss1'), pg_temp.v('subb'), 'res-squat-' || pg_temp.v('s'),
254
+ pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send}')));
255
+ -- sqlstate ALONE does not pin this: with the ownership refusal bypassed the request
256
+ -- falls through to the state gate, which raises the SAME 23505 for its own reason. The
257
+ -- assertion therefore requires the OWNERSHIP refusal specifically.
258
+ select ok(
259
+ (select state = '23505' and msg like '%to a different principal%'
260
+ from _err where label = 'squat-subject'),
261
+ 'R-3: a key owned by a DIFFERENT subject at the same issuer is refused AS a different principal (23505)');
262
+ select ok(
263
+ (select position(h in msg) = 0 from _err, (select handle h from _e where label = 'E1') x where label = 'squat-subject'),
264
+ 'R-3: the refusal names NO handle -- a squatter learns nothing about who holds the key');
265
+ select is(
266
+ (select count(*)::int from agent_board.agent_signing_keys k where k.fingerprint = pg_temp.v('fp1')),
267
+ 1, 'R-3: the refused squat left exactly one key on that fingerprint');
268
+
269
+ select pg_temp.try('squat-issuer', (select pg_temp.enroll_sql(
270
+ pg_temp.v('iss2'), pg_temp.v('suba'), 'res-squat2-' || pg_temp.v('s'),
271
+ pg_temp.v('pub1'), pg_temp.v('fp1'), '{board:send}')));
272
+ select ok(
273
+ (select state = '23505' and msg like '%to a different principal%'
274
+ from _err where label = 'squat-issuer'),
275
+ 'R-4: the same subject at a DIFFERENT approved issuer is refused as a different principal');
276
+ select ok(
277
+ (select position(h in msg) = 0 from _err, (select handle h from _e where label = 'E1') x where label = 'squat-issuer'),
278
+ 'R-4: that refusal names no handle either');
279
+
280
+ -- ---------------------------------------------------------------------------
281
+ -- R-5 / R-6 -- a STAMPED enrollment keeps its frozen contract; only the message improves
282
+ -- ---------------------------------------------------------------------------
283
+ select pg_temp.try('frozen', (select pg_temp.enroll_sql(
284
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-2-renamed',
285
+ pg_temp.v('pub2'), pg_temp.v('fp2'), '{board:send,board:read}')));
286
+ select is((select state from _err where label = 'frozen'), '23505',
287
+ 'R-5: a stamped enrollment re-presented by its owner with a DIFFERENT slug still raises (frozen contract intact)');
288
+ select ok(
289
+ (select position(h in msg) > 0 from _err, (select handle h from _e where label = 'E2') x where label = 'frozen'),
290
+ 'R-5: but the message now NAMES the existing handle');
291
+ select ok(
292
+ (select msg like '%need not be repeated%' from _err where label = 'frozen'),
293
+ 'R-5: and says enrollment need not be repeated -- the actionable half of the ask');
294
+
295
+ select lives_ok(
296
+ (select 'insert into _r6 ' || pg_temp.enroll_sql(
297
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-2-' || pg_temp.v('s'),
298
+ pg_temp.v('pub2'), pg_temp.v('fp2'), '{board:send,board:read}')),
299
+ 'R-6: an EXACT stamped replay still succeeds (the released path is unregressed)');
300
+ select is(
301
+ (select agent_id from _r6),
302
+ (select agent_id from _e where label = 'E2'),
303
+ 'R-6: and still returns the original identity');
304
+
305
+ -- ---------------------------------------------------------------------------
306
+ -- R-7 -- resumption never revives an identity that is not usable
307
+ -- ---------------------------------------------------------------------------
308
+ select pg_temp.try('revoked', (select pg_temp.enroll_sql(
309
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-3-' || pg_temp.v('s'),
310
+ pg_temp.v('pub3'), pg_temp.v('fp3'), '{board:send}')));
311
+ select is((select state from _err where label = 'revoked'), '23505',
312
+ 'R-7: an owned but REVOKED identity is not resumable');
313
+ select ok(
314
+ (select position(h in msg) > 0 from _err, (select handle h from _e where label = 'E3') x where label = 'revoked'),
315
+ 'R-7: its owner is told which handle is stuck');
316
+
317
+ -- ---------------------------------------------------------------------------
318
+ -- R-8/R-9/R-10 -- each of the three ownership evidences is load-bearing
319
+ -- ---------------------------------------------------------------------------
320
+ select pg_temp.try('no-owner-match', (select pg_temp.enroll_sql(
321
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-4-' || pg_temp.v('s'),
322
+ pg_temp.v('pub4'), pg_temp.v('fp4'), '{board:send}')));
323
+ select ok(
324
+ (select state = '23505' and msg like '%to a different principal%'
325
+ from _err where label = 'no-owner-match'),
326
+ 'R-8: evidence (i) -- owner_user_id not equal to the derived owner reads as NOT owned');
327
+
328
+ select pg_temp.try('no-creation-stamp', (select pg_temp.enroll_sql(
329
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-5-' || pg_temp.v('s'),
330
+ pg_temp.v('pub5'), pg_temp.v('fp5'), '{board:send}')));
331
+ select ok(
332
+ (select state = '23505' and msg like '%to a different principal%'
333
+ from _err where label = 'no-creation-stamp'),
334
+ 'R-9: evidence (ii) -- a missing creation-time federation subject reads as NOT owned (null is not a pass)');
335
+
336
+ select pg_temp.try('no-audit-row', (select pg_temp.enroll_sql(
337
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-6-' || pg_temp.v('s'),
338
+ pg_temp.v('pub6'), pg_temp.v('fp6'), '{board:send}')));
339
+ select ok(
340
+ (select state = '23505' and msg like '%to a different principal%'
341
+ from _err where label = 'no-audit-row'),
342
+ 'R-10: evidence (iii) -- a missing enrollment audit row reads as NOT owned');
343
+
344
+ -- ---------------------------------------------------------------------------
345
+ -- R-11 -- branch (b) is reachable on its own, not merely implied by ownership
346
+ -- ---------------------------------------------------------------------------
347
+ select pg_temp.try('no-granted-scopes', (select pg_temp.enroll_sql(
348
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-7-' || pg_temp.v('s'),
349
+ pg_temp.v('pub7'), pg_temp.v('fp7'), '{board:send}')));
350
+ select is((select state from _err where label = 'no-granted-scopes'), '23505',
351
+ 'R-11: an audit row recording NO granted scopes is owned but not resumable');
352
+ select ok(
353
+ (select position(h in msg) > 0 from _err, (select handle h from _e where label = 'E7') x where label = 'no-granted-scopes'),
354
+ 'R-11: and because ownership IS proven there, the owner is told the handle');
355
+
356
+ -- ---------------------------------------------------------------------------
357
+ -- R-12 -- the internal creator stays a hard refusal, but an actionable one
358
+ -- ---------------------------------------------------------------------------
359
+ select pg_temp.try('once-direct', (select format(
360
+ $q$select * from agent_board._enroll_federated_identity_once(%L,'message-board',%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
361
+ pg_temp.v('iss1'), pg_temp.v('suba'),
362
+ jsonb_build_object('iss', pg_temp.v('iss1'), 'aud', 'message-board', 'sub', pg_temp.v('suba'))::text,
363
+ 'res-once-' || pg_temp.v('s'), pg_temp.v('pub1'), pg_temp.v('fp1'))));
364
+ select is((select state from _err where label = 'once-direct'), '23505',
365
+ 'R-12: _enroll_federated_identity_once still refuses an already-enrolled key outright (no twin resumption)');
366
+ select ok(
367
+ (select position(h in msg) > 0 and msg like '%need not be repeated%'
368
+ from _err, (select handle h from _e where label = 'E1') x where label = 'once-direct'),
369
+ 'R-12: and its refusal now names the holder and says enrollment need not be repeated');
370
+
371
+ -- ---------------------------------------------------------------------------
372
+ -- R-13 -- the null-owner exemption, asserted POSITIVELY and in BOTH directions
373
+ -- ---------------------------------------------------------------------------
374
+ select lives_ok(
375
+ (select 'insert into _r13 ' || pg_temp.enroll_sql(
376
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-8-' || pg_temp.v('s'),
377
+ pg_temp.v('pub8'), pg_temp.v('fp8'), '{board:send}')),
378
+ 'R-13: a row with NO owner_user_id (pre-owner-model, the atomic-fleet backfill shape) still resumes for its owner');
379
+ select is(
380
+ (select agent_id from _r13),
381
+ (select agent_id from _e where label = 'E8'),
382
+ 'R-13: and it resumes to that same pre-owner-model identity');
383
+ select pg_temp.try('null-owner-squat', (select pg_temp.enroll_sql(
384
+ pg_temp.v('iss1'), pg_temp.v('subb'), 'res-8-squat-' || pg_temp.v('s'),
385
+ pg_temp.v('pub8'), pg_temp.v('fp8'), '{board:send}')));
386
+ select ok(
387
+ (select state = '23505' and msg like '%to a different principal%'
388
+ from _err where label = 'null-owner-squat'),
389
+ 'R-13: the exemption does NOT open a squat -- the same null-owner row refuses a different subject as one');
390
+
391
+ -- ---------------------------------------------------------------------------
392
+ -- R-14 -- the frozen scope comparison is ORDER-SENSITIVE, and that is asserted
393
+ -- rather than assumed
394
+ -- ---------------------------------------------------------------------------
395
+ -- `v_replay -> 'requestedScopes' is distinct from to_jsonb(v_requested_scopes)` is a
396
+ -- JSONB comparison and a jsonb array preserves order, so the SAME scopes in a different
397
+ -- order are a DIFFERENT request. That is pre-existing released behaviour of the frozen
398
+ -- contract (20260901000100), not something #176 introduced, and this migration
399
+ -- deliberately does not loosen it. It is pinned here because the offline MockEnroll
400
+ -- sorted its scope arrays and so disagreed with production silently; the client now
401
+ -- asserts the same property, and neither side can drift without one of them going red.
402
+ select pg_temp.try('frozen-scope-order', (select pg_temp.enroll_sql(
403
+ pg_temp.v('iss1'), pg_temp.v('suba'), 'res-2-' || pg_temp.v('s'),
404
+ pg_temp.v('pub2'), pg_temp.v('fp2'), '{board:read,board:send}')));
405
+ select ok(
406
+ (select state = '23505' and position(h in msg) > 0
407
+ from _err, (select handle h from _e where label = 'E2') x
408
+ where label = 'frozen-scope-order'),
409
+ 'R-14: the SAME frozen scopes REORDERED are a different frozen contract and raise, naming the handle');
410
+
411
+ select * from finish();
412
+ rollback;