@consiliency/agent-board-schema 1.12.0 → 1.23.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 (156) hide show
  1. package/fixtures/already-applied.json +160 -0
  2. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  3. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  4. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  5. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  6. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  7. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  8. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  9. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  10. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  11. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  12. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  13. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  14. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  15. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  16. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  17. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  18. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  19. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  20. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  21. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  22. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  23. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  24. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  25. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  26. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  27. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  28. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  29. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  30. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  31. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  32. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  33. package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
  34. package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
  35. package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
  36. package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
  37. package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
  38. package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
  39. package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
  40. package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
  41. package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
  42. package/package.json +1 -1
  43. package/schema-manifest.json +363 -3
  44. package/src/tooling.test.mjs +113 -2
  45. package/tests/agent_board_actorspoof.test.sql +349 -0
  46. package/tests/agent_board_agentsdir.test.sql +154 -0
  47. package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
  48. package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
  49. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
  50. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  51. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  52. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  53. package/tests/agent_board_claimkey.test.sql +229 -0
  54. package/tests/agent_board_consent_backfill.test.sql +48 -0
  55. package/tests/agent_board_consent_emission.test.sql +601 -0
  56. package/tests/agent_board_consent_gating.test.sql +489 -0
  57. package/tests/agent_board_consent_substrate.test.sql +720 -0
  58. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  59. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  60. package/tests/agent_board_delegation_policies.test.sql +107 -1
  61. package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
  62. package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
  63. package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
  64. package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
  65. package/tests/agent_board_fedsession.test.sql +51 -5
  66. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
  67. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  68. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  69. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  70. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  71. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  72. package/tests/agent_board_fleet_policy.test.sql +2 -2
  73. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  74. package/tests/agent_board_identity_providers.test.sql +9 -0
  75. package/tests/agent_board_inbox_reads.test.sql +45 -31
  76. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  77. package/tests/agent_board_invitepolicy.test.sql +205 -0
  78. package/tests/agent_board_leaveidem.test.sql +194 -0
  79. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  80. package/tests/agent_board_mint_harden.test.sql +303 -0
  81. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  82. package/tests/agent_board_obsgate.test.sql +178 -0
  83. package/tests/agent_board_obsgate2.test.sql +153 -0
  84. package/tests/agent_board_obsgate3.test.sql +170 -0
  85. package/tests/agent_board_obsgate4.test.sql +196 -0
  86. package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
  87. package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
  88. package/tests/agent_board_pubexec_class.test.sql +53 -0
  89. package/tests/agent_board_rejectsem.test.sql +271 -0
  90. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  91. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  92. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  93. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  94. package/tests/agent_board_rolereset.test.sql +351 -0
  95. package/tests/agent_board_roomdeleg.test.sql +5 -3
  96. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  97. package/tests/agent_board_roomlink.test.sql +27 -11
  98. package/tests/agent_board_rowid.test.sql +1274 -0
  99. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  100. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  101. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  102. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  103. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  104. package/tests/agent_board_signing_store.test.sql +6 -6
  105. package/tests/agent_board_wake_recovery.test.sql +756 -0
  106. package/tests/fixtures/isolation-substrate.sql +93 -0
  107. package/tests/fixtures/test-prereqs.sql +33 -0
  108. package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
  109. package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
  110. package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
  111. package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
  112. package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
  113. package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
  114. package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
  115. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
  116. package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
  117. package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
  118. package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
  119. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  120. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  121. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  122. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  123. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  124. package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
  125. package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
  126. package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
  127. package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
  128. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  129. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  130. package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
  131. package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
  132. package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
  133. package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
  134. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
  135. package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
  136. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
  137. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
  138. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
  139. package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
  140. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
  141. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
  142. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  143. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  144. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  145. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  146. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  147. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
  148. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
  149. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
  150. package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
  151. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  152. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  153. package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
  154. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  155. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
  156. package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
