@consiliency/agent-board-schema 1.60.0 → 1.63.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 (68) hide show
  1. package/README.md +80 -0
  2. package/fixtures/already-applied.json +12 -0
  3. package/migrations/20261011000100_agent_board_session_identities.sql +1964 -0
  4. package/migrations/20261012000100_agent_board_sidowner.sql +209 -0
  5. package/migrations/20261014000100_agent_board_grant_target_role.sql +218 -0
  6. package/package.json +1 -1
  7. package/schema-manifest.json +30 -3
  8. package/src/tooling.test.mjs +6 -2
  9. package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -4
  10. package/tests/agent_board_consent_emission.test.sql +30 -11
  11. package/tests/agent_board_consent_gating.test.sql +16 -2
  12. package/tests/agent_board_delegate_role_class.test.sql +1 -1
  13. package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
  14. package/tests/agent_board_delegation_enforcement.test.sql +1 -1
  15. package/tests/agent_board_enroll_resumption.test.sql +22 -4
  16. package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
  17. package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
  18. package/tests/agent_board_fk_lock_edges.test.sql +74 -14
  19. package/tests/agent_board_grant_target_role.test.sql +1118 -0
  20. package/tests/agent_board_invite_no_downgrade.test.sql +10 -8
  21. package/tests/agent_board_ledger_authority.test.sql +70 -12
  22. package/tests/agent_board_membership_current_row_class.test.sql +9 -1
  23. package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
  24. package/tests/agent_board_mint_harden.test.sql +38 -27
  25. package/tests/agent_board_rolereset.test.sql +8 -2
  26. package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
  27. package/tests/agent_board_rowid.test.sql +3 -1
  28. package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
  29. package/tests/agent_board_session_identity_backfill.test.sql +82 -0
  30. package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
  31. package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
  32. package/tests/agent_board_session_identity_class.test.sql +118 -0
  33. package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
  34. package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
  35. package/tests/agent_board_session_identity_lease.test.sql +235 -0
  36. package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
  37. package/tests/agent_board_session_identity_limits.test.sql +208 -0
  38. package/tests/agent_board_session_identity_lineage.test.sql +64 -0
  39. package/tests/agent_board_session_identity_link.test.sql +201 -0
  40. package/tests/agent_board_session_identity_mint.test.sql +233 -0
  41. package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
  42. package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
  43. package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
  44. package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
  45. package/tests/agent_board_sidowner_leases.test.sql +107 -0
  46. package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
  47. package/tests/agent_board_sidowner_read_class.test.sql +171 -0
  48. package/tests/agent_board_sidowner_reads.test.sql +136 -0
  49. package/tests/agent_board_signer_lifecycle.test.sql +26 -4
  50. package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
  51. package/tests/agent_board_write_enforcement_class.test.sql +74 -15
  52. package/tests/catalog/function_execute_privileges.json +218 -2
  53. package/tests/catalog/p_metadata_parameters.json +98 -2
  54. package/tests/isolation/expected/grant_target_role_race.out +348 -0
  55. package/tests/isolation/expected/session_identity_limits.out +165 -0
  56. package/tests/isolation/expected/session_identity_links.out +235 -0
  57. package/tests/isolation/expected/session_identity_reclaim.out +253 -0
  58. package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
  59. package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
  60. package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
  61. package/tests/isolation/specs/grant_target_role_race.spec +207 -0
  62. package/tests/isolation/specs/session_identity_limits.spec +81 -0
  63. package/tests/isolation/specs/session_identity_links.spec +80 -0
  64. package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
  65. package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
  66. package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
  67. package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +108 -0
  68. package/tests/seeds/20261011000100_agent_board_session_identities.sql.pre.sql +47 -0
@@ -17,7 +17,7 @@ set search_path = extensions, public, agent_board;
17
17
 
18
18
  begin;
19
19
 
20
- select plan(54);
20
+ select plan(55);
21
21
 
22
22
  create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
23
23
  begin
