@consiliency/agent-board-schema 1.12.0 → 1.22.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.
- package/fixtures/already-applied.json +124 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +282 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -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(
|
|
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(
|
|
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;
|
|
@@ -17,7 +17,7 @@ set search_path = extensions, public, agent_board;
|
|
|
17
17
|
|
|
18
18
|
begin;
|
|
19
19
|
|
|
20
|
-
select plan(
|
|
20
|
+
select plan(37);
|
|
21
21
|
|
|
22
22
|
-- ---------------------------------------------------------------------------
|
|
23
23
|
-- Phase 0 (superuser): function metadata + privileges
|
|
@@ -184,10 +184,12 @@ select ok(
|
|
|
184
184
|
|
|
185
185
|
-- Idempotency + L-4b ---------------------------------------------------------------------
|
|
186
186
|
-- 24
|
|
187
|
-
select
|
|
188
|
-
|
|
187
|
+
select lives_ok($$
|
|
188
|
+
select agent_board.enroll_federated_human(
|
|
189
189
|
(select iss from _f), 'message-board', (select sub_a from _f),
|
|
190
|
-
jsonb_build_object('sub', (select sub_a from _f), 'email_verified', true), 'Alice again')
|
|
190
|
+
jsonb_build_object('sub', (select sub_a from _f), 'email_verified', true), 'Alice again') $$,
|
|
191
|
+
're-provision of the same (issuer,sub) succeeds [call succeeds]');
|
|
192
|
+
select is((select id from agent_board.agents where auth_user_id = (select auth_user_id from _r where label='A')),
|
|
191
193
|
(select agent_id from _r where label='A'),
|
|
192
194
|
're-provision of the same (issuer,sub) is idempotent (same agent_id)');
|
|
193
195
|
-- 25
|