@@ -0,0 +1,303 @@
1
+ -- DELEGCAPCEILING (#142) pgTAP — operator delegation caps are a DURABLE hard ceiling
2
+ -- that a self-service (authenticated) root cannot loosen. Every property is
3
+ -- mutation-oriented (CLAUDE.md rule #2):
4
+ --
5
+ -- * PRESERVATION: a self-service upsert leaves the operator_* ceiling columns UNCHANGED
6
+ -- and inserts NO shadow row (positive exemption, rule #1) — mutate the upsert to drop
7
+ -- the preserve and operator_max/op_ttl flip.
8
+ -- * MINT CAP CLAMP: under an operator max_active_children=1, a self-service mint mints at
9
+ -- most 1 (the effective cap is min(self, operator, 25), NOT 25) — mutate the mint clamp
10
+ -- back to least(coalesce(self,25),25) and the throw disappears.
11
+ -- * MINT TTL CLAMP: under an operator child_ttl_ceiling=2h, a self-service child is clamped
12
+ -- to <=2h — mutate the mint clamp to ignore operator_child_ttl_ceiling and it jumps to 24h.
13
+ -- * REQUIRE DURABILITY (invariant 4): an operator require=true is not relaxed to false by
14
+ -- self-service (the self-pin), so a keyless self-service mint is refused — mutate the pin
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.
18
+ -- * NO REGRESSION: a root with no operator policy self-services bounded only by the global
19
+ -- 25/24h ceiling.
20
+ -- * EXECUTE class: CREATE OR REPLACE (no DROP) kept the ACL — mint stays service_role-only,
21
+ -- upsert stays authenticated-executable (neither re-opened to PUBLIC/anon).
22
+ --
23
+ -- Allow-side assertions use lives_ok(...) + a separate state read (never
24
+ -- is((select ... from rpc(...)))) so an allow that silently raises cannot pass green (#113).
25
+ -- Runs as postgres; request.jwt.claim.role toggles _is_service_role() (''/service_role => true,
26
+ -- 'authenticated' => false) and request.jwt.claim.sub picks the acting agent. Mints run as
27
+ -- service_role with p_self_service=true simulating the EF-forwarded authenticated call (the EF
28
+ -- binds root==session; that binding is tested in the EF suite).
29
+ create extension if not exists pgtap with schema extensions;
30
+ set search_path = extensions, public, agent_board;
31
+
32
+ begin;
33
+
34
+ select plan(40);
35
+
36
+ create temp table _cf (
37
+ owner_id uuid,
38
+ cap uuid, cap_user uuid, cap_child uuid,
39
+ ttl uuid, ttl_user uuid,
40
+ req uuid, req_user uuid,
41
+ none uuid, none_user uuid,
42
+ lat uuid, lat_user uuid,
43
+ k1 text, k2 text, k3 text, k4 text
44
+ ) on commit drop;
45
+
46
+ do $$
47
+ declare
48
+ s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
49
+ u_owner uuid;
50
+ v_cap uuid := gen_random_uuid();
51
+ v_ttl uuid := gen_random_uuid();
52
+ v_req uuid := gen_random_uuid();
53
+ v_none uuid := gen_random_uuid();
54
+ v_lat uuid := gen_random_uuid();
55
+ v_cap_child uuid := gen_random_uuid();
56
+ v_cap_user uuid; v_ttl_user uuid; v_req_user uuid; v_none_user uuid; v_lat_user uuid;
57
+ begin
58
+ u_owner := public.ensure_github_user('cf-own-'||s, 'cf-own-'||s||'@example.test', 'cf-own-'||s, 'CF Owner');
59
+ -- Each self-servicing root needs its own auth user (current_agent_id maps auth.uid ->
60
+ -- agents.auth_user_id; the acting sub is set to this user id).
61
+ v_cap_user := public.ensure_github_user('cf-cap-' ||s, 'cf-cap-' ||s||'@example.test', 'cf-cap-' ||s, 'CF Cap');
62
+ v_ttl_user := public.ensure_github_user('cf-ttl-' ||s, 'cf-ttl-' ||s||'@example.test', 'cf-ttl-' ||s, 'CF Ttl');
63
+ v_req_user := public.ensure_github_user('cf-req-' ||s, 'cf-req-' ||s||'@example.test', 'cf-req-' ||s, 'CF Req');
64
+ v_none_user := public.ensure_github_user('cf-none-'||s, 'cf-none-'||s||'@example.test', 'cf-none-'||s, 'CF None');
65
+ v_lat_user := public.ensure_github_user('cf-lat-' ||s, 'cf-lat-' ||s||'@example.test', 'cf-lat-' ||s, 'CF Lat');
66
+
67
+ -- All roots are TRUE lineage roots (parent null, not delegated children).
68
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
69
+ values
70
+ (v_cap, 'agent', 'cf-cap-' ||s, 'CF Cap', 'active', v_cap_user, u_owner),
71
+ (v_ttl, 'agent', 'cf-ttl-' ||s, 'CF Ttl', 'active', v_ttl_user, u_owner),
72
+ (v_req, 'agent', 'cf-req-' ||s, 'CF Req', 'active', v_req_user, u_owner),
73
+ (v_none, 'agent', 'cf-none-'||s, 'CF None', 'active', v_none_user, u_owner),
74
+ (v_lat, 'agent', 'cf-lat-' ||s, 'CF Lat', 'active', v_lat_user, u_owner);
75
+
76
+ -- A pre-created child agent to hang root_cap's single preloaded live grant on
77
+ -- (delegation_grants.child_agent_id is a unique FK to agents).
78
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, owner_user_id)
79
+ values (v_cap_child, 'agent', 'cf-cap-child-'||s, 'CF Cap Child', 'active', u_owner);
80
+
81
+ insert into _cf values (
82
+ u_owner,
83
+ v_cap, v_cap_user, v_cap_child,
84
+ v_ttl, v_ttl_user,
85
+ v_req, v_req_user,
86
+ v_none, v_none_user,
87
+ v_lat, v_lat_user,
88
+ -- Distinct canonical 43-char base64url raw keys (one per key-inserting mint).
89
+ rtrim(translate(encode(extensions.gen_random_bytes(32), 'base64'), '+/', '-_'), '='),
90
+ rtrim(translate(encode(extensions.gen_random_bytes(32), 'base64'), '+/', '-_'), '='),
91
+ rtrim(translate(encode(extensions.gen_random_bytes(32), 'base64'), '+/', '-_'), '='),
92
+ rtrim(translate(encode(extensions.gen_random_bytes(32), 'base64'), '+/', '-_'), '='));
93
+ end
94
+ $$;
95
+
96
+ -- ---------------------------------------------------------------------------
97
+ -- Existence of the operator-ceiling columns.
98
+ -- ---------------------------------------------------------------------------
99
+ select has_column('agent_board', 'delegation_policies', 'operator_max_active_children',
100
+ 'delegation_policies.operator_max_active_children exists');
101
+ select has_column('agent_board', 'delegation_policies', 'operator_child_ttl_ceiling',
102
+ 'delegation_policies.operator_child_ttl_ceiling exists');
103
+
104
+ -- ===========================================================================
105
+ -- A. Operator max=1 is a DURABLE ceiling: survives a self-service widen, and the
106
+ -- self-service MINT is clamped to 1 (not 25).
107
+ -- ===========================================================================
108
+ -- Operator establishes the cap (service_role path).
109
+ select set_config('request.jwt.claim.role', '', true);
110
+ select set_config('request.jwt.claim.sub', '', true);
111
+ select lives_ok(
112
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, 1, interval '8 hours', false) $$,
113
+ (select cap from _cf)),
114
+ 'A: operator upsert (max_active_children=1) lives');
115
+ select is((select operator_max_active_children from agent_board.delegation_policies where root_agent_id = (select cap from _cf) and revoked_at is null order by created_at desc limit 1),
116
+ 1, 'A: operator establishes operator_max_active_children=1');
117
+
118
+ -- Root self-services a widen to 25 (authenticated path).
119
+ select set_config('request.jwt.claim.role', 'authenticated', true);
120
+ select set_config('request.jwt.claim.sub', (select cap_user from _cf)::text, true);
121
+ select lives_ok(
122
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, 25, interval '24 hours', false) $$,
123
+ (select cap from _cf)),
124
+ 'A: self-service upsert (widen to 25) lives');
125
+ select is((select operator_max_active_children from agent_board.delegation_policies where root_agent_id = (select cap from _cf) and revoked_at is null order by created_at desc limit 1),
126
+ 1, 'A: operator_max_active_children=1 SURVIVES the self-service overwrite (durable ceiling)');
127
+ select is((select max_active_children from agent_board.delegation_policies where root_agent_id = (select cap from _cf) and revoked_at is null order by created_at desc limit 1),
128
+ 25, 'A: self-service wrote its OWN cap (25) into max_active_children');
129
+ select is((select scope_attestation from agent_board.delegation_policies where root_agent_id = (select cap from _cf) and revoked_at is null order by created_at desc limit 1),
130
+ 'self_declared', 'A: the row flipped to self_declared');
131
+ select is((select count(*) from agent_board.delegation_policies where root_agent_id = (select cap from _cf) and revoked_at is null),
132
+ 1::bigint, 'A: no shadow row inserted — the single latest-unrevoked row was updated in place');
133
+
134
+ -- Preload ONE live grant on root_cap so the effective cap of 1 is already reached.
135
+ insert into agent_board.delegation_grants(child_agent_id, root_agent_id, parent_agent_id, delegation_policy_id, granted_scopes, grant_expires_at)
136
+ select _cf.cap_child, _cf.cap, _cf.cap, dp.id, '[]'::jsonb, now() + interval '1 hour'
137
+ from agent_board.delegation_policies dp, _cf
138
+ where dp.root_agent_id = _cf.cap and dp.revoked_at is null;
139
+
140
+ -- Self-service mint: effective cap = min(self 25, operator 1, global 25) = 1. With one live
141
+ -- grant it is refused. The mutation reverting the mint clamp to least(coalesce(self,25),25)=25
142
+ -- lets the mint through (25 > 1) and this throw disappears.
143
+ select set_config('request.jwt.claim.role', '', true);
144
+ select set_config('request.jwt.claim.sub', '', true);
145
+ select throws_ok(
146
+ (select format($$ select agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, null, null, true) $$,
147
+ cap, 'cf-sscap-'||replace(substr(gen_random_uuid()::text,1,8),'-','')) from _cf),
148
+ '42501', 'max_active_children: root has reached its active delegated-child ceiling',
149
+ 'A: self-service mint clamps max_active_children to the operator ceiling of 1 (stored 25 NOT trusted)');
150
+
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.
153
+ 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) $$,
155
+ 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)');
157
+ select is((select count(*) from _a_opcap), 1::bigint,
158
+ 'A: operator cap mint produced exactly one child (not a silent zero-row pass)');
159
+
160
+ -- ===========================================================================
161
+ -- B. Operator child_ttl_ceiling=2h is a DURABLE ceiling: a self-service child is
162
+ -- clamped to <=2h; the operator path keeps its latitude.
163
+ -- ===========================================================================
164
+ select set_config('request.jwt.claim.role', '', true);
165
+ select set_config('request.jwt.claim.sub', '', true);
166
+ select lives_ok(
167
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, null, interval '2 hours', false) $$,
168
+ (select ttl from _cf)),
169
+ 'B: operator upsert (child_ttl_ceiling=2h) lives');
170
+ select is((select operator_child_ttl_ceiling from agent_board.delegation_policies where root_agent_id = (select ttl from _cf) and revoked_at is null order by created_at desc limit 1),
171
+ interval '2 hours', 'B: operator establishes operator_child_ttl_ceiling=2h');
172
+
173
+ select set_config('request.jwt.claim.role', 'authenticated', true);
174
+ select set_config('request.jwt.claim.sub', (select ttl_user from _cf)::text, true);
175
+ select lives_ok(
176
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, null, interval '24 hours', false) $$,
177
+ (select ttl from _cf)),
178
+ 'B: self-service upsert (child_ttl_ceiling=24h) lives');
179
+ select is((select operator_child_ttl_ceiling from agent_board.delegation_policies where root_agent_id = (select ttl from _cf) and revoked_at is null order by created_at desc limit 1),
180
+ interval '2 hours', 'B: operator_child_ttl_ceiling=2h SURVIVES the self-service overwrite');
181
+ select is((select child_ttl_ceiling from agent_board.delegation_policies where root_agent_id = (select ttl from _cf) and revoked_at is null order by created_at desc limit 1),
182
+ interval '24 hours', 'B: self-service wrote its OWN child_ttl_ceiling (24h)');
183
+
184
+ -- Self-service mint: TTL ceiling = min(self 24h, operator 2h, global 24h) = 2h.
185
+ select set_config('request.jwt.claim.role', '', true);
186
+ select set_config('request.jwt.claim.sub', '', true);
187
+ 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) $$,
189
+ ttl, 'cf-ssttl-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k2) from _cf),
190
+ 'B: self-service mint under the 2h operator ceiling lives');
191
+ select ok(
192
+ (select grant_expires_at <= now() + interval '2 hours' + interval '1 minute' from _b_ssttl),
193
+ 'B: self-service child TTL clamped to the 2h operator ceiling (stored 24h NOT trusted)');
194
+
195
+ -- Operator path honors the stored 24h ceiling (NOT clamped to the 2h operator ceiling: latitude).
196
+ 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) $$,
198
+ ttl, 'cf-opttl-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k3) from _cf),
199
+ 'B: operator mint under the same policy lives');
200
+ 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)');
203
+
204
+ -- ===========================================================================
205
+ -- C. Operator require_child_signing_key=true is durable: self-service cannot relax it
206
+ -- to false (self-pin), so a keyless self-service mint is refused (invariant 4).
207
+ -- ===========================================================================
208
+ select set_config('request.jwt.claim.role', '', true);
209
+ select set_config('request.jwt.claim.sub', '', true);
210
+ select lives_ok(
211
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, null, interval '8 hours', true) $$,
212
+ (select req from _cf)),
213
+ 'C: operator upsert (require_child_signing_key=true) lives');
214
+ select is((select require_child_signing_key from agent_board.delegation_policies where root_agent_id = (select req from _cf) and revoked_at is null order by created_at desc limit 1),
215
+ true, 'C: operator require_child_signing_key=true established');
216
+
217
+ select set_config('request.jwt.claim.role', 'authenticated', true);
218
+ select set_config('request.jwt.claim.sub', (select req_user from _cf)::text, true);
219
+ select lives_ok(
220
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, null, interval '8 hours', false) $$,
221
+ (select req from _cf)),
222
+ 'C: self-service upsert requesting require=false lives');
223
+ select is((select require_child_signing_key from agent_board.delegation_policies where root_agent_id = (select req from _cf) and revoked_at is null order by created_at desc limit 1),
224
+ true, 'C: require_child_signing_key stays true — self-service cannot relax an operator true (pinned)');
225
+
226
+ -- A keyless self-service mint is refused because the policy still requires a key.
227
+ select set_config('request.jwt.claim.role', '', true);
228
+ select set_config('request.jwt.claim.sub', '', true);
229
+ select throws_ok(
230
+ (select format($$ select agent_board.mint_delegated_child(%L::uuid, %L, '[]'::jsonb, null, null, true) $$,
231
+ req, 'cf-ssreq-'||replace(substr(gen_random_uuid()::text,1,8),'-','')) from _cf),
232
+ '22023', 'a child public key is required when the delegation policy requires a signing key',
233
+ 'C: keyless self-service mint refused — the operator require=true survived (invariant 4)');
234
+
235
+ -- ===========================================================================
236
+ -- D. No operator policy => the operator ceiling is NULL and self-service is bounded
237
+ -- only by the global 25/24h ceiling (no regression).
238
+ -- ===========================================================================
239
+ select set_config('request.jwt.claim.role', 'authenticated', true);
240
+ select set_config('request.jwt.claim.sub', (select none_user from _cf)::text, true);
241
+ select lives_ok(
242
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, 10, interval '12 hours', false) $$,
243
+ (select none from _cf)),
244
+ 'D: first-ever self-service upsert (no operator policy) lives');
245
+ select is((select operator_max_active_children from agent_board.delegation_policies where root_agent_id = (select none from _cf) and revoked_at is null order by created_at desc limit 1),
246
+ null, 'D: operator_max_active_children is NULL (no operator ceiling)');
247
+ select is((select operator_child_ttl_ceiling from agent_board.delegation_policies where root_agent_id = (select none from _cf) and revoked_at is null order by created_at desc limit 1),
248
+ null, 'D: operator_child_ttl_ceiling is NULL (no operator ceiling)');
249
+
250
+ -- Self-service mint bounded by the root's own 12h ceiling (min(self 12h, no-operator, 24h)=12h).
251
+ select set_config('request.jwt.claim.role', '', true);
252
+ select set_config('request.jwt.claim.sub', '', true);
253
+ 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) $$,
255
+ none, 'cf-ssnone-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), k4) from _cf),
256
+ 'D: self-service mint with no operator policy lives');
257
+ select ok(
258
+ (select grant_expires_at <= now() + interval '12 hours' + interval '1 minute'
259
+ and grant_expires_at > now() + interval '11 hours' from _d_ss),
260
+ 'D: self-service child honors the root''s own 12h ceiling (bounded by self/global only)');
261
+
262
+ -- ===========================================================================
263
+ -- E. Operator WRITER latitude: an operator can still set AND loosen its own ceiling.
264
+ -- ===========================================================================
265
+ select set_config('request.jwt.claim.role', '', true);
266
+ select set_config('request.jwt.claim.sub', '', true);
267
+ select lives_ok(
268
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, 1, interval '8 hours', false) $$,
269
+ (select lat from _cf)),
270
+ 'E: operator upsert sets a tight ceiling (max=1) — lives');
271
+ select is((select operator_max_active_children from agent_board.delegation_policies where root_agent_id = (select lat from _cf) and revoked_at is null order by created_at desc limit 1),
272
+ 1, 'E: operator ceiling established at 1');
273
+ select lives_ok(
274
+ format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, 25, interval '8 hours', false) $$,
275
+ (select lat from _cf)),
276
+ 'E: operator upsert loosens its own ceiling (max=25) — lives');
277
+ select is((select operator_max_active_children from agent_board.delegation_policies where root_agent_id = (select lat from _cf) and revoked_at is null order by created_at desc limit 1),
278
+ 25, 'E: operator can loosen its OWN ceiling to 25 (writer latitude)');
279
+ select is((select scope_attestation from agent_board.delegation_policies where root_agent_id = (select lat from _cf) and revoked_at is null order by created_at desc limit 1),
280
+ 'operator_vetted', 'E: an operator rewrite re-stamps operator_vetted');
281
+
282
+ -- ===========================================================================
283
+ -- F. EXECUTE class — CREATE OR REPLACE (no DROP) preserved the ACL; neither function
284
+ -- re-opened to PUBLIC/anon.
285
+ -- ===========================================================================
286
+ select ok(not has_function_privilege('authenticated',
287
+ 'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean)', 'EXECUTE'),
288
+ 'F: mint_delegated_child stays service_role-only (authenticated has NO EXECUTE after recreate)');
289
+ select ok(not has_function_privilege('anon',
290
+ 'agent_board.upsert_delegation_policy(uuid, text, jsonb, integer, interval, boolean)', 'EXECUTE'),
291
+ 'F: anon has NO EXECUTE on upsert_delegation_policy');
292
+ select ok(has_function_privilege('authenticated',
293
+ 'agent_board.upsert_delegation_policy(uuid, text, jsonb, integer, interval, boolean)', 'EXECUTE'),
294
+ 'F: authenticated retains EXECUTE on upsert_delegation_policy after recreate');
295
+ select ok(has_function_privilege('service_role',
296
+ 'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean)', 'EXECUTE'),
297
+ 'F: service_role retains EXECUTE on mint_delegated_child');
298
+
299
+ select set_config('request.jwt.claim.role', '', true);
300
+ select set_config('request.jwt.claim.sub', '', true);
301
+
302
+ select * from finish();
303
+ rollback;
@@ -13,20 +13,22 @@ set search_path = extensions, public, agent_board;
13
13
 