@@ -350,11 +350,25 @@ select is(
350
350
  'invite_board_member', -- writes `invited` only -- an offer, not a membership
351
351
  'request_join_board', -- writes `requested` only
352
352
  'leave_board', -- writes `left` only
353
- 'grant_board_role' -- requires an already-active membership; never SETs the state
353
+ 'grant_board_role', -- requires an already-active membership; never SETs the state
354
+ 'readmit_session_identity' -- human owner mints a NEW principal; positively pinned below and behaviorally in SID readmission
354
355
  )),
355
356
  '',
356
357
  'EC-4 CLASS: every authenticated route reaching an active membership is gated or exempt');
357
358
 
359
+ select ok(
360
+ agent_board_test.stripped_body('agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval)'::regprocedure)
361
+ ~ agent_board_test.call_re('_session_identity_owner', false)
362
+ and agent_board_test.stripped_body('agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval)'::regprocedure)
363
+ ~ agent_board_test.stmt_re('insert into', 'agents', false)
364
+ and agent_board_test.stripped_body('agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval)'::regprocedure, true)
365
+ ~ agent_board_test.re(agent_board_test.call_frag('_upsert_board_membership_state', false) || '\s*v_board\s*,\s*v_new_id\s*,\s*''joined''')
366
+ and agent_board_test.stripped_body('agent_board._session_identity_owner(uuid)'::regprocedure, true)
367
+ like '%human.agent_type = ''human''%'
368
+ and agent_board_test.stripped_body('agent_board._session_identity_owner(uuid)'::regprocedure)
369
+ like '%root.owner_user_id = auth.uid()%',
370
+ 'EC-4-readmit: human-root-owner placement is onto a fresh principal, never manufactured target consent');
371
+
358
372
  -- ...and the exemptions are not assertions of convenience: the three "writes only X" claims are
359
373
  -- checked, because an exemption that stops being true is how the fifth route hid.
360
374
  -- Matches the literal passed to EITHER the shared sink OR update_board_membership. The first
@@ -301,7 +301,7 @@ select f.oid, f.name,
301
301
  -- 24
302
302
  select is(
303
303
  (select string_agg(name || case when writes_role then '[role]' else '' end, ',' order by name) from _bm_writers),
304
- '_fedsession_autojoin_default_board[role],_redeem_unbounded_invite_for_agent[role],bootstrap_board_owner_memberships[role],create_board[role],grant_board_role[role]',
304
+ '_fedsession_autojoin_default_board[role],_redeem_unbounded_invite_for_agent[role],bootstrap_board_owner_memberships[role],create_board[role],grant_board_role[role],link_session_alias[role],mint_delegated_child[role],readmit_session_identity[role],set_board_member_label',
305
305
  'the direct board_memberships writers outside the sink, derived (a new one names itself)');
306
306
  -- 25
