@consiliency/agent-board-schema 1.60.0 → 1.64.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 (100) hide show
  1. package/README.md +86 -0
  2. package/fixtures/already-applied.json +16 -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/migrations/20261014000200_agent_board_last_owner.sql +239 -0
  7. package/package.json +1 -1
  8. package/schema-manifest.json +39 -3
  9. package/src/tooling.test.mjs +10 -2
  10. package/tests/agent_board_atomic_fleet_onboarding.test.sql +15 -4
  11. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +7 -0
  12. package/tests/agent_board_consent_emission.test.sql +30 -11
  13. package/tests/agent_board_consent_gating.test.sql +16 -2
  14. package/tests/agent_board_delegate_role_class.test.sql +1 -1
  15. package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
  16. package/tests/agent_board_delegation_enforcement.test.sql +1 -1
  17. package/tests/agent_board_enroll_resumption.test.sql +22 -4
  18. package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
  19. package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
  20. package/tests/agent_board_fk_lock_edges.test.sql +77 -16
  21. package/tests/agent_board_grant_target_role.test.sql +1118 -0
  22. package/tests/agent_board_hosted_delegates.test.sql +5 -3
  23. package/tests/agent_board_invite_no_downgrade.test.sql +15 -11
  24. package/tests/agent_board_ledger_authority.test.sql +77 -12
  25. package/tests/agent_board_membership_current_row_class.test.sql +9 -1
  26. package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
  27. package/tests/agent_board_mint_harden.test.sql +38 -27
  28. package/tests/agent_board_role_guard.test.sql +319 -0
  29. package/tests/agent_board_rolereset.test.sql +25 -29
  30. package/tests/agent_board_roomdeleg.test.sql +8 -0
  31. package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
  32. package/tests/agent_board_roomown.test.sql +3 -1
  33. package/tests/agent_board_rowid.test.sql +7 -2
  34. package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
  35. package/tests/agent_board_session_identity_backfill.test.sql +82 -0
  36. package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
  37. package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
  38. package/tests/agent_board_session_identity_class.test.sql +118 -0
  39. package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
  40. package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
  41. package/tests/agent_board_session_identity_lease.test.sql +235 -0
  42. package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
  43. package/tests/agent_board_session_identity_limits.test.sql +208 -0
  44. package/tests/agent_board_session_identity_lineage.test.sql +64 -0
  45. package/tests/agent_board_session_identity_link.test.sql +201 -0
  46. package/tests/agent_board_session_identity_mint.test.sql +233 -0
  47. package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
  48. package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
  49. package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
  50. package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
  51. package/tests/agent_board_sidowner_leases.test.sql +107 -0
  52. package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
  53. package/tests/agent_board_sidowner_read_class.test.sql +171 -0
  54. package/tests/agent_board_sidowner_reads.test.sql +136 -0
  55. package/tests/agent_board_signer_lifecycle.test.sql +26 -4
  56. package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
  57. package/tests/agent_board_write_enforcement_class.test.sql +74 -15
  58. package/tests/catalog/function_execute_privileges.json +227 -2
  59. package/tests/catalog/p_metadata_parameters.json +102 -2
  60. package/tests/isolation/expected/grant_target_role_race.out +348 -0
  61. package/tests/isolation/expected/last_owner_departure.out +562 -0
  62. package/tests/isolation/expected/session_identity_limits.out +165 -0
  63. package/tests/isolation/expected/session_identity_links.out +235 -0
  64. package/tests/isolation/expected/session_identity_reclaim.out +253 -0
  65. package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
  66. package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
  67. package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
  68. package/tests/isolation/specs/atomic_fleet_authority_races.spec +8 -3
  69. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +0 -3
  70. package/tests/isolation/specs/atomic_fleet_board_archive.spec +0 -3
  71. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +0 -3
  72. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +0 -3
  73. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +0 -3
  74. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +0 -3
  75. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +0 -3
  76. package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +0 -2
  77. package/tests/isolation/specs/claimpolicy_board_scope.spec +0 -2
  78. package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +0 -2
  79. package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +0 -2
  80. package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +0 -2
  81. package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +0 -2
  82. package/tests/isolation/specs/consent_approval_lock.spec +0 -1
  83. package/tests/isolation/specs/consent_emit_actual_state.spec +0 -1
  84. package/tests/isolation/specs/grant_target_role_race.spec +206 -0
  85. package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +0 -1
  86. package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +0 -1
  87. package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +0 -1
  88. package/tests/isolation/specs/last_owner_departure.spec +137 -0
  89. package/tests/isolation/specs/ledger_authority_revoke.spec +0 -3
  90. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +0 -3
  91. package/tests/isolation/specs/outbox_wake_ordering.spec +0 -1
  92. package/tests/isolation/specs/session_identity_limits.spec +81 -0
  93. package/tests/isolation/specs/session_identity_links.spec +82 -0
  94. package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
  95. package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
  96. package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
  97. package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +107 -0
  98. package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +0 -1
  99. package/tests/isolation/specs/signer_lifecycle_revoke.spec +0 -1
  100. package/tests/seeds/20261011000100_agent_board_session_identities.sql.pre.sql +47 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-schema",