14
14
  begin;
15
15
 
16
- select plan(18);
16
+ select plan(21);
17
17
 
18
18
  create temp table _deleg_fx (
19
- owner_id uuid, r1 uuid, r2 uuid, r3 uuid,
19
+ owner_id uuid, r1 uuid, r2 uuid, r3 uuid, r4 uuid,
20
20
  child_a uuid, child_b uuid, exp_a timestamptz,
21
- slug_c text, ca_pubkey text
21
+ slug_c text, pol4 uuid, ca_pubkey text
22
22
  ) on commit drop;
23
23
 
24
24
  do $$
25
25
  declare
26
26
  s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
27
- u_owner uuid; u_r1 uuid; u_r2 uuid; u_r3 uuid;
27
+ u_owner uuid; u_r1 uuid; u_r2 uuid; u_r3 uuid; u_r4 uuid;
28
28
  v_r1 uuid := gen_random_uuid(); v_r2 uuid := gen_random_uuid(); v_r3 uuid := gen_random_uuid();
29
+ v_r4 uuid := gen_random_uuid();
29
30
  v_pol1 uuid := gen_random_uuid(); v_pol2 uuid := gen_random_uuid(); v_pol3 uuid := gen_random_uuid();
31
+ v_pol4 uuid := gen_random_uuid();
30
32
  v_ca uuid; v_cb uuid; v_exp_a timestamptz;