307
307
  select is(
@@ -13,8 +13,8 @@
13
13
  -- * REQUIRE DURABILITY (invariant 4): an operator require=true is not relaxed to false by
14
14
  -- self-service (the self-pin), so a keyless self-service mint is refused — mutate the pin
15
15
  -- (true -> coalesce(p_require,true)) and both the state read and the keyless-mint throw flip.
16
- -- * OPERATOR LATITUDE (invariant 5): the operator path is unaffected — it honors the stored
17
- -- caps and can still set/loosen its own ceiling.
16
+ -- * SIDSCHEMA: both mint paths consume the tighter operator ceiling. The operator
17
+ -- can still set/loosen that ceiling through its existing writer path.
18
18
  -- * NO REGRESSION: a root with no operator policy self-services bounded only by the global
19
19
  -- 25/24h ceiling.
20
20
  -- * EXECUTE class: CREATE OR REPLACE (no DROP) kept the ACL — mint stays service_role-only,
@@ -31,7 +31,11 @@ set search_path = extensions, public, agent_board;
31
31
 
32
32
  begin;
33
33
 
34
- select plan(40);
34
+ select plan(42);
35
+ create temp table _a_opcap(child_agent_id uuid, grant_expires_at timestamptz, signing_key_id uuid);
36
+ create temp table _b_ssttl(like _a_opcap);
37
+ create temp table _b_opttl(like _a_opcap);
38
+ create temp table _d_ss(like _a_opcap);
35
39
 
36
40
  create temp table _cf (
37
41
  owner_id uuid,
@@ -148,12 +152,19 @@ select throws_ok(
148
152
  '42501', 'max_active_children: root has reached its active delegated-child ceiling',
149
153
  'A: self-service mint clamps max_active_children to the operator ceiling of 1 (stored 25 NOT trusted)');
150
154
 
151
- -- Operator path (p_self_service=false) honors the stored 25 and mints despite the 1 live grant
152
- -- (operator latitude). require=true after the self-service pin, so supply a key.
155
+ -- SIDSCHEMA's operator ceiling bounds BOTH mint paths, not merely self-service.
156
+ select throws_ok(
157
+ (select format($$ select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, false) $$,
158
+ cap, 'cf-opfull-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k1) from _cf),
159
+ '42501', 'max_active_children: root has reached its active delegated-child ceiling',
160
+ 'A: operator mint cannot bypass its tighter stored operator ceiling');
161
+ select is((select count(*) from _a_opcap), 0::bigint,
162
+ 'A: operator ceiling refusal leaves no returned child');
163
+ update agent_board.delegation_grants set revoked_at = now() where child_agent_id = (select cap_child from _cf);
153
164
  select lives_ok(
154
- (select format($$ create temp table _a_opcap as select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, false) $$,
165
+ (select format($$ insert into _a_opcap select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, false) $$,
155
166
  cap, 'cf-opcap-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k1) from _cf),
156
- 'A: operator mint honors the stored 25 (1 live grant < 25 => mints; latitude)');
167
+ 'A: operator mint succeeds below the tighter ceiling (0 active grants < 1)');
157
168
  select is((select count(*) from _a_opcap), 1::bigint,
158
169
  'A: operator cap mint produced exactly one child (not a silent zero-row pass)');
159
170
 
@@ -185,21 +196,21 @@ select is((select child_ttl_ceiling from agent_board.delegation_policies where r
185
196
  select set_config('request.jwt.claim.role', '', true);
186
197
  select set_config('request.jwt.claim.sub', '', true);
187
198
  select lives_ok(
188
- (select format($$ create temp table _b_ssttl as select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, true) $$,
199
+ (select format($$ insert into _b_ssttl select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, true) $$,
189
200
  ttl, 'cf-ssttl-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k2) from _cf),
190
201
  'B: self-service mint under the 2h operator ceiling lives');
191
202
  select ok(
192
203
  (select grant_expires_at <= now() + interval '2 hours' + interval '1 minute' from _b_ssttl),
193
204
  'B: self-service child TTL clamped to the 2h operator ceiling (stored 24h NOT trusted)');
194
205
 
195
- -- Operator path honors the stored 24h ceiling (NOT clamped to the 2h operator ceiling: latitude).
206
+ -- The operator mint also consumes the durable 2h ceiling, per SIDSCHEMA's matrix.
196
207
  select lives_ok(
197
- (select format($$ create temp table _b_opttl as select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, false) $$,
208
+ (select format($$ insert into _b_opttl select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, false) $$,
198
209
  ttl, 'cf-opttl-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k3) from _cf),
199
210
  'B: operator mint under the same policy lives');
200
211
  select ok(
201
- (select grant_expires_at > now() + interval '3 hours' from _b_opttl),
202
- 'B: operator path honors the stored 24h ceiling (not clamped to the 2h operator ceiling; latitude)');
212
+ (select grant_expires_at <= now() + interval '2 hours' + interval '1 minute' from _b_opttl),
213
+ 'B: operator mint consumes the same tighter 2h operator ceiling');
203
214
 
204
215
  -- ===========================================================================
205
216
  -- C. Operator require_child_signing_key=true is durable: self-service cannot relax it
@@ -251,7 +262,7 @@ select is((select operator_child_ttl_ceiling from agent_board.delegation_policie
251
262
  select set_config('request.jwt.claim.role', '', true);
252
263
  select set_config('request.jwt.claim.sub', '', true);
253
264
  select lives_ok(
254
- (select format($$ create temp table _d_ss as select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, true) $$,
265
+ (select format($$ insert into _d_ss select * from agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, %L, null, true) $$,
255
266
  none, 'cf-ssnone-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k4) from _cf),
256
267
  'D: self-service mint with no operator policy lives');
257
268
  select ok(
@@ -284,7 +295,7 @@ select is((select scope_attestation from agent_board.delegation_policies where r
284
295
  -- re-opened to PUBLIC/anon.
285
296
  -- ===========================================================================
286
297
  select ok(not has_function_privilege('authenticated',
287
- 'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean)', 'EXECUTE'),
298
+ 'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean, text, text, text, jsonb, jsonb, uuid)', 'EXECUTE'),
288
299
  'F: mint_delegated_child stays service_role-only (authenticated has NO EXECUTE after recreate)');
289
300
  select ok(not has_function_privilege('anon',
290
301
  'agent_board.upsert_delegation_policy(uuid, text, jsonb, integer, interval, boolean)', 'EXECUTE'),
@@ -293,7 +304,7 @@ select ok(has_function_privilege('authenticated',
293
304
  'agent_board.upsert_delegation_policy(uuid, text, jsonb, integer, interval, boolean)', 'EXECUTE'),
294
305
  'F: authenticated retains EXECUTE on upsert_delegation_policy after recreate');
295
306
  select ok(has_function_privilege('service_role',
296
- 'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean)', 'EXECUTE'),
307
+ 'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean, text, text, text, jsonb, jsonb, uuid)', 'EXECUTE'),
297
308
  'F: service_role retains EXECUTE on mint_delegated_child');
298
309
 
299
310
  select set_config('request.jwt.claim.role', '', true);
@@ -87,7 +87,7 @@ end
87
87
  $$;
88
88
 
89
89
  -- Function existence.
90
- select has_function('agent_board', 'mint_delegated_child', array['uuid','text','jsonb','text','interval','boolean'], 'mint_delegated_child exists');
90
+ select has_function('agent_board', 'mint_delegated_child', array['uuid','text','jsonb','text','interval','boolean','text','text','text','jsonb','jsonb','uuid'], 'mint_delegated_child exists');
91
91
  select has_function('agent_board', 'renew_delegated_grant', array['uuid','interval'], 'renew_delegated_grant exists');
92
92
 
93
93
  -- Happy mint: grant row shape.
@@ -27,7 +27,7 @@ set search_path = extensions, public, agent_board;
27
27
 
28
28
  begin;
29
29
 
30
- select plan(35);
30
+ select plan(37);
31
31
 
32
32
  -- ---------------------------------------------------------------------------
33
33
  -- Fixtures
@@ -185,7 +185,10 @@ select is(
185
185
  '_enroll_federated_identity_once, enroll_federated_identity, onboard_federated_fleet_agent',
186
186
  'EC-1: exactly three agent_board routes accept an enrolled key fingerprint (pg_proc-derived)');
187
187
 
188
- -- ...and only two functions in the whole schema can CREATE a signing key, so
188
+ -- Readmission creates a fresh pending key for a verified human owner, never an
189
+ -- enrollment resumption. The exemption is positively checked below and exercised
190
+ -- with a real human owner and proof challenge in the SIDSCHEMA readmission suite.
191
+ -- The other creators still reach an enrolled key through the enroll wrapper, so
189
192
  -- onboard_federated_fleet_agent necessarily reaches an already-enrolled key through
190
193
  -- enroll_federated_identity rather than deciding the duplicate case itself.
191
194
  select is(
@@ -201,8 +204,23 @@ select is(
201
204
  -- invisible to the #196 lint because that lint only inspects POSITIONAL and
202
205
  -- NEGATIVE assertions, and this is a PRESENCE match -- see the note on the lint.
203
206
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into', 'agent_signing_keys')),
204
- '_enroll_federated_identity_once, mint_delegated_child',
205
- 'EC-2: only the enroll creator and delegated-child mint insert signing keys (strip_sql-derived)');
207
+ '_enroll_federated_identity_once, mint_delegated_child, readmit_session_identity',
208
+ 'EC-2: only enroll, delegated-child mint and fresh readmission insert signing keys (strip_sql-derived)');
209
+ select is(
210
+ (select string_agg(r || '=' || has_function_privilege(r, p.oid, 'EXECUTE')::text, ' ' order by r)
211
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace,
212
+ unnest(array['anon', 'authenticated', 'public', 'service_role']) r
213
+ where n.nspname = 'agent_board' and p.proname = 'readmit_session_identity'),
214
+ 'anon=false authenticated=true public=false service_role=false',
215
+ 'EC-2: readmission is human-authenticated only, not a service enrollment route');
216
+ select ok(
217
+ (select count(*) = 1 and bool_and(
218
+ agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_session_identity_owner')
219
+ -- Keep literals only because these two key-state values are the markers.
220
+ and agent_board_test.stripped_body(p.oid, true) ~ $$'pending_proof'\s*,\s*'unproven'$$)
221
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
222
+ where n.nspname = 'agent_board' and p.proname = 'readmit_session_identity'),
223
+ 'EC-2: readmission positively verifies the owner and writes a pending, unproven fresh key');
206
224
 
207
225
  -- ---------------------------------------------------------------------------
208
226
  -- R-1 -- THE #176 FIX: an unstamped enrollment resumes for its owner
@@ -19,7 +19,7 @@ set search_path = extensions, public, agent_board;
19
19
 
20
20
  begin;
21
21
 
22
- select plan(66);
22
+ select plan(68);
23
23
 
24
24
  -- ---------------------------------------------------------------------------
25
25
  -- Fixtures
@@ -452,8 +452,23 @@ select is(
452
452
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
453
453
  where n.nspname = 'agent_board' and p.prokind = 'f'
454
454
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into', 'agent_signing_keys')),
455
- '_enroll_federated_identity_once, mint_delegated_child',
456
- 'EC-T2: exactly two functions insert signing keys (the EC-2 form)');
455
+ '_enroll_federated_identity_once, mint_delegated_child, readmit_session_identity',
456
+ 'EC-T2: exactly three functions insert signing keys, including fresh readmission (the EC-2 form)');
457
+ select is(
458
+ (select string_agg(r || '=' || has_function_privilege(r, p.oid, 'EXECUTE')::text, ' ' order by r)
459
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace,
460
+ unnest(array['anon', 'authenticated', 'public', 'service_role']) r
461
+ where n.nspname = 'agent_board' and p.proname = 'readmit_session_identity'),
462
+ 'anon=false authenticated=true public=false service_role=false',
463
+ 'EC-T2: readmission is human-authenticated only, not a Portal-token enrollment route');
464
+ select ok(
465
+ (select count(*) = 1 and bool_and(
466
+ agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_session_identity_owner')
467
+ -- Keep literals only because these two key-state values are the markers.
468
+ and agent_board_test.stripped_body(p.oid, true) ~ $$'pending_proof'\s*,\s*'unproven'$$)
469
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
470
+ where n.nspname = 'agent_board' and p.proname = 'readmit_session_identity'),
471
+ 'EC-T2: readmission positively verifies the owner and writes a pending, unproven fresh key');
457
472
  select is(
458
473
  (select string_agg(r || '=' || has_function_privilege(r, p.oid, 'EXECUTE')::text, ' ' order by r)
459
474
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace,
@@ -233,8 +233,8 @@ select is(
233
233
  (select string_agg(split_part(sig, '(', 1), ', ' order by split_part(sig, '(', 1))
234
234
  from _lockclass where reaches_board and reaches_owner),
235
235
  '_enroll_federated_human_after_revocation_gate, _enroll_federated_identity_once, '
236
- || 'enroll_federated_human, enroll_federated_identity, onboard_federated_fleet_agent',
237
- 'EC-2: the board+owner lock class is exactly the five federated write routes');
236
+ || 'enroll_federated_human, enroll_federated_identity, mint_delegated_child, onboard_federated_fleet_agent, readmit_session_identity',
237
+ 'EC-2: the board+owner lock class includes the federated routes and both SID mints');
238
238
 
239
239
  -- EC-3: THE ORDERING INVARIANT. Never acquire auth.users before boards. The failure
240
240
  -- prints the offending signatures (pre-fix this named enroll_federated_identity and
@@ -40,7 +40,7 @@ set search_path = extensions, public, agent_board;
40
40
 
41
41
  begin;
42
42
 
43
- select plan(18);
43
+ select plan(21);
44
44
 
45
45
  -- ---------------------------------------------------------------------------
46
46
  -- The derivation, materialized once. `first_path` is the position PATH of the
@@ -49,7 +49,46 @@ select plan(18);
49
49
  -- function takes on that relation anywhere, which over-reports conflicts rather
50
50
  -- than under-reporting them.
51
51
  -- ---------------------------------------------------------------------------
52
- create temp table _le on commit drop as select * from agent_board_test.lock_edges();
52
+ create temp table _le_raw on commit drop as select * from agent_board_test.lock_edges();
53
+ -- The fixture conservatively assigns every target-table FK to every INSERT.
54
+ -- Bound these two exemptions by the executable column lists AND the catalog:
55
+ -- every INSERT must use an explicit list, omit all nullable/default-free FK
56
+ -- columns, and have no BEFORE INSERT trigger that could populate them. UPDATE
57
+ -- edges and any new writer/constructor remain in the graph.
58
+ create temp table _null_fk_insert on commit drop as
59
+ with candidates as (
60
+ select distinct e.origin_oid, e.origin_name, e.rel_oid, e.path[1] as pos,
61
+ c.oid as constraint_oid, c.conname, c.conrelid, c.conkey,
62
+ cl.relname, agent_board_test.stripped_body(e.origin_oid) as body
63
+ from _le_raw e join pg_constraint c on e.via='fk-insert:' || c.conname and c.confrelid=e.rel_oid
64
+ join pg_class cl on cl.oid=c.conrelid
65
+ where e.own and e.origin_name in ('_session_identity_agent_lineage','_revoke_agent_principal')
66
+ ), proven as (
67
+ select c.*, (select count(*) from regexp_matches(c.body,
68
+ agent_board_test.re(agent_board_test.stmt_frag('insert into', c.relname, true)
69
+ || '\s*(\([^)]*\))?'), 'g')) as insert_count
70
+ from candidates c
71
+ where not exists (select 1 from unnest(c.conkey) k join pg_attribute a
72
+ on a.attrelid=c.conrelid and a.attnum=k where a.attnotnull or a.atthasdef)
73
+ and not exists (select 1 from pg_trigger t where t.tgrelid=c.conrelid
74
+ and not t.tgisinternal and (t.tgtype & 2)=2 and (t.tgtype & 4)=4)
75
+ and not exists (select 1 from regexp_matches(c.body,
76
+ agent_board_test.re(agent_board_test.stmt_frag('insert into', c.relname, true)
77
+ || '\s*(\([^)]*\))?'), 'g') m
78
+ where m[1] is null or exists (select 1 from unnest(c.conkey) k join pg_attribute a
79
+ on a.attrelid=c.conrelid and a.attnum=k
80
+ where a.attname = any(string_to_array(trim(both '()' from
81
+ translate(lower(m[1]), ' "' || chr(9) || chr(10) || chr(13), '')), ','))))
82
+ )
83
+ select * from proven where insert_count>0;
84
+ create temp table _le on commit drop as
85
+ select e.* from _le_raw e where not exists (
86
+ select 1 from _null_fk_insert n where n.origin_oid=e.origin_oid and n.rel_oid=e.rel_oid
87
+ and n.pos=e.path[cardinality(e.path)]
88
+ and not exists (select 1 from _le_raw kept where kept.own and kept.origin_oid=e.origin_oid
89
+ and kept.rel_oid=e.rel_oid and kept.path[1]=n.pos
90
+ and not exists (select 1 from _null_fk_insert omitted where omitted.origin_oid=kept.origin_oid
91
+ and omitted.rel_oid=kept.rel_oid and omitted.pos=kept.path[1] and kept.via='fk-insert:' || omitted.conname)));
53
92
  create temp table _cl on commit drop as select * from agent_board_test.fn_call_closure();
54
93
 
55
94
  create temp table _acq on commit drop as
@@ -187,8 +226,28 @@ select is(
187
226
  join pg_class cl on cl.oid = t.tgrelid
188
227
  join pg_namespace n on n.oid = cl.relnamespace
189
228
  where not t.tgisinternal and n.nspname = 'agent_board')),
190
- '',
191
- 'FKL-8: no trigger function on an agent_board table takes a lock edge -- trigger locks are NOT folded in');
229
+ '_session_identity_agent_lineage',
230
+ 'FKL-8: the catalog-derived locking trigger class is exactly the positively bounded fresh legacy-identity constructor; new trigger locks name themselves');
231
+
232
+ -- This trigger may INSERT only the identity for NEW.id on an agents INSERT,
233
+ -- never another ancestor and never on UPDATE. Its own-agent FK is already
234
+ -- owned by the insert; parent/root KEY SHARE locks are the same FK targets the
235
+ -- agents INSERT already acquired. The constructor is exercised positively by
236
+ -- agent_board_session_identity_lineage.test.sql, including session-kind retention.
237
+ select is((select string_agg(distinct e.rel_name || ' ' || e.lock_mode || ' via ' || e.via, ' | '
238
+ order by e.rel_name || ' ' || e.lock_mode || ' via ' || e.via)
239
+ from _le e where e.fn_name='_session_identity_agent_lineage' and e.own),
240
+ 'agent_board.agents key share via fk-insert:session_identities_agent_id_fkey | agent_board.agents key share via fk-insert:session_identities_parent_agent_id_fkey | agent_board.agents key share via fk-insert:session_identities_root_agent_id_fkey',
241
+ 'FKL-8-constructor-positive: every actual constructor FK lock is on its already inserted agent or the same parent/root agents targets; it has no independent identity update lock');
242
+
243
+ select is((select string_agg(conname || ':' || insert_count, ', ' order by conname)
244
+ from _null_fk_insert where origin_name='_session_identity_agent_lineage'),
245
+ 'session_identities_forked_from_fkey:1, session_identities_lease_alias_fkey:1, session_identities_readmitted_from_fkey:1',
246
+ 'FKL-8-constructor-null-positive: all three nullable/default-free continuity columns are omitted from every constructor INSERT and no BEFORE INSERT trigger can populate them');
247
+ select is((select string_agg(conname || ':' || insert_count, ', ' order by conname)
248
+ from _null_fk_insert where origin_name='_revoke_agent_principal'),
249
+ 'fleet_revocation_events_harness_registration_id_fkey:2, fleet_revocation_events_machine_id_fkey:2, fleet_revocation_events_repo_scope_id_fkey:2, fleet_revocation_events_trust_domain_id_fkey:2',
250
+ 'FKL-8-revoke-null-positive: every principal ledger INSERT omits all four nullable/default-free fleet links with no BEFORE INSERT trigger; real FK edges are retained');
192
251
 
193
252
  -- ===========================================================================
194
253
  -- FKL-9: THE CLASS section 9 could not compute. Every function whose lock graph
@@ -239,8 +298,13 @@ select is(
239
298
  'invite_board_member: memberships(update) then agents(key share) [unserialised]',
240
299
  'join_board: memberships(update) then agents(key share) [unserialised]',
241
300
  'leave_board: memberships(update) then agents(key share) [unserialised]',
301
+ -- SIDSCHEMA role writers use the existing consent sink and board mutex.
302
+ -- Their installed lock order is asserted, not exempted from the class.
303
+ 'link_session_alias: agents(update) then memberships(update) [advisory]',
304
+ 'mint_delegated_child: agents(update) then memberships(update) [advisory]',
242
305
  'onboard_federated_fleet_agent: agents(update) then memberships(update) [advisory]',
243
306
  'post_board_message: agents(key share) then memberships(key share) [unserialised]',
307
+ 'readmit_session_identity: agents(update) then memberships(update) [advisory]',
244
308
  'redeem_invite_link: memberships(key share) then agents(key share) [unserialised]',
245
309
  'request_join_board: memberships(update) then agents(key share) [unserialised]',
246
310
  'resolve_board_request: memberships(update) then agents(key share) [unserialised]',
@@ -252,12 +316,9 @@ select is(
252
316
  'signed_send_direct_message: agents(key share) then memberships(key share) [unserialised]',
253
317
  'signed_send_multicast_message: agents(key share) then memberships(key share) [unserialised]',
254
318
  'subscribe_callback_endpoint: memberships(key share) then agents(key share) [unserialised]',
255
- -- HOSTED (20261008000100). Unserialised, and deliberately NOT given the advisory lock:
256
- -- add_delegate_to_board takes the instance row and THEN the advisory lock, so a tombstone
257
- -- (instance row FOR UPDATE first) taking the advisory lock after it would be the ABBA this
258
- -- file exists to catch. Every row it locks belongs to the tombstoned delegate -- its agents
259
- -- row, its credentials, its memberships -- which onboarding never locks.
260
- 'tombstone_delegate_instance: agents(update) then memberships(update) [unserialised]',
319
+ -- SIDSCHEMA integrity repair: board mutexes precede owner/instance and family row locks,
320
+ -- matching add_delegate_to_board's board-before-instance order. No withdrawal exemption.
321
+ 'tombstone_delegate_instance: agents(update) then memberships(update) [advisory]',
261
322
  'update_board_membership: memberships(update) then agents(key share) [unserialised]'
262
323
  ]::text[],
263
324
  'FKL-9: the agents+board_memberships lock class, FK edges included (section 9''s text scan sees 3 of these)');
@@ -267,6 +328,9 @@ select is(
267
328
  -- orders with CONFLICTING modes and no shared advisory lock. Pinned at the
268
329
  -- relation-pair level rather than the function-pair level (146 of those today)
269
330
  -- because a pair of tables entering this set is the reviewable event.
331
+ -- SIDSCHEMA's positively proved NULL-insert filtering above removes four
332
+ -- previously inferred fleet-machine/trust-domain pairs: PostgreSQL cannot take
333
+ -- an FK row lock for a NULL reference. No new relation pair is exempted.
270
334
  --
271
335
  -- KNOWN-BAD and deliberately not fixed -- see the file header. ONE pair has been
272
336
  -- removed since: #281 R1 fixed `agent_board.tasks <-> agent_board.threads` by
@@ -340,14 +404,12 @@ select is(
340
404
  'agent_board.agent_signing_keys <-> agent_board.agents',
341
405
  'agent_board.agent_signing_keys <-> agent_board.board_memberships',
342
406
  'agent_board.agent_signing_keys <-> agent_board.fleet_machine_agents',
343
- 'agent_board.agent_signing_keys <-> agent_board.fleet_machines',
344
407
  'agent_board.agent_signing_keys <-> agent_board.fleet_trust_domains',
345
408
  'agent_board.agents <-> agent_board.board_invite_tokens',
346
409
  'agent_board.agents <-> agent_board.board_memberships',
347
410
  'agent_board.agents <-> agent_board.boards',
348
411
  'agent_board.agents <-> agent_board.enroll_token_uses',
349
412
  'agent_board.agents <-> agent_board.fleet_machine_agents',
350
- 'agent_board.agents <-> agent_board.fleet_machines',
351
413
  'agent_board.agents <-> agent_board.fleet_trust_domains',
352
414
  'agent_board.agents <-> agent_board.identity_providers',
353
415
  'agent_board.agents <-> auth.users',
@@ -361,8 +423,6 @@ select is(
361
423
  'agent_board.fleet_harness_registrations <-> agent_board.fleet_machine_agents',
362
424
  'agent_board.fleet_machine_agents <-> agent_board.fleet_repo_scopes',
363
425
  'agent_board.fleet_machine_agents <-> agent_board.fleet_trust_domains',
364
- 'agent_board.fleet_machines <-> auth.users',
365
- 'agent_board.fleet_trust_domains <-> auth.users',
366
426
  'agent_board.identity_providers <-> auth.users'
367
427
  ]::text[],
368
428
  'FKL-10: the relation pairs held in opposite orders with conflicting modes and no shared advisory lock');