3
- "version": "1.60.0",
3
+ "version": "1.64.0",
4
4
  "description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
5
5
  "repository": "https://github.com/Consiliency/message-board.git",
6
6
  "type": "module",
@@ -2,7 +2,7 @@
2
2
  "schema": "agent_board_schema_manifest.v1",
3
3
  "interface": "IF-0-MBPLANE-BUNDLE-1",
4
4
  "package": "@consiliency/agent-board-schema",
5
- "packageVersion": "1.60.0",
5
+ "packageVersion": "1.64.0",
6
6
  "schemaVersion": 8,
7
7
  "minClientVersion": "1.1.0",
8
8
  "source": {
@@ -46,8 +46,8 @@
46
46
  "usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
47
47
  }
48
48
  ],
49
- "migrationCount": 158,
50
- "aggregateSha256": "e887208c42b9617a6a7cc2baae96c87d2d2a011320a8cd4799605d689159cf63",
49
+ "migrationCount": 162,
50
+ "aggregateSha256": "b6b24a1c50a1ac51cd935f6ad2091876a0b0ff98f001d776c89445a889416745",
51
51
  "migrations": [
52
52
  {
53
53
  "order": 0,
@@ -1470,6 +1470,42 @@
1470
1470
  "sourceRef": "message-board",
1471
1471
  "bytes": 25831,
1472
1472
  "sha256": "6cbbe0bb9ccf7ab65402a58de145ee21bc2158ddc7502671ea13a98415ca6421"
1473
+ },
1474
+ {
1475
+ "order": 158,
1476
+ "id": "20261011000100_agent_board_session_identities",
1477
+ "filename": "20261011000100_agent_board_session_identities.sql",
1478
+ "origin": "message-board-forward",
1479
+ "sourceRef": "message-board",
1480
+ "bytes": 119197,
1481
+ "sha256": "0f429d2b7c2a361741fb00019a173c82024a59362ea2d3a70cb11ab594b1d055"
1482
+ },
1483
+ {
1484
+ "order": 159,
1485
+ "id": "20261012000100_agent_board_sidowner",
1486
+ "filename": "20261012000100_agent_board_sidowner.sql",
1487
+ "origin": "message-board-forward",
1488
+ "sourceRef": "message-board",
1489
+ "bytes": 9979,
1490
+ "sha256": "e18fb5a7ce0b00e9cccf12a60cbad49d820612c2c50f65c648d4105c80fbc921"
1491
+ },
1492
+ {
1493
+ "order": 160,
1494
+ "id": "20261014000100_agent_board_grant_target_role",
1495
+ "filename": "20261014000100_agent_board_grant_target_role.sql",
1496
+ "origin": "message-board-forward",
1497
+ "sourceRef": "message-board",
1498
+ "bytes": 14704,
1499
+ "sha256": "4a6c00546a1d2837f88eece1de082cc76eec1f85cce8cf9ac7998e42bf32a5c5"
1500
+ },
1501
+ {
1502
+ "order": 161,
1503
+ "id": "20261014000200_agent_board_last_owner",
1504
+ "filename": "20261014000200_agent_board_last_owner.sql",
1505
+ "origin": "message-board-forward",
1506
+ "sourceRef": "message-board",
1507
+ "bytes": 13141,
1508
+ "sha256": "2fed8fc5a2c7c0ba9acbb12872b329db3f0902d96597c52cc50e21e4682c2fff"
1473
1509
  }
1474
1510
  ],