31
33
  -- Real (random) 43-char base64url Ed25519 public keys, generated caller-side.
32
34
  v_ca_pubkey text := rtrim(translate(encode(extensions.gen_random_bytes(32), 'base64'), '+/', '-_'), '=');
@@ -36,6 +38,7 @@ begin
36
38
  u_r1 := public.ensure_github_user('deleg-r1-'||s, 'deleg-r1-'||s||'@example.test', 'deleg-r1-'||s, 'R1');
37
39
  u_r2 := public.ensure_github_user('deleg-r2-'||s, 'deleg-r2-'||s||'@example.test', 'deleg-r2-'||s, 'R2');
38
40
  u_r3 := public.ensure_github_user('deleg-r3-'||s, 'deleg-r3-'||s||'@example.test', 'deleg-r3-'||s, 'R3');
41
+ u_r4 := public.ensure_github_user('deleg-r4-'||s, 'deleg-r4-'||s||'@example.test', 'deleg-r4-'||s, 'R4');
39
42
 
40
43
  -- Root R1: root_minted policy, allowed_scopes=[a,b], ceiling 8h, max 2 children.
41
44
  insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
@@ -56,6 +59,16 @@ begin
56
59
  insert into agent_board.delegation_policies(id, root_agent_id, mode, allowed_scopes, child_ttl_ceiling, require_child_signing_key)
