@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,351 @@
|
|
|
1
|
+
-- MBROLERESET pgTAP: role is NOT identity.
|
|
2
|
+
--
|
|
3
|
+
-- Closes #79 on two axes:
|
|
4
|
+
-- (1) agent_board._upsert_board_membership_state clamps `role` to the
|
|
5
|
+
-- board_memberships.role table default ('member') on any transition whose
|
|
6
|
+
-- ORIGIN is a non-active state (requested|invited|left|removed) and whose
|
|
7
|
+
-- TARGET is joined|subscribed. UPDATE branch only -- the INSERT branch already
|
|
8
|
+
-- omits `role` and takes the table default, so a no-prior-row entry needs no clamp.
|
|
9
|
+
-- (2) agent_board.grant_board_role refuses a grant onto a non-active membership:
|
|
10
|
+
-- a membership_state filter in the PRE-CHECK (P0002) *and* mirrored into the
|
|
11
|
+
-- UPDATE (the TOCTOU half, which no serial test can reach -- asserted structurally).
|
|
12
|
+
--
|
|
13
|
+
-- Runs as the postgres db role; request.jwt.claim.role toggles _is_service_role()
|
|
14
|
+
-- (''/service_role => true, 'authenticated' => false) and request.jwt.claim.sub picks
|
|
15
|
+
-- the acting agent. Fixture style mirrors agent_board_roomown.test.sql.
|
|
16
|
+
--
|
|
17
|
+
-- Methodology notes that are load-bearing here (each has produced a false result in
|
|
18
|
+
-- this repo before):
|
|
19
|
+
-- * every negative assertion goes through throws_ok -- the runner fails on ANY line
|
|
20
|
+
-- containing 'ERROR:', so a bare uncaught raise fails the file even when expected.
|
|
21
|
+
-- * an authenticated block carries an explicit _is_service_role() = false control;
|
|
22
|
+
-- `set role` alone leaves session_user = postgres, so the service branch would
|
|
23
|
+
-- satisfy every assertion vacuously.
|
|
24
|
+
-- * both a DENY and an ALLOW are asserted -- a fail-closed bug is invisible to a
|
|
25
|
+
-- "is it locked down?" check.
|
|
26
|
+
create extension if not exists pgtap with schema extensions;
|
|
27
|
+
set search_path = extensions, public, agent_board;
|
|
28
|
+
|
|
29
|
+
begin;
|
|
30
|
+
|
|
31
|
+
select plan(23);
|
|
32
|
+
|
|
33
|
+
create temp table _rr_fixture (
|
|
34
|
+
open_board uuid,
|
|
35
|
+
priv_board uuid,
|
|
36
|
+
owner_agent uuid,
|
|
37
|
+
owner_user uuid,
|
|
38
|
+
admin_agent uuid,
|
|
39
|
+
requester uuid,
|
|
40
|
+
invitee uuid,
|
|
41
|
+
fresh uuid,
|
|
42
|
+
lockout_board uuid,
|
|
43
|
+
lockout_owner uuid,
|
|
44
|
+
lockout_user uuid
|
|
45
|
+
) on commit drop;
|
|
46
|
+
|
|
47
|
+
do $$
|
|
48
|
+
declare
|
|
49
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
50
|
+
v_owner uuid := gen_random_uuid();
|
|
51
|
+
v_owner_user uuid;
|
|
52
|
+
v_admin uuid := gen_random_uuid();
|
|
53
|
+
v_requester uuid := gen_random_uuid();
|
|
54
|
+
v_invitee uuid := gen_random_uuid();
|
|
55
|
+
v_fresh uuid := gen_random_uuid();
|
|
56
|
+
v_lockout_owner uuid := gen_random_uuid();
|
|
57
|
+
v_lockout_user uuid;
|
|
58
|
+
v_open uuid;
|
|
59
|
+
v_priv uuid;
|
|
60
|
+
v_lockout uuid;
|
|
61
|
+
begin
|
|
62
|
+
v_owner_user := public.ensure_github_user('rr-owner-' || v_suffix, 'rr-owner-' || v_suffix || '@example.test', 'rr-owner-' || v_suffix, 'RR Owner');
|
|
63
|
+
v_lockout_user := public.ensure_github_user('rr-lock-' || v_suffix, 'rr-lock-' || v_suffix || '@example.test', 'rr-lock-' || v_suffix, 'RR Lockout');
|
|
64
|
+
|
|
65
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
66
|
+
values
|
|
67
|
+
(v_owner, 'agent', 'rr-owner-' || v_suffix, 'RR Owner', 'active', v_owner_user, v_owner_user),
|
|
68
|
+
(v_lockout_owner, 'agent', 'rr-lock-' || v_suffix, 'RR Lockout', 'active', v_lockout_user, v_lockout_user),
|
|
69
|
+
(v_admin, 'agent', 'rr-admin-' || v_suffix, 'RR Admin', 'active', null, null),
|
|
70
|
+
(v_requester, 'agent', 'rr-req-' || v_suffix, 'RR Requester', 'active', null, null),
|
|
71
|
+
(v_invitee, 'agent', 'rr-inv-' || v_suffix, 'RR Invitee', 'active', null, null),
|
|
72
|
+
(v_fresh, 'agent', 'rr-fresh-' || v_suffix, 'RR Fresh', 'active', null, null);
|
|
73
|
+
|
|
74
|
+
-- Board OPEN: hosts repro B (owner self-leave -> self-rejoin) because join_board's
|
|
75
|
+
-- self path only admits a stranger on an 'open' board.
|
|
76
|
+
select board_id into v_open
|
|
77
|
+
from agent_board.create_board(v_owner, 'rr-open-' || v_suffix, 'RR Open', 'rolereset open', '{}'::jsonb, 'open');
|
|
78
|
+
|
|
79
|
+
-- Board PRIVATE: hosts the per-origin clamp matrix, driven service_role so the
|
|
80
|
+
-- transition itself is never the thing under test.
|
|
81
|
+
select board_id into v_priv
|
|
82
|
+
from agent_board.create_board(v_owner, 'rr-priv-' || v_suffix, 'RR Private', 'rolereset private', '{}'::jsonb, 'private');
|
|
83
|
+
|
|
84
|
+
-- Board LOCKOUT: a board whose sole owner will self-leave and rejoin as member,
|
|
85
|
+
-- to pin the documented no-RPC-recovery limitation.
|
|
86
|
+
select board_id into v_lockout
|
|
87
|
+
from agent_board.create_board(v_lockout_owner, 'rr-lock-' || v_suffix, 'RR Lockout', 'sole owner', '{}'::jsonb, 'open');
|
|
88
|
+
|
|
89
|
+
-- Elevated non-owner memberships whose roles must NOT survive re-entry.
|
|
90
|
+
-- board_memberships_state_left_at (20260515000000:21-25): left/removed REQUIRE a
|
|
91
|
+
-- non-null left_at; requested/invited/joined/subscribed REQUIRE null. Seed distinct
|
|
92
|
+
-- PAST timestamps -- now() is transaction_timestamp() and constant for the whole
|
|
93
|
+
-- transaction, so any later "was this restamped?" comparison written now-vs-now
|
|
94
|
+
-- could not fail.
|
|
95
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role, left_at)
|
|
96
|
+
values
|
|
97
|
+
(v_priv, v_admin, 'removed', v_owner, 'admin', timestamptz '2019-06-01'),
|
|
98
|
+
(v_priv, v_requester, 'requested', v_owner, 'admin', null),
|
|
99
|
+
(v_priv, v_invitee, 'invited', v_owner, 'admin', null);
|
|
100
|
+
|
|
101
|
+
insert into _rr_fixture values
|
|
102
|
+
(v_open, v_priv, v_owner, v_owner_user, v_admin, v_requester, v_invitee, v_fresh,
|
|
103
|
+
v_lockout, v_lockout_owner, v_lockout_user);
|
|
104
|
+
end
|
|
105
|
+
$$;
|
|
106
|
+
|
|
107
|
+
-- 1. The clamp's target value is the table default, and it is still 'member'. -------
|
|
108
|
+
-- The clamp hardcodes 'member' (reading information_schema.column_default at runtime
|
|
109
|
+
-- would yield the quoted EXPRESSION "'member'::text", not the value). This assertion is
|
|
110
|
+
-- the other half of that contract: a future `alter ... set default` turns the suite red
|
|
111
|
+
-- instead of silently diverging from the hardcoded clamp.
|
|
112
|
+
select is(
|
|
113
|
+
(select column_default from information_schema.columns
|
|
114
|
+
where table_schema = 'agent_board' and table_name = 'board_memberships' and column_name = 'role'),
|
|
115
|
+
'''member''::text',
|
|
116
|
+
'board_memberships.role default is still ''member'' (the value the clamp hardcodes)');
|
|
117
|
+
|
|
118
|
+
-- 2. Redemption presets can never cross a non-active -> active edge. ---------------
|
|
119
|
+
-- _redeem_invite_for_agent raw-inserts role=preset_role (admin max). If preset_membership_state
|
|
120
|
+
-- could be 'invited', the target's later acceptance would travel invited->joined THROUGH the
|
|
121
|
+
-- mutator and the clamp would erase the preset. The CHECK forbids it; pin that.
|
|
122
|
+
select is(
|
|
123
|
+
(select count(*)::int from information_schema.check_constraints cc
|
|
124
|
+
join information_schema.constraint_column_usage ccu on ccu.constraint_name = cc.constraint_name
|
|
125
|
+
where ccu.table_schema = 'agent_board'
|
|
126
|
+
and ccu.table_name = 'board_invite_tokens'
|
|
127
|
+
and ccu.column_name = 'preset_membership_state'
|
|
128
|
+
and cc.check_clause like '%joined%'
|
|
129
|
+
and cc.check_clause like '%subscribed%'
|
|
130
|
+
and cc.check_clause not like '%invited%'),
|
|
131
|
+
1,
|
|
132
|
+
'board_invite_tokens.preset_membership_state is constrained to joined|subscribed, so a REDEEMED row is active on arrival and never crosses the clamp (a fresh token grant to a left agent is a separate, intentional path)');
|
|
133
|
+
|
|
134
|
+
-- 3. Redemption bypasses the mutator entirely, so the clamp cannot reach its preset. -
|
|
135
|
+
select ok(
|
|
136
|
+
(select pg_get_functiondef(p.oid) not like '%_upsert_board_membership_state%'
|
|
137
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
138
|
+
where n.nspname = 'agent_board' and p.proname = '_redeem_invite_for_agent'
|
|
139
|
+
limit 1),
|
|
140
|
+
'_redeem_invite_for_agent raw-inserts and never calls the mutator (admin preset survives)');
|
|
141
|
+
|
|
142
|
+
-- 4. create_board's owner seed is a raw insert and is likewise unreachable by the clamp.
|
|
143
|
+
select is(
|
|
144
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.owner_agent),
|
|
145
|
+
'owner',
|
|
146
|
+
'create_board owner seed still lands ''owner'' (raw insert, clamp must not reach it)');
|
|
147
|
+
|
|
148
|
+
-- ==================================================================================
|
|
149
|
+
-- The clamp matrix. Driven as service_role so the TRANSITION is never what is under
|
|
150
|
+
-- test -- only the resulting role. Per the ruling, service_role gets NO exemption:
|
|
151
|
+
-- an operator re-activating an evicted agent gets the default role like anyone else.
|
|
152
|
+
-- ==================================================================================
|
|
153
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
154
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
155
|
+
|
|
156
|
+
-- 5. removed -> joined clamps (this is repro A's second half).
|
|
157
|
+
select agent_board.update_board_membership(priv_board, admin_agent, 'joined') from _rr_fixture;
|
|
158
|
+
select is(
|
|
159
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.admin_agent),
|
|
160
|
+
'member',
|
|
161
|
+
'removed -> joined clamps role to the table default (a re-admitted admin is not admin)');
|
|
162
|
+
|
|
163
|
+
-- 6. requested -> joined clamps.
|
|
164
|
+
select agent_board.update_board_membership(priv_board, requester, 'joined') from _rr_fixture;
|
|
165
|
+
select is(
|
|
166
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.requester),
|
|
167
|
+
'member',
|
|
168
|
+
'requested -> joined clamps role to the table default');
|
|
169
|
+
|
|
170
|
+
-- 7. invited -> subscribed clamps. Deliberately targets SUBSCRIBED, not joined: the
|
|
171
|
+
-- invariant covers entry into BOTH active states, and an implementation that clamps only
|
|
172
|
+
-- 'joined' would pass a joined-only matrix while a stale role survived via subscribed.
|
|
173
|
+
select agent_board.update_board_membership(priv_board, invitee, 'subscribed') from _rr_fixture;
|
|
174
|
+
select is(
|
|
175
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.invitee),
|
|
176
|
+
'member',
|
|
177
|
+
'invited -> SUBSCRIBED clamps role to the table default (subscribed is not a clamp hole)');
|
|
178
|
+
|
|
179
|
+
-- 8. left -> subscribed clamps. Second subscribed-target origin.
|
|
180
|
+
select agent_board.update_board_membership(priv_board, admin_agent, 'left') from _rr_fixture;
|
|
181
|
+
-- Re-elevate role ONLY (state and left_at untouched) so the next transition's origin is
|
|
182
|
+
-- a genuine non-active row carrying a non-default role.
|
|
183
|
+
update agent_board.board_memberships bm set role = 'admin'
|
|
184
|
+
from _rr_fixture f where bm.board_id = f.priv_board and bm.agent_id = f.admin_agent;
|
|
185
|
+
select agent_board.update_board_membership(priv_board, admin_agent, 'subscribed') from _rr_fixture;
|
|
186
|
+
select is(
|
|
187
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.admin_agent),
|
|
188
|
+
'member',
|
|
189
|
+
'left -> SUBSCRIBED clamps role to the table default');
|
|
190
|
+
|
|
191
|
+
-- 9. ACTIVE -> ACTIVE NO-REGRESSION. The one edge the clamp must NOT fire on: an
|
|
192
|
+
-- already-active membership changing between joined and subscribed keeps its role,
|
|
193
|
+
-- including 'owner'. Without this the clamp would silently demote every owner who
|
|
194
|
+
-- toggles subscription.
|
|
195
|
+
select agent_board.update_board_membership(priv_board, owner_agent, 'subscribed') from _rr_fixture;
|
|
196
|
+
select is(
|
|
197
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.owner_agent),
|
|
198
|
+
'owner',
|
|
199
|
+
'joined -> subscribed on an ACTIVE membership PRESERVES owner (active->active is not clamped)');
|
|
200
|
+
|
|
201
|
+
-- 10. No-prior-row entry takes the table default via the INSERT branch, with no clamp
|
|
202
|
+
-- involved. Recorded so the INSERT branch is never "fixed" to write role explicitly.
|
|
203
|
+
select agent_board.join_board(open_board, fresh, 'joined') from _rr_fixture;
|
|
204
|
+
select is(
|
|
205
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.open_board and bm.agent_id = f.fresh),
|
|
206
|
+
'member',
|
|
207
|
+
'first-time join lands ''member'' via the table default (INSERT branch omits role)');
|
|
208
|
+
|
|
209
|
+
-- ==================================================================================
|
|
210
|
+
-- grant_board_role: the second axis.
|
|
211
|
+
-- ==================================================================================
|
|
212
|
+
|
|
213
|
+
-- 11. ALLOW control: a grant onto an ACTIVE membership still works and really changes
|
|
214
|
+
-- the role. A DENY-only suite cannot tell "correctly filtered" from "broken for everyone".
|
|
215
|
+
-- invitee is ACTIVE (subscribed) on priv_board after assertion 7, and owner_agent still
|
|
216
|
+
-- holds 'owner' there after the active->active no-regression at 9, so the ceiling permits admin.
|
|
217
|
+
select lives_ok(
|
|
218
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin', %L::uuid)$q$, priv_board, invitee, owner_agent) from _rr_fixture),
|
|
219
|
+
'service_role: grant_board_role onto an ACTIVE (subscribed) membership still succeeds');
|
|
220
|
+
|
|
221
|
+
-- 11b. ...and it REALLY CHANGED THE ROLE. lives_ok alone is not an ALLOW control: an
|
|
222
|
+
-- implementation that correctly refuses non-active grants but silently no-ops ACTIVE ones
|
|
223
|
+
-- passes lives_ok. Read the role back.
|
|
224
|
+
select is(
|
|
225
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.invitee),
|
|
226
|
+
'admin',
|
|
227
|
+
'the ALLOW grant actually wrote the role (lives_ok alone would pass against a silent no-op)');
|
|
228
|
+
|
|
229
|
+
-- 12. REPRO A (first half): grant_board_role onto a NON-ACTIVE membership is refused.
|
|
230
|
+
-- Today this SUCCEEDS and writes role onto the removed row -- the live WHERE clause is
|
|
231
|
+
-- board_id+agent_id only, so it MATCHES. That is the #79 re-arm.
|
|
232
|
+
select agent_board.update_board_membership(priv_board, requester, 'removed') from _rr_fixture;
|
|
233
|
+
select throws_ok(
|
|
234
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin', %L::uuid)$q$, priv_board, requester, owner_agent) from _rr_fixture),
|
|
235
|
+
'P0002',
|
|
236
|
+
'grantee has no membership on this board',
|
|
237
|
+
'grant_board_role onto a REMOVED membership raises P0002 (the #79 re-arm is closed)');
|
|
238
|
+
|
|
239
|
+
-- 13. The pre-check must not have become a no-row oracle: an agent with NO row at all
|
|
240
|
+
-- raises the same P0002, so a removed grantee is not distinguishable from an absent one.
|
|
241
|
+
select throws_ok(
|
|
242
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'member', %L::uuid)$q$, priv_board, fresh, owner_agent) from _rr_fixture),
|
|
243
|
+
'P0002',
|
|
244
|
+
'grantee has no membership on this board',
|
|
245
|
+
'a no-row grantee raises the SAME P0002 as a removed one (no error-code oracle)');
|
|
246
|
+
|
|
247
|
+
-- 14. The removed row's role was not mutated by the refused grant.
|
|
248
|
+
select is(
|
|
249
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.priv_board and bm.agent_id = f.requester),
|
|
250
|
+
'member',
|
|
251
|
+
'the refused grant left the removed row''s role untouched');
|
|
252
|
+
|
|
253
|
+
-- 15. STRUCTURAL: the mirrored UPDATE-side predicate is present in the shipped body.
|
|
254
|
+
-- No serial test can reach this path -- a pre-check-only implementation passes every
|
|
255
|
+
-- behavioural assertion above, because the UPDATE is never reached with a non-active row
|
|
256
|
+
-- unless an eviction lands between the pre-check and the UPDATE (the TOCTOU window).
|
|
257
|
+
-- Both review seats flagged this independently; assert the predicate structurally.
|
|
258
|
+
select ok(
|
|
259
|
+
(select (
|
|
260
|
+
select count(*) from regexp_matches(
|
|
261
|
+
pg_get_functiondef(p.oid),
|
|
262
|
+
'membership_state\s+in\s*\(\s*''joined''\s*,\s*''subscribed''\s*\)',
|
|
263
|
+
'g')) >= 2
|
|
264
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
265
|
+
where n.nspname = 'agent_board' and p.proname = 'grant_board_role'
|
|
266
|
+
limit 1),
|
|
267
|
+
'grant_board_role carries the active-state predicate TWICE (pre-check + UPDATE, the TOCTOU half)');
|
|
268
|
+
|
|
269
|
+
-- 16. STRUCTURAL: the clamp lives in the mutator, not in a trigger. A BEFORE UPDATE
|
|
270
|
+
-- trigger on board_memberships would also fire for the four raw-insert seeders and
|
|
271
|
+
-- erase create_board's owner seed and ROOMLINK's admin preset.
|
|
272
|
+
select is(
|
|
273
|
+
(select count(*)::int from pg_trigger t
|
|
274
|
+
join pg_class c on c.oid = t.tgrelid
|
|
275
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
276
|
+
where n.nspname = 'agent_board' and c.relname = 'board_memberships'
|
|
277
|
+
and not t.tgisinternal),
|
|
278
|
+
0,
|
|
279
|
+
'no user trigger on board_memberships (the clamp is mutator-sited, never a trigger)');
|
|
280
|
+
|
|
281
|
+
-- 17. STRUCTURAL: the mutator's superseding comment no longer claims it never writes role.
|
|
282
|
+
select ok(
|
|
283
|
+
(select obj_description(p.oid, 'pg_proc') not like '%never writes%'
|
|
284
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
285
|
+
where n.nspname = 'agent_board' and p.proname = '_upsert_board_membership_state'
|
|
286
|
+
limit 1),
|
|
287
|
+
'the mutator''s comment was re-issued and no longer contradicts the body');
|
|
288
|
+
|
|
289
|
+
-- ==================================================================================
|
|
290
|
+
-- REPRO B + the authenticated principal. From here the caller is a real authenticated
|
|
291
|
+
-- agent, NOT service_role.
|
|
292
|
+
-- ==================================================================================
|
|
293
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
294
|
+
select set_config('request.jwt.claim.sub', (select owner_user from _rr_fixture)::text, true);
|
|
295
|
+
|
|
296
|
+
-- 18. The vacuity control. Without this, every assertion below could pass through the
|
|
297
|
+
-- service branch while proving nothing: `set role` does not change session_user, so
|
|
298
|
+
-- _is_service_role() would return TRUE and the authenticated path would never execute.
|
|
299
|
+
select is(agent_board._is_service_role(), false,
|
|
300
|
+
'control: the authenticated block really is NOT service_role (assertions below are not vacuous)');
|
|
301
|
+
|
|
302
|
+
-- 19. REPRO B: the owner self-leaves the OPEN board...
|
|
303
|
+
select lives_ok(
|
|
304
|
+
(select format($q$select agent_board.leave_board(%L::uuid, %L::uuid)$q$, open_board, owner_agent) from _rr_fixture),
|
|
305
|
+
'the owner can self-leave the open board');
|
|
306
|
+
|
|
307
|
+
-- 20. ...and self-rejoining returns them at the DEFAULT role, not owner. join_board
|
|
308
|
+
-- excludes only 'removed' from self-join, so 'left' was the strictly safer attacker
|
|
309
|
+
-- state and the whole re-arm chain routed around a reset-on-removal fix.
|
|
310
|
+
select agent_board.join_board(open_board, owner_agent, 'joined') from _rr_fixture;
|
|
311
|
+
select is(
|
|
312
|
+
(select role from agent_board.board_memberships bm join _rr_fixture f on bm.board_id = f.open_board and bm.agent_id = f.owner_agent),
|
|
313
|
+
'member',
|
|
314
|
+
'owner self-leave -> self-rejoin on an OPEN board returns at ''member'', not ''owner'' (#79 repro B closed)');
|
|
315
|
+
|
|
316
|
+
-- 21. SOLE-OWNER LOCKOUT: grant_board_role cannot recover it. _grant_within_ceiling is
|
|
317
|
+
-- checked UNCONDITIONALLY -- no service_role bypass -- so an operator must nominate a
|
|
318
|
+
-- grantor that already holds rank, and in a sole-owner lockout none exists.
|
|
319
|
+
-- NOTE (CR correction): this does NOT mean "no RPC recovery" exists, as an earlier draft
|
|
320
|
+
-- of this suite and the CHANGELOG both claimed. bootstrap_board_owner_memberships IS a
|
|
321
|
+
-- service_role RPC and raw-inserts joined/owner for a creator with no ACTIVE membership,
|
|
322
|
+
-- so leave-again + bootstrap is a genuine RPC recovery path. Assertion 22 proves it.
|
|
323
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
324
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
325
|
+
select agent_board.leave_board(lockout_board, lockout_owner) from _rr_fixture;
|
|
326
|
+
select agent_board.join_board(lockout_board, lockout_owner, 'joined') from _rr_fixture;
|
|
327
|
+
select throws_ok(
|
|
328
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'owner', %L::uuid)$q$, lockout_board, lockout_owner, lockout_owner) from _rr_fixture),
|
|
329
|
+
'42501',
|
|
330
|
+
'grant exceeds grantor role or board authority ceiling',
|
|
331
|
+
'sole-owner lockout has NO grant_board_role recovery even as service_role (ceiling has no service bypass)');
|
|
332
|
+
|
|
333
|
+
-- 22. The REAL recovery path, exercised as an RPC rather than asserted by hand. An earlier
|
|
334
|
+
-- draft did `update ... set role='owner'` and then read it back, which is tautological -- it
|
|
335
|
+
-- proved SQL works, not that a recovery path exists. Leave again so the creator has no ACTIVE
|
|
336
|
+
-- membership, then call bootstrap_board_owner_memberships(); it raw-inserts joined/owner.
|
|
337
|
+
select agent_board.leave_board(lockout_board, lockout_owner) from _rr_fixture;
|
|
338
|
+
select agent_board.bootstrap_board_owner_memberships();
|
|
339
|
+
select is(
|
|
340
|
+
(select role from agent_board.board_memberships bm
|
|
341
|
+
join _rr_fixture f on bm.board_id = f.lockout_board and bm.agent_id = f.lockout_owner
|
|
342
|
+
where bm.membership_state in ('joined','subscribed')),
|
|
343
|
+
'owner',
|
|
344
|
+
'RPC recovery EXISTS: leave-again + bootstrap_board_owner_memberships restores an active owner');
|
|
345
|
+
|
|
346
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
347
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
348
|
+
|
|
349
|
+
select finish();
|
|
350
|
+
|
|
351
|
+
rollback;
|
|
@@ -9,7 +9,7 @@ set search_path = extensions, public, agent_board;
|
|
|
9
9
|
|
|
10
10
|
begin;
|
|
11
11
|
|
|
12
|
-
select plan(
|
|
12
|
+
select plan(12);
|
|
13
13
|
|
|
14
14
|
create temp table _roomdeleg_fixture (
|
|
15
15
|
board uuid,
|
|
@@ -98,8 +98,10 @@ select throws_ok(
|
|
|
98
98
|
|
|
99
99
|
-- 4. owner (Alice) flips false->true (pinned to a single assertion) -----------------
|
|
100
100
|
select set_config('request.jwt.claim.sub', (select alice_user from _roomdeleg_fixture)::text, true);
|
|
101
|
-
select
|
|
102
|
-
|
|
101
|
+
select lives_ok($$
|
|
102
|
+
select agent_board.set_board_role_gate((select board from _roomdeleg_fixture), true) $$,
|
|
103
|
+
'set_board_role_gate: the owner flips the gate [call succeeds]');
|
|
104
|
+
select is((select role_gate_enforced from agent_board.boards where id = (select board from _roomdeleg_fixture)),
|
|
103
105
|
true, 'set_board_role_gate: the owner flips role_gate_enforced false->true');
|
|
104
106
|
|
|
105
107
|
-- === Part B: the flipped gate restricts invite/resolve (gate now ON) ==============
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
-- ROOMDELEG-2 / Lane B (#59): self-service delegation POLICY writer for an
|
|
2
|
+
-- authenticated root. (The minter opening is deferred to #138 — this suite asserts it
|
|
3
|
+
-- stays service_role-only.)
|
|
4
|
+
--
|
|
5
|
+
-- Runs as the postgres db role; request.jwt.claim.role toggles _is_service_role()
|
|
6
|
+
-- (''/service_role => true, 'authenticated' => false) and request.jwt.claim.sub picks
|
|
7
|
+
-- the acting agent (auth.uid() -> agents.auth_user_id -> current_agent_id()).
|
|
8
|
+
--
|
|
9
|
+
-- Every property is mutation-oriented: the self-service CLAMPS (signing-key pinned,
|
|
10
|
+
-- child cap bounded, TTL capped, depth pinned), the scope_attestation PROVENANCE stamp,
|
|
11
|
+
-- the strict actor binding, and the EXECUTE grant class (policy writer open to
|
|
12
|
+
-- authenticated; minter NOT). Allow-side assertions use lives_ok(...) + a separate state
|
|
13
|
+
-- read (never is((select ... from rpc(...)))), so an allow that silently raises cannot
|
|
14
|
+
-- pass as green (#113).
|
|
15
|
+
|
|
16
|
+
begin;
|
|
17
|
+
select plan(19);
|
|
18
|
+
|
|
19
|
+
-- A non-all-'A' 43-char base64url raw-Ed25519 key (matches the mint's key regex).
|
|
20
|
+
create temporary table _lb_fx (
|
|
21
|
+
root uuid,
|
|
22
|
+
root_user uuid,
|
|
23
|
+
other uuid,
|
|
24
|
+
other_user uuid,
|
|
25
|
+
pubkey text
|
|
26
|
+
) on commit drop;
|
|
27
|
+
|
|
28
|
+
do $$
|
|
29
|
+
declare
|
|
30
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
31
|
+
v_root uuid := gen_random_uuid();
|
|
32
|
+
v_other uuid := gen_random_uuid();
|
|
33
|
+
v_root_user uuid;
|
|
34
|
+
v_other_user uuid;
|
|
35
|
+
begin
|
|
36
|
+
v_root_user := public.ensure_github_user('lb-root-' || v_suffix, 'lb-root-' || v_suffix || '@example.test', 'lb-root-' || v_suffix, 'LB Root');
|
|
37
|
+
v_other_user := public.ensure_github_user('lb-other-' || v_suffix, 'lb-other-' || v_suffix || '@example.test', 'lb-other-' || v_suffix, 'LB Other');
|
|
38
|
+
|
|
39
|
+
-- Both are TRUE lineage roots (parent null, not delegated children).
|
|
40
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
41
|
+
values
|
|
42
|
+
(v_root, 'agent', 'lb-root-' || v_suffix, 'LB Root', 'active', v_root_user, v_root_user),
|
|
43
|
+
(v_other, 'agent', 'lb-other-' || v_suffix, 'LB Other', 'active', v_other_user, v_other_user);
|
|
44
|
+
|
|
45
|
+
insert into _lb_fx values (
|
|
46
|
+
v_root, v_root_user, v_other, v_other_user,
|
|
47
|
+
-- 43 base64url chars, not all-'A'
|
|
48
|
+
'Bcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH');
|
|
49
|
+
end
|
|
50
|
+
$$;
|
|
51
|
+
|
|
52
|
+
-- ---------------------------------------------------------------------------
|
|
53
|
+
-- Existence
|
|
54
|
+
-- ---------------------------------------------------------------------------
|
|
55
|
+
select has_function('agent_board', 'upsert_delegation_policy',
|
|
56
|
+
array['uuid','text','jsonb','integer','interval','boolean'], 'upsert_delegation_policy exists');
|
|
57
|
+
select has_column('agent_board', 'delegation_policies', 'scope_attestation', 'delegation_policies.scope_attestation exists');
|
|
58
|
+
|
|
59
|
+
-- ===========================================================================
|
|
60
|
+
-- SELF-SERVICE PATH (authenticated root == actor)
|
|
61
|
+
-- ===========================================================================
|
|
62
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
63
|
+
select set_config('request.jwt.claim.sub', (select root_user from _lb_fx)::text, true);
|
|
64
|
+
|
|
65
|
+
-- M3: the self-service write stamps scope_attestation='self_declared' (advisory).
|
|
66
|
+
-- M1: require_child_signing_key is PINNED true even though false was requested.
|
|
67
|
+
-- M2a: max_active_children null is CLAMPED to the non-null default (5).
|
|
68
|
+
-- M6a: child_ttl_ceiling 12h is accepted and stored; max_depth pinned 1.
|
|
69
|
+
select lives_ok(
|
|
70
|
+
format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', jsonb_build_array('scope:a'), null, interval '12 hours', false) $$,
|
|
71
|
+
(select root from _lb_fx)),
|
|
72
|
+
'self-service upsert_delegation_policy lives');
|
|
73
|
+
|
|
74
|
+
select is((select scope_attestation from agent_board.delegation_policies where root_agent_id = (select root from _lb_fx) and revoked_at is null order by created_at desc limit 1),
|
|
75
|
+
'self_declared', 'M3: self-service scopes are stamped self_declared');
|
|
76
|
+
select is((select require_child_signing_key from agent_board.delegation_policies where root_agent_id = (select root from _lb_fx) and revoked_at is null order by created_at desc limit 1),
|
|
77
|
+
true, 'M1: require_child_signing_key pinned true (requested false ignored)');
|
|
78
|
+
select is((select max_active_children from agent_board.delegation_policies where root_agent_id = (select root from _lb_fx) and revoked_at is null order by created_at desc limit 1),
|
|
79
|
+
5, 'M2a: null max_active_children clamped to non-null default 5');
|
|
80
|
+
select is((select max_depth from agent_board.delegation_policies where root_agent_id = (select root from _lb_fx) and revoked_at is null order by created_at desc limit 1),
|
|
81
|
+
1, 'max_depth pinned 1');
|
|
82
|
+
select is((select child_ttl_ceiling from agent_board.delegation_policies where root_agent_id = (select root from _lb_fx) and revoked_at is null order by created_at desc limit 1),
|
|
83
|
+
interval '12 hours', 'M6: within-cap ttl (12h) stored');
|
|
84
|
+
|
|
85
|
+
-- M2b/M2c: out-of-range child cap rejected (both ends).
|
|
86
|
+
select throws_ok(
|
|
87
|
+
format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', jsonb_build_array('scope:a'), 26) $$, (select root from _lb_fx)),
|
|
88
|
+
'22023', null, 'M2b: max_active_children 26 (> 25) rejected');
|
|
89
|
+
select throws_ok(
|
|
90
|
+
format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', jsonb_build_array('scope:a'), 0) $$, (select root from _lb_fx)),
|
|
91
|
+
'22023', null, 'M2c: max_active_children 0 (< 1) rejected');
|
|
92
|
+
|
|
93
|
+
-- M6b: over-cap ttl rejected.
|
|
94
|
+
select throws_ok(
|
|
95
|
+
format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', jsonb_build_array('scope:a'), 5, interval '90 days') $$, (select root from _lb_fx)),
|
|
96
|
+
'22023', null, 'M6b: child_ttl_ceiling 90 days (> 24h) rejected');
|
|
97
|
+
|
|
98
|
+
-- M4 (policy writer): an authenticated caller cannot write UNDER A DIFFERENT ROOT.
|
|
99
|
+
select set_config('request.jwt.claim.sub', (select other_user from _lb_fx)::text, true);
|
|
100
|
+
select throws_ok(
|
|
101
|
+
format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', jsonb_build_array('scope:a')) $$, (select root from _lb_fx)),
|
|
102
|
+
'42501', null, 'M4: authenticated OTHER cannot upsert policy under a different root');
|
|
103
|
+
|
|
104
|
+
-- ===========================================================================
|
|
105
|
+
-- OPERATOR PATH (service_role): trusted — scopes operator_vetted, caps as supplied.
|
|
106
|
+
-- ===========================================================================
|
|
107
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
108
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
109
|
+
|
|
110
|
+
select lives_ok(
|
|
111
|
+
format($$ select agent_board.upsert_delegation_policy(%L::uuid, 'root_minted', jsonb_build_array('scope:a'), null, interval '8 hours', false) $$, (select root from _lb_fx)),
|
|
112
|
+
'operator upsert_delegation_policy lives');
|
|
113
|
+
select is((select scope_attestation from agent_board.delegation_policies where root_agent_id = (select root from _lb_fx) and revoked_at is null order by created_at desc limit 1),
|
|
114
|
+
'operator_vetted', 'operator scopes are operator_vetted');
|
|
115
|
+
select is((select require_child_signing_key from agent_board.delegation_policies where root_agent_id = (select root from _lb_fx) and revoked_at is null order by created_at desc limit 1),
|
|
116
|
+
false, 'operator require_child_signing_key=false honored (trusted)');
|
|
117
|
+
|
|
118
|
+
-- ===========================================================================
|
|
119
|
+
-- M5 — EXECUTE grant class. The POLICY writer is executable by authenticated
|
|
120
|
+
-- (not anon). The MINTER is DEFERRED: it must remain service_role-only — assert
|
|
121
|
+
-- authenticated does NOT gain EXECUTE, proving the narrowing held (opening it needs
|
|
122
|
+
-- child-key PoP + raw-byte fingerprint first, tracked in #138).
|
|
123
|
+
-- ===========================================================================
|
|
124
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
125
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
126
|
+
|
|
127
|
+
select ok(has_function_privilege('authenticated',
|
|
128
|
+
'agent_board.upsert_delegation_policy(uuid, text, jsonb, integer, interval, boolean)', 'EXECUTE'),
|
|
129
|
+
'M5: authenticated may EXECUTE upsert_delegation_policy');
|
|
130
|
+
select ok(not has_function_privilege('anon',
|
|
131
|
+
'agent_board.upsert_delegation_policy(uuid, text, jsonb, integer, interval, boolean)', 'EXECUTE'),
|
|
132
|
+
'M5: anon may NOT EXECUTE upsert_delegation_policy');
|
|
133
|
+
select ok(not has_function_privilege('authenticated',
|
|
134
|
+
'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean)', 'EXECUTE'),
|
|
135
|
+
'M5: mint_delegated_child stays service_role-only — authenticated has NO EXECUTE (deferred to #138)');
|
|
136
|
+
select ok(not has_function_privilege('anon',
|
|
137
|
+
'agent_board.mint_delegated_child(uuid, text, jsonb, text, interval, boolean)', 'EXECUTE'),
|
|
138
|
+
'M5: anon may NOT EXECUTE mint_delegated_child');
|
|
139
|
+
|
|
140
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
141
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
142
|
+
|
|
143
|
+
select * from finish();
|
|
144
|
+
rollback;
|
|
@@ -22,7 +22,7 @@ set search_path = extensions, public, agent_board;
|
|
|
22
22
|
|
|
23
23
|
begin;
|
|
24
24
|
|
|
25
|
-
select plan(
|
|
25
|
+
select plan(38);
|
|
26
26
|
|
|
27
27
|
create temp table _lf (
|
|
28
28
|
board uuid,
|
|
@@ -187,10 +187,20 @@ select throws_ok(
|
|
|
187
187
|
select set_config('request.jwt.claim.role', '', true);
|
|
188
188
|
select set_config('request.jwt.claim.sub', '', true);
|
|
189
189
|
|
|
190
|
+
-- Persisted-role reader (message-board#113): allow-side redeem assertions read the seated role
|
|
191
|
+
-- from the table via this getter, not from the RPC return in a bare select -- so an over-refusing
|
|
192
|
+
-- gate surfaces as a NAMED `not ok` from the paired lives_ok instead of aborting the file.
|
|
193
|
+
create function pg_temp.rl_role(ag uuid) returns text language sql as $$
|
|
194
|
+
select role from agent_board.board_memberships bm join _lf f on bm.board_id = f.board and bm.agent_id = ag
|
|
195
|
+
where bm.membership_state in ('joined','subscribed') limit 1;
|
|
196
|
+
$$;
|
|
197
|
+
|
|
190
198
|
-- === C. redeem basics ================================================== (3)
|
|
191
199
|
-- 9
|
|
192
|
-
select
|
|
193
|
-
|
|
200
|
+
select lives_ok($$
|
|
201
|
+
select agent_board._redeem_invite_for_agent((select code_member from _lf), (select carol from _lf)) $$,
|
|
202
|
+
'redeem seats Carol [call succeeds]');
|
|
203
|
+
select is(pg_temp.rl_role((select carol from _lf)),
|
|
194
204
|
'member', 'redeem seats Carol with the member role');
|
|
195
205
|
-- 10
|
|
196
206
|
select is(
|
|
@@ -204,8 +214,10 @@ select is(
|
|
|
204
214
|
|
|
205
215
|
-- === D. idempotency + exhaustion (maxed link) ========================== (5)
|
|
206
216
|
-- 12
|
|
207
|
-
select
|
|
208
|
-
|
|
217
|
+
select lives_ok($$
|
|
218
|
+
select agent_board._redeem_invite_for_agent((select code_maxed from _lf), (select frank from _lf)) $$,
|
|
219
|
+
'first redeem of the maxed link seats Frank [call succeeds]');
|
|
220
|
+
select is(pg_temp.rl_role((select frank from _lf)),
|
|
209
221
|
'member', 'first redeem of the maxed link seats Frank');
|
|
210
222
|
-- 13
|
|
211
223
|
select is(
|
|
@@ -227,8 +239,10 @@ select throws_ok(
|
|
|
227
239
|
|
|
228
240
|
-- === E. no-downgrade (rank-max) ======================================== (2)
|
|
229
241
|
-- 17
|
|
230
|
-
select
|
|
231
|
-
|
|
242
|
+
select lives_ok($$
|
|
243
|
+
select agent_board._redeem_invite_for_agent((select code_member from _lf), (select dave from _lf)) $$,
|
|
244
|
+
'redeeming a member link as Dave [call succeeds]');
|
|
245
|
+
select is(pg_temp.rl_role((select dave from _lf)),
|
|
232
246
|
'admin', 'redeeming a member link never downgrades Dave''s existing admin role (R-LINK-1)');
|
|
233
247
|
-- 18
|
|
234
248
|
select is(
|
|
@@ -294,8 +308,10 @@ select throws_ok(
|
|
|
294
308
|
|
|
295
309
|
-- === L. public redeem happy path ======================================= (1)
|
|
296
310
|
-- 27
|
|
297
|
-
select
|
|
298
|
-
|
|
311
|
+
select lives_ok($$
|
|
312
|
+
select agent_board.redeem_invite_link((select code_public from _lf)) $$,
|
|
313
|
+
'the public redeem_invite_link succeeds [call succeeds]');
|
|
314
|
+
select is(pg_temp.rl_role((select carol from _lf)),
|
|
299
315
|
'member', 'the public redeem_invite_link resolves the actor via current_agent_id() and seats Carol');
|
|
300
316
|
select set_config('request.jwt.claim.role', '', true);
|
|
301
317
|
select set_config('request.jwt.claim.sub', '', true);
|