1475
1511
  "excludedBridge": {
@@ -24,6 +24,9 @@ const historyExact = () => ({ applied: ordered.map((m) => ({ id: m.id, sha256: m
24
24
  const union = () => ordered.filter((m) => m.origin !== 'message-board-forward');
25
25
 
26
26
  describe('manifest integrity', () => {
27
+ it('pins the exact owner-authorized A3/A10.3 test amendment for Consiliency/message-board#675', () => {
28
+ expect(sha256(readFileSync(new URL('../tests/agent_board_grant_target_role.test.sql', import.meta.url)))).toBe('5c0a857d8c138f368235fa345a18c76fafa773cfe8eb57185b232d466d222f93');
29
+ });
27
30
  it('aggregate digest matches recomputation over ordered per-file digests', () => {
28
31
  const agg = sha256(Buffer.from(ordered.map((e) => `${e.order} ${e.id} ${e.sha256}`).join('\n')));
29
32
  expect(agg).toBe(manifest.aggregateSha256);
@@ -38,9 +41,9 @@ describe('manifest integrity', () => {
38
41
  expect(manifest.schemaVersion).toBe(8);
39
42
  expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
40
43
  });
41
- it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 158)', () => {
44
+ it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE + one ADVISORYREVERTRESET + one CLAIMPOLICYRACE + one CLAIMPOLICYLOCKGRANT + one SESSIONCHALLENGESUPERSEDE + one ROOMCONTRACT + one CALLBACKCONSUMER + one ENROLLTOKENSINGLEUSE + one ROOMCONTRACT-ACTIVEFIRST + one ONBOARD + one HOSTED + one HOSTED-FOLLOWUPS + one OWNPOST + one SIDSCHEMA + one SIDOWNER + one GRANTTARGETROLE (#674) + one LASTOWNER (Consiliency/message-board#675) forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 162)', () => {
42
45
  expect(manifest.migrationCount).toBe(manifest.migrations.length);
43
- expect(manifest.migrationCount).toBe(158);
46
+ expect(manifest.migrationCount).toBe(162);
44
47
  // MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
45
48
  // schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
46
49
  // enrollment-only additive surface (identity_providers registry, enrollment audit,
@@ -356,6 +359,11 @@ describe('manifest integrity', () => {
356
359
  // OWNPOST (#608): an agent's own board post is a feed-only row for it, not an inbox item;
357
360
  // a board-wide task fan-out skips its creator; own_inbox callbacks skip feed-only rows.
358
361
  '20261010000100_agent_board_own_post_not_delivered.sql',
362
+ '20261011000100_agent_board_session_identities.sql',
363
+ '20261012000100_agent_board_sidowner.sql',
364
+ // #674: grant_board_role checks the target's current role (C-A + C-B).
365
+ '20261014000100_agent_board_grant_target_role.sql',
366
+ '20261014000200_agent_board_last_owner.sql',
359
367
  ]) {
360
368
  expect(names.has(f)).toBe(true);
361
369
  }
@@ -307,11 +307,11 @@ select ok(
307
307
  'agent fleet revocation takes its machine locks before the agent lock [SOLE PIN: no isolation spec discriminates this ordering -- measured against all 20]'
308
308
  );
309
309
  select ok(
310
- strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = p_agent_id for update'
310
+ strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = v_id for update'
311
311
  ) > 0
312
312
  and strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agent_signing_keys'
313
313
  ) > 0
314
- and strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = p_agent_id for update'
314
+ and strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agents a where a.id = v_id for update'
315
315
  ) < strpos(lower(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure)), 'from agent_board.agent_signing_keys'
316
316
  ),
317
317
  'subtree primitive locks the agent before signing keys [SOLE PIN: no isolation spec discriminates this ordering -- measured against all 20]'
@@ -558,7 +558,10 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
558
558
  -- lock also closes this reader's exposure to the ceiling row's ABSENCE, one of the three
559
559
  -- 20260901000110 left explicitly undetermined.
560
560
  'agent_board.grant_board_role(uuid,uuid,text,uuid) => serialized',
561
- 'agent_board.onboard_federated_fleet_agent(text,text,text,jsonb,text,text,text,text,boolean,text,timestamp with time zone,text,text,text[],text[],text,uuid,uuid,text,text,text,text,text,text,text) => serialized'
561
+ 'agent_board.link_session_alias(uuid,text,text) => serialized',
562
+ 'agent_board.mint_delegated_child(uuid,text,jsonb,text,interval,boolean,text,text,text,jsonb,jsonb,uuid) => serialized',
563
+ 'agent_board.onboard_federated_fleet_agent(text,text,text,jsonb,text,text,text,text,boolean,text,timestamp with time zone,text,text,text[],text[],text,uuid,uuid,text,text,text,text,text,text,text) => serialized',
564
+ 'agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval) => serialized'
562
565
  ]::text[],
563
566
  'every ceiling-authorization reader in pg_proc is declared with its serialization state'
564
567
  );
@@ -697,6 +700,7 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
697
700
  -- than naming the table itself. Internal — it reads rows for an arbitrary agent id with no
698
701
  -- authorization of its own, so service_role only, like every other helper here.
699
702
  'agent_board._actor_fleet_revoked(uuid) [definer] => service_role',
703
+ 'agent_board._board_memberships_last_owner_guard() [definer] => (none)',
700
704
  'agent_board._lock_board_authority_ceiling(uuid) [invoker] => service_role',
701
705
  'agent_board._redeem_fleet_invite_for_agent(text,uuid) [definer] => service_role',
702
706
  'agent_board._redeem_invite_for_agent(text,uuid) [definer] => service_role',
@@ -718,8 +722,11 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
718
722
  -- once the ledger gate made it lock both). Still authenticated,service_role — #276 removes
719
723
  -- authority from a revoked principal, it does not change who may call the RPC.
720
724
  'agent_board.grant_board_role(uuid,uuid,text,uuid) [definer] => authenticated,service_role',
725
+ 'agent_board.link_session_alias(uuid,text,text) [definer] => service_role',
726
+ 'agent_board.mint_delegated_child(uuid,text,jsonb,text,interval,boolean,text,text,text,jsonb,jsonb,uuid) [definer] => service_role',
721
727
  'agent_board.onboard_federated_fleet_agent(text,text,text,jsonb,text,text,text,text,boolean,text,timestamp with time zone,text,text,text[],text[],text,uuid,uuid,text,text,text,text,text,text,text) [definer] => service_role',
722
728
  'agent_board.reactivate_fleet_machine(uuid,uuid,text) [definer] => service_role',
729
+ 'agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval) [definer] => authenticated',
723
730
  'agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb) [definer] => authenticated,service_role',
724
731
  'agent_board.revoke_human_subtree(uuid,uuid,text) [definer] => service_role',
725
732
  'agent_board.set_board_authority_ceiling(uuid,text,uuid) [definer] => authenticated,service_role',
@@ -727,7 +734,11 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
727
734
  -- FIRST, because its order was board_memberships(grantee) -> agents(signer) against
728
735
  -- onboard_federated_fleet_agent's agents -> board_memberships -- a reproduced ABBA between
729
736
  -- two honest callers. It is service_role-only like every other signed_* wrapper (#118).
730
- 'agent_board.signed_grant_board_role(jsonb,jsonb) [definer] => service_role'
737
+ 'agent_board.signed_grant_board_role(jsonb,jsonb) [definer] => service_role',
738
+ -- SIDSCHEMA integrity repair: the verified Portal withdrawal now takes board mutexes
739
+ -- before the instance/family rows. Its service-only transport is unchanged.
740
+ 'agent_board.tombstone_delegate_instance(text,text,text,text) [definer] => service_role',
741
+ 'agent_board.update_board_membership(uuid,uuid,text,jsonb) [definer] => authenticated,service_role'
731
742
  ]::text[],
732
743
  'every function on this migration set''s surface is declared with its definer flag and exact EXECUTE grantees'
733
744
  );
@@ -837,6 +837,13 @@ select is(
837
837
  '0/0', 'generic-invite rejection leaves uses and redemption counts unchanged'
838
838
  );
839
839
 
840
+ -- Preserve a second owner while exercising the invite creator's lost membership.
841
+ insert into agent_board.agents(id, agent_type, handle, display_name, status)
842
+ values ('16210000-0000-0000-0000-000000000002', 'agent', 'atomic-runtime-coowner', 'Atomic Runtime Coowner', 'active');
843
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role)
844
+ select board_id, '16210000-0000-0000-0000-000000000002', 'joined', 'owner' from _atomic_cfg;
845
+ select agent_board._lock_board_authority_ceiling(board_id) from _atomic_cfg;
846
+
840
847
  update agent_board.board_memberships
841
848
  set membership_state='removed', left_at=now(), updated_at=now()
842
849
  where board_id=(select board_id from _atomic_cfg)
@@ -14,7 +14,7 @@ set search_path = extensions, public, agent_board;
14
14
 
15
15
  begin;
16
16
 
17
- select plan(68);
17
+ select plan(69);
18
18
 
19
19
  create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
20
20
  begin
@@ -215,9 +215,10 @@ select is(
215
215
  -- reason. Keying only on INSERT would miss a future path that flips membership_state via
216
216
  -- UPDATE -- and catching FUTURE writers is the entire purpose of this assertion.
217
217
  --
218
- -- The single exemption is grant_board_role: it sets `role` only, with membership_state
219
- -- appearing exclusively in its WHERE clauses, so it cannot change membership state and a role
220
- -- change is not a consent event. If that ever stops being true, this assertion fails.
218
+ -- The exemptions are grant_board_role and set_board_member_label: each changes metadata
219
+ -- only, with membership_state appearing exclusively outside a SET clause, so neither can
220
+ -- change membership state and neither is a consent event. If that ever stops being true,
221
+ -- the positive assertion below fails.
221
222
  -- COMMENTS ARE STRIPPED and CALL SITES ARE COUNTED. Checking that the emitter name appears
222
223
  -- anywhere in the body is fail-open twice over: the name in a comment or a dead branch would
223
224
  -- satisfy it, and calling the emitter TWICE would too -- while this suite claims exactly one
@@ -234,7 +235,12 @@ select is(
234
235
  and p.prolang <> (select oid from pg_language where lanname = 'internal')
235
236
  and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into|update', 'board_memberships')
236
237
  and agent_board_test.stripped_body(p.oid) !~* 'perform agent_board\._emit_membership_event'
237
- and p.proname <> 'grant_board_role'),
238
+ -- Mixed SID writers change state only through the emitting sink; their
239
+ -- direct role-only UPDATE is not a second membership transition.
240
+ and (agent_board_test.stripped_body(p.oid) !~ agent_board_test.call_re('_upsert_board_membership_state', false)
241
+ or agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into', 'board_memberships')
242
+ or agent_board_test.stripped_body(p.oid) ~ agent_board_test.col_write_re('board_memberships', 'membership_state'))
243
+ and p.proname not in ('grant_board_role', 'set_board_member_label')),
238
244
  '',
239
245
  'COMPLETENESS: every function writing board_memberships emits, or is a stated exemption');
240
246
 
@@ -261,13 +267,26 @@ select is(
261
267
  '',
262
268
  'COMPLETENESS: no writer emits more than once (exactly one event per transition)');
263
269
 
264
- -- ...and the one exemption is genuinely state-blind: membership_state never appears in a SET.
265
- select ok(
266
- (select agent_board_test.stripped_body(p.oid, true)
267
- !~ agent_board_test.re(agent_board_test.col_assign_frag('board_memberships', 'membership_state', agent_board_test.bare_ok(p.oid)))
270
+ -- ...and each exemption is genuinely state-blind: membership_state never appears in a SET.
271
+ select is(
272
+ (select coalesce(string_agg(p.proname, ', ' order by p.proname), '')
268
273
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
269
- where n.nspname = 'agent_board' and p.proname = 'grant_board_role'),
270
- 'COMPLETENESS: the exemption holds -- grant_board_role never SETs membership_state');
274
+ where n.nspname = 'agent_board'
275
+ and p.proname in ('grant_board_role', 'set_board_member_label')
276
+ and agent_board_test.stripped_body(p.oid, true)
277
+ !~ agent_board_test.re(agent_board_test.col_assign_frag('board_memberships', 'membership_state', agent_board_test.bare_ok(p.oid)))),
278
+ 'grant_board_role, set_board_member_label',
279
+ 'COMPLETENESS: each stated exemption is state-blind and never SETs membership_state');
280
+
281
+ select is(
282
+ (select string_agg(p.proname, ', ' order by p.proname)
283
+ from pg_proc p where p.pronamespace = 'agent_board'::regnamespace
284
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.col_write_re('board_memberships', 'role')
285
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_board_membership_state', false)
286
+ and agent_board_test.stripped_body(p.oid) !~ agent_board_test.stmt_re('insert into', 'board_memberships')
287
+ and agent_board_test.stripped_body(p.oid) !~ agent_board_test.col_write_re('board_memberships', 'membership_state')),
288
+ 'link_session_alias, mint_delegated_child, readmit_session_identity',
289
+ 'COMPLETENESS: each mixed SID writer directly writes roles and delegates state transitions to the emitting sink');
271
290
 
272
291
  -- ...and the emitter itself is the single implementation, not five copies.
273
292
  select is(
@@ -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