57
60
  values (v_pol3, v_r3, 'root_minted', jsonb_build_array('scope:a'), interval '8 hours', true);
58
61
 
62
+ -- Root R4: root_minted, UNCAPPED (max_active_children null), no child signing key
63
+ -- required -> exercises the "policy unrevoked" mint gate. Starts unrevoked so the
64
+ -- ALLOW twin mints; the DENY revokes v_pol4 in-test. Uncapped is load-bearing: with
65
+ -- no ceiling, the unrevoked mint cannot raise max_active_children, so the DENY throw
66
+ -- can only come from the revocation (see the mutation-pin rationale in the test body).
67
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
68
+ values (v_r4, 'agent', 'deleg-r4-'||s, 'R4', 'active', u_r4, u_owner);
69
+ insert into agent_board.delegation_policies(id, root_agent_id, mode, allowed_scopes, child_ttl_ceiling, require_child_signing_key)
70
+ values (v_pol4, v_r4, 'root_minted', jsonb_build_array('scope:a'), interval '8 hours', false);
71
+
59
72
  -- Happy child-a (scope:a), enrolling the caller-supplied public key. Grant #1.
60
73
  select child_agent_id, grant_expires_at into v_ca, v_exp_a
61
74
  from agent_board.mint_delegated_child(v_r1, 'deleg-ca-'||s, jsonb_build_array('scope:a'), v_ca_pubkey);
@@ -69,12 +82,12 @@ begin
69
82
  select child_agent_id into v_cb
70
83
  from agent_board.mint_delegated_child(v_r1, 'deleg-cb-'||s, jsonb_build_array('scope:a'), v_cb_pubkey, interval '30 days');
71
84
 
72
- insert into _deleg_fx values (u_owner, v_r1, v_r2, v_r3, v_ca, v_cb, v_exp_a, 'deleg-cc-'||s, v_ca_pubkey);
85
+ insert into _deleg_fx values (u_owner, v_r1, v_r2, v_r3, v_r4, v_ca, v_cb, v_exp_a, 'deleg-cc-'||s, v_pol4, v_ca_pubkey);
73
86
  end
74
87
  $$;
75
88
 
76
89
  -- Function existence.
77
- select has_function('agent_board', 'mint_delegated_child', array['uuid','text','jsonb','text','interval'], 'mint_delegated_child exists');
90
+ select has_function('agent_board', 'mint_delegated_child', array['uuid','text','jsonb','text','interval','boolean'], 'mint_delegated_child exists');
78
91
  select has_function('agent_board', 'renew_delegated_grant', array['uuid','interval'], 'renew_delegated_grant exists');
79
92
 
80
93
  -- Happy mint: grant row shape.
@@ -170,6 +183,42 @@ select throws_ok(
170
183
  (select format($q$select agent_board.renew_delegated_grant(%L::uuid)$q$, child_a) from _deleg_fx),
171
184
  '42501', null, 'renew raises once the root is inactive (grant cannot outlive root authority)');
172
185
 
186
+ -- Mint-time "root principal active" gate (message-board#87): the bound set in the
187
+ -- header requires an active root at MINT, not only at renew. R1 is now suspended,
188
+ -- so a fresh mint must be rejected by the principal-active gate. The exact errmsg
189
+ -- pins this to that gate (it fires before max_active_children, which would also
190
+ -- raise 42501) — proving root-active is enforced at mint, not incidentally caught.
191
+ select throws_ok(
192
+ (select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:a'), %L)$q$,
193
+ r1, 'deleg-inact-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), ca_pubkey) from _deleg_fx),
194
+ '42501',
195
+ 'principal_inactive: root agent principal is not fully active',
196
+ 'mint rejects when the root principal is inactive (root-active gate holds at mint, not just renew)');
197
+
198
+ -- Mint-time "policy unrevoked" gate (message-board#91): the bound set in the header
199
+ -- requires the governing delegation_policies row to be unrevoked. The policy SELECT
200
+ -- filters `revoked_at is null`, so a revoked policy makes the root's policy not-found
201
+ -- and the "no active delegation policy for root agent" gate fires (42501). R4 is an
202
+ -- independent, active, UNCAPPED root_minted root, so:
203
+ -- ALLOW twin: while v_pol4 is unrevoked, a fresh mint succeeds — proving the fixture
204
+ -- is otherwise sound and the DENY below can only come from the revocation (this pair
205
+ -- distinguishes "revoked" from "no policy at all", since both share the errmsg).
206
+ -- DENY: after revoking v_pol4, the same mint is rejected. The 4-arg exact-errmsg pin
207
+ -- excludes the other 42501 gates (mode/principal/scope/max_active_children).
208
+ select lives_ok(
209
+ (select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:a'))$q$,
210
+ r4, 'deleg-pa-'||replace(substr(gen_random_uuid()::text,1,8),'-','')) from _deleg_fx),
211
+ 'mint succeeds while the governing delegation policy is unrevoked (policy-unrevoked ALLOW twin)');
212
+ select lives_ok(
213
+ (select format($q$update agent_board.delegation_policies set revoked_at = now() where id = %L::uuid$q$, pol4) from _deleg_fx),
214
+ 'revoke the R4 delegation policy');
215
+ select throws_ok(
216
+ (select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:a'))$q$,
217
+ r4, 'deleg-pr-'||replace(substr(gen_random_uuid()::text,1,8),'-','')) from _deleg_fx),
218
+ '42501',
219
+ 'no active delegation policy for root agent',
220
+ 'mint rejects when the governing delegation policy is revoked (policy-unrevoked gate holds at mint)');
221
+
173
222
  select finish();
174
223
 
175
224
  rollback;
@@ -5,7 +5,7 @@ set search_path = extensions, public, agent_board;
5
5
 
6
6
  begin;
7
7
 
8
- select plan(16);
8
+ select plan(26);
9
9
 
10
10
  create temp table _deleg_fixture (
11
11
  board uuid,
@@ -50,6 +50,11 @@ begin
50
50
  end
51
51
  $$;
52
52
  grant select on _deleg_fixture to authenticated;
53
+ -- #152: the sole-writer runtime-deny below reads owner_agent from this fixture while
54
+ -- `set local role service_role` is active (to prove a raw service_role INSERT is denied),
55
+ -- so service_role needs SELECT on the fixture too — otherwise the fixture read (not the
56
+ -- insert) raises and ABORTS the whole file, truncating the run (CLAUDE.md #3).
57
+ grant select on _deleg_fixture to service_role;
53
58
 
54
59
  -- delegation_policies shape -------------------------------------------------
55
60
  select has_table('agent_board', 'delegation_policies', 'delegation_policies table exists');
@@ -115,6 +120,107 @@ select is(
115
120
  (select max_grantable_role from agent_board.board_authority_ceilings c where c.board_id = (select board from _deleg_fixture)),
116
121
  'admin', 'the owner-set ceiling was upserted to admin');
117
122
 
123
+ -- =====================================================================
124
+ -- #152 SOLE-WRITER: upsert_delegation_policy is the ONLY write door to
125
+ -- delegation_policies. service_role's direct INSERT/UPDATE grant is revoked
126
+ -- (20260830000100) so an operator cannot set caps via raw SQL (which would
127
+ -- leave the #142 operator_* ceiling NULL and let a later self-service widen
128
+ -- escape it). SELECT is KEPT.
129
+ --
130
+ -- EC-4-style catalog assertions (self-maintaining; a regression re-grant names
131
+ -- itself): has_table_privilege folds in PUBLIC + role inheritance, so the
132
+ -- mutation-pin (re-grant insert,update to service_role in the migration) flips
133
+ -- exactly the two grant assertions and the runtime-deny throws_ok. The allow-side
134
+ -- lives_ok blocks are a REGRESSION GUARD that the owner-privileged definer path
135
+ -- still functions after the revoke -- they do NOT flip under that mutation (the
136
+ -- function runs as the table OWNER regardless of service_role's grant), and that is
137
+ -- expected, not a coverage gap (CLAUDE.md #2: never claim a property a mutation
138
+ -- cannot break as pinned).
139
+ -- =====================================================================
140
+
141
+ -- The set_board_authority_ceiling block above left request.jwt.claim.* set
142
+ -- (set_config(...,true) persists for the txn; `reset role` does NOT clear GUCs).
143
+ -- Restore the operator context: role=service_role => auth.role()='service_role'
144
+ -- => _is_service_role() true => operator branch of upsert_delegation_policy.
145
+ select set_config('request.jwt.claim.role', 'service_role', true);
146
+ select set_config('request.jwt.claim.sub', '', true);
147
+
148
+ -- Catalog: the closed grant asserted POSITIVELY (false), plus SELECT asserted true
149
+ -- (proves the revoke did not over-reach and drop read access).
150
+ select is(has_table_privilege('service_role', 'agent_board.delegation_policies', 'INSERT'), false,
151
+ '#152: service_role has NO direct INSERT on delegation_policies (sole-writer)');
152
+ select is(has_table_privilege('service_role', 'agent_board.delegation_policies', 'UPDATE'), false,
153
+ '#152: service_role has NO direct UPDATE on delegation_policies (sole-writer)');
154
+ select is(has_table_privilege('service_role', 'agent_board.delegation_policies', 'SELECT'), true,
155
+ '#152: service_role RETAINS direct SELECT on delegation_policies');
156
+
157
+ -- LOAD-BEARING INVARIANT (Fable, #152): with service_role's direct grant revoked, the
158
+ -- ONLY thing keeping the write path alive is that upsert_delegation_policy is SECURITY
159
+ -- DEFINER AND owned by the delegation_policies table owner (so it writes on the owner's
160
+ -- implicit privileges). If it were flipped to SECURITY INVOKER, or reowned to a role
161
+ -- without table privilege, EVERY write path (operator + self-service) fails closed in
162
+ -- production — a total write outage. The pgTAP session user IS the table owner, so
163
+ -- invoker==owner MASKS the break here (all allow-side assertions still pass); only these
164
+ -- catalog assertions catch it.
165
+ -- * prosecdef (SECURITY DEFINER) IS mutation-pinned: `alter function ... security invoker`
166
+ -- flips assertion 20 alone (verified: "failed 1 test of 26", allow-side stays green),
167
+ -- which is the whole point — the invoker break is otherwise silent under the owner session.
168
+ -- * The owner-OID equality is a STRUCTURAL TRIPWIRE, NOT in-harness mutation-pinnable
169
+ -- (CLAUDE.md #2 honest labeling, not a covered claim): postgres cannot reassign the
170
+ -- function's owner to any role that BOTH it can SET ROLE to AND that holds CREATE on
171
+ -- agent_board — `alter function ... owner to service_role/authenticated` is rejected
172
+ -- ("permission denied for schema agent_board") and supabase_admin is not SET ROLE-able.
173
+ -- Postgres's own ownership rules thus enforce the invariant; this assertion is the
174
+ -- named tripwire for a FUTURE migration that recreates the function under a different
175
+ -- owner. Its real write consequence (owner CAN write despite the revoke) is behaviorally
176
+ -- covered by the allow-side lives_ok below, which pass only BECAUSE the revoke is in place.
177
+ select is(
178
+ (select prosecdef from pg_proc
179
+ where oid = 'agent_board.upsert_delegation_policy(uuid,text,jsonb,integer,interval,boolean)'::regprocedure),
180
+ true, '#152: upsert_delegation_policy is SECURITY DEFINER (sole-writer invariant)');
181
+ select is(
182
+ (select proowner from pg_proc
183
+ where oid = 'agent_board.upsert_delegation_policy(uuid,text,jsonb,integer,interval,boolean)'::regprocedure),
184
+ (select relowner from pg_class where oid = 'agent_board.delegation_policies'::regclass),
185
+ '#152: upsert_delegation_policy owner = delegation_policies table owner (definer writes on owner privileges)');
186
+
187
+ -- Runtime proof the raw-SQL door is actually shut: a direct INSERT executed AS
188
+ -- service_role raises 42501. The re-grant mutation flips this to a live insert.
189
+ set local role service_role;
190
+ select throws_ok(
191
+ (select format($q$insert into agent_board.delegation_policies(root_agent_id) values (%L::uuid)$q$, owner_agent) from _deleg_fixture),
192
+ '42501', null,
193
+ '#152: a raw service_role INSERT into delegation_policies is denied (function is the sole writer)');
194
+ reset role;
195
+
196
+ -- Allow-side regression guard: the definer path is UNBROKEN by the revoke.
197
+ -- Operator branch establishes the operator ceiling. #113: run the mutating RPC under
198
+ -- lives_ok, then read state separately (never assert on a bare RPC value expression).
199
+ select lives_ok(
200
+ (select format($q$select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, 3, interval '4 hours', true)$q$, owner_agent) from _deleg_fixture),
201
+ '#152: operator upsert_delegation_policy still succeeds after the revoke (definer runs as table owner)');
202
+ select is(
203
+ (select operator_max_active_children
204
+ from agent_board.delegation_policies dp join _deleg_fixture f on dp.root_agent_id = f.owner_agent
205
+ where dp.revoked_at is null order by dp.created_at desc limit 1),
206
+ 3, '#152: operator upsert established the operator ceiling (operator_max_active_children=3)');
207
+
208
+ -- Self-service branch (role=authenticated, sub=owner_user => _is_service_role() false)
209
+ -- still writes and PRESERVES the operator ceiling (#142): a widen to 25 does NOT
210
+ -- overwrite operator_max_active_children (stays 3).
211
+ select set_config('request.jwt.claim.role', 'authenticated', true);
212
+ select set_config('request.jwt.claim.sub', (select owner_user from _deleg_fixture)::text, true);
213
+ select lives_ok(
214
+ (select format($q$select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', '[]'::jsonb, 25, interval '24 hours', true)$q$, owner_agent) from _deleg_fixture),
215
+ '#152: self-service upsert_delegation_policy still succeeds after the revoke');
216
+ select is(
217
+ (select operator_max_active_children
218
+ from agent_board.delegation_policies dp join _deleg_fixture f on dp.root_agent_id = f.owner_agent
219
+ where dp.revoked_at is null order by dp.created_at desc limit 1),
220
+ 3, '#152: self-service upsert PRESERVED the operator ceiling (still 3, not widened to 25)');
221
+ select set_config('request.jwt.claim.role', '', true);
222
+ select set_config('request.jwt.claim.sub', '', true);
223
+
118
224
  select finish();
119
225
 
120
226
  rollback;
@@ -47,7 +47,7 @@ begin
47
47
 
48
48
  insert into _p values (
49
49
  'https://fed-ok-'||s||'.test', 'message-board', 'portal|'||s, 'fed-agent-'||s,
50
- jsonb_build_object('sub', 'portal|'||s, 'act', jsonb_build_object('sub', 'act|'||s), 'name', 'Fed '||s),
50
+ jsonb_build_object('iss','https://fed-ok-'||s||'.test','aud','message-board','sub', 'portal|'||s, 'act', jsonb_build_object('sub', 'act|'||s), 'name', 'Fed '||s),
51
51
  'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0',
52
52
  'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
53
53
  'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk',
@@ -162,7 +162,7 @@ select is(
162
162
  -- Provider / scope / kind rejection matrix still raises ------------------------------
163
163
  select throws_ok(
164
164
  (select format($q$select agent_board.enroll_federated_identity('https://nope.test',%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
165
- aud, sub, claim::text, 'x-'||slug, pub1, fp1) from _p),
165
+ aud, sub, (claim || jsonb_build_object('iss','https://nope.test'))::text, 'x-'||slug, pub1, fp1) from _p),
166
166
  '42501', null, 'unknown (issuer,audience) is rejected');
167
167
  select throws_ok(
168
168
  (select format($q$select agent_board.enroll_federated_identity(%L,%L,NULL,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
@@ -198,7 +198,7 @@ begin
198
198
  into v_agent, v_kid, v_kver, v_handle
199
199
  from agent_board.enroll_federated_identity(
200
200
  'https://fed-b1-'||s||'.test','message-board','portal|'||s,
201
- jsonb_build_object('sub','portal|'||s), 'b1-'||s, v_pub,'raw_ed25519_base64url', v_fp,
201
+ jsonb_build_object('iss','https://fed-b1-'||s||'.test','aud','message-board','sub','portal|'||s), 'b1-'||s, v_pub,'raw_ed25519_base64url', v_fp,
202
202
  true, null, 'agent', '{board:send}', '{}') e;
203
203
  insert into _b1 values (v_agent, v_kid, v_kver, v_handle);
204
204
  end $$;
@@ -15,7 +15,7 @@ set search_path = extensions, public, agent_board;
15
15
 
16
16
  begin;
17
17
 
18
- select plan(18);
18
+ select plan(20);
19
19
 
20
20
  -- Client<->DB fingerprint round-trip (B2 raw-byte scheme): the DB derivation over the
21
21
  -- DECODED 32 key bytes must equal the constant an independent tool (Node crypto,
@@ -41,7 +41,7 @@ begin
41
41
  values ('https://fed-pop-'||s||'.test', 'message-board', true, true, '{board:send}', '{}', '{agent,human}');
42
42
  insert into _p values (
43
43
  'https://fed-pop-'||s||'.test', 'message-board', 'portal|'||s,
44
- jsonb_build_object('sub','portal|'||s,'act',jsonb_build_object('sub','act|'||s)),
44
+ jsonb_build_object('iss','https://fed-pop-'||s||'.test','aud','message-board','sub','portal|'||s,'act',jsonb_build_object('sub','act|'||s)),
45
45
  'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0', 'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
46
46
  'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk', 'sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA');
47
47
  end
@@ -58,7 +58,17 @@ select is(
58
58
  (select claim->'act'->>'sub' from _p),
59
59
  'the act.sub is captured (audit-only) in the enrollment claim snapshot');
60
60
 
61
- -- Duplicate fingerprint: re-enrolling key 1 raises (dedup on the UNIQUE index).
61
+ -- Exact replay: rotating presentation claims do not create a second identity.
62
+ select lives_ok(
63
+ (select format($q$create temp table _exact_replay as select * from agent_board.enroll_federated_identity(%L,%L,%L,(%L::jsonb || '{"iat":200,"exp":9999999999,"jti":"rotated"}'::jsonb),%L,%L,'raw_ed25519_base64url',%L,true,%L,'agent','{board:send}','{}')$q$,
64
+ iss, aud, sub, claim::text, 'pop-ok', pub1, fp1, 'act|present') from _p),
65
+ 'an exact stable-input replay with rotating JWT presentation claims succeeds');
66
+ select is(
67
+ (select agent_id from _exact_replay),
68
+ (select a.id from agent_board.agents a where a.handle='pop-ok'),
69
+ 'exact replay returns the original server-assigned identity');
70
+
71
+ -- A replay mutation (slug and independent actor_sub) fails closed with 23505.
62
72
  select throws_ok(
63
73
  (select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
64
74
  iss, aud, sub, claim::text, 'pop-dup', pub1, fp1) from _p),