@consiliency/agent-board-schema 1.63.0 → 1.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/fixtures/already-applied.json +4 -0
- package/migrations/20261014000200_agent_board_last_owner.sql +239 -0
- package/package.json +1 -1
- package/schema-manifest.json +12 -3
- package/src/tooling.test.mjs +6 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +3 -1
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +7 -0
- package/tests/agent_board_fk_lock_edges.test.sql +3 -2
- package/tests/agent_board_grant_target_role.test.sql +4 -4
- package/tests/agent_board_hosted_delegates.test.sql +5 -3
- package/tests/agent_board_invite_no_downgrade.test.sql +5 -3
- package/tests/agent_board_ledger_authority.test.sql +7 -0
- package/tests/agent_board_role_guard.test.sql +319 -0
- package/tests/agent_board_rolereset.test.sql +17 -27
- package/tests/agent_board_roomdeleg.test.sql +8 -0
- package/tests/agent_board_roomown.test.sql +3 -1
- package/tests/agent_board_rowid.test.sql +4 -1
- package/tests/catalog/function_execute_privileges.json +9 -0
- package/tests/catalog/p_metadata_parameters.json +4 -0
- package/tests/isolation/expected/last_owner_departure.out +562 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +8 -3
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +0 -3
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +0 -3
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +0 -3
- package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_board_scope.spec +0 -2
- package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +0 -2
- package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +0 -2
- package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +0 -2
- package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +0 -2
- package/tests/isolation/specs/consent_approval_lock.spec +0 -1
- package/tests/isolation/specs/consent_emit_actual_state.spec +0 -1
- package/tests/isolation/specs/grant_target_role_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +0 -1
- package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +0 -1
- package/tests/isolation/specs/last_owner_departure.spec +137 -0
- package/tests/isolation/specs/ledger_authority_revoke.spec +0 -3
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +0 -3
- package/tests/isolation/specs/outbox_wake_ordering.spec +0 -1
- package/tests/isolation/specs/session_identity_links.spec +3 -1
- package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +0 -1
- package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +0 -1
- package/tests/isolation/specs/signer_lifecycle_revoke.spec +0 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
-- Consiliency/message-board#675: actual authenticated RPCs, table paths,
|
|
2
|
+
-- catalog-derived writer classes, and paired allow cases. Mutations are recorded
|
|
3
|
+
-- in plans/evidence/roleguard; every run must emit its full plan.
|
|
4
|
+
create extension if not exists pgtap with schema extensions;
|
|
5
|
+
set search_path = extensions, public, agent_board;
|
|
6
|
+
begin;
|
|
7
|
+
select plan(76);
|
|
8
|
+
grant usage on schema extensions to authenticated, service_role;
|
|
9
|
+
-- On unexpected success, force the probe's subtransaction to roll back. The
|
|
10
|
+
-- wrong SQLSTATE still fails throws_ok, while later fixtures and plan counts survive.
|
|
11
|
+
create function pg_temp.refusal_probe(p_sql text) returns void language plpgsql as $$
|
|
12
|
+
begin
|
|
13
|
+
execute p_sql;
|
|
14
|
+
raise exception 'refusal probe unexpectedly succeeded' using errcode='PZ001';
|
|
15
|
+
end $$;
|
|
16
|
+
create function pg_temp.a(n int) returns uuid language sql immutable as $$
|
|
17
|
+
select ('67410000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid $$;
|
|
18
|
+
create function pg_temp.b(n int) returns uuid language sql immutable as $$
|
|
19
|
+
select ('67520000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid $$;
|
|
20
|
+
create function pg_temp.actor(n int) returns void language sql as $$
|
|
21
|
+
select set_config('request.jwt.claims',jsonb_build_object('role','authenticated',
|
|
22
|
+
'sub','67400000-0000-0000-0000-' || lpad(n::text,12,'0'))::text,true)::text::void $$;
|
|
23
|
+
insert into auth.users(id,instance_id,aud,role,email)
|
|
24
|
+
select ('67400000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid,
|
|
25
|
+
'00000000-0000-0000-0000-000000000000','authenticated','authenticated','roleguard-'||n||'@test.invalid'
|
|
26
|
+
from generate_series(1,6) n;
|
|
27
|
+
insert into agent_board.agents(id,agent_type,kind,handle,display_name,status,auth_user_id)
|
|
28
|
+
select pg_temp.a(n),'agent','agent','roleguard-'||n,'Roleguard '||n,'active',
|
|
29
|
+
('67400000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid from generate_series(1,6) n;
|
|
30
|
+
insert into agent_board.boards(id,slug,name,created_by_agent_id,visibility)
|
|
31
|
+
select pg_temp.b(n),'roleguard-'||n,'Roleguard '||n,pg_temp.a(1),'private' from generate_series(1,5) n;
|
|
32
|
+
insert into agent_board.board_memberships(board_id,agent_id,role,membership_state,created_by_agent_id)
|
|
33
|
+
select pg_temp.b(b),pg_temp.a(a),
|
|
34
|
+
case when a in (1,2) then 'owner' when a in (3,4) then 'admin' when a=5 then 'member' else 'observer' end,
|
|
35
|
+
case when b=4 and a=1 then 'subscribed' else 'joined' end,pg_temp.a(1)
|
|
36
|
+
from generate_series(1,5) b cross join generate_series(1,6) a
|
|
37
|
+
where (b in (1,5) or a<>2) and (b<>3 or a<>1);
|
|
38
|
+
-- Pending and terminal owners do not count as active owners.
|
|
39
|
+
insert into agent_board.board_memberships(board_id,agent_id,role,membership_state,created_by_agent_id)
|
|
40
|
+
values (pg_temp.b(2),pg_temp.a(2),'owner','invited',pg_temp.a(1));
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
select pg_temp.actor(1);
|
|
44
|
+
set local role authenticated;
|
|
45
|
+
|
|
46
|
+
select lives_ok($$select * from agent_board.grant_board_role(pg_temp.b(1),pg_temp.a(1),'admin')$$, 'LO-self-demote two owners allowed');
|
|
47
|
+
|
|
48
|
+
reset role;
|
|
49
|
+
select set_config('request.jwt.claims', '', true);
|
|
50
|
+
|
|
51
|
+
select is((select role from agent_board.board_memberships where board_id=pg_temp.b(1) and agent_id=pg_temp.a(1) and membership_state in ('joined','subscribed')), 'admin', 'LO-self-demote state');
|
|
52
|
+
|
|
53
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(1) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-self-demote keeps one owner');
|
|
54
|
+
|
|
55
|
+
update agent_board.board_memberships set role='owner' where board_id=pg_temp.b(1) and agent_id=pg_temp.a(1);
|
|
56
|
+
|
|
57
|
+
select pg_temp.actor(1);
|
|
58
|
+
set local role authenticated;
|
|
59
|
+
|
|
60
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$select * from agent_board.grant_board_role(pg_temp.b(2),pg_temp.a(1),'member')$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-last self-demote refused');
|
|
61
|
+
|
|
62
|
+
reset role;
|
|
63
|
+
select set_config('request.jwt.claims', '', true);
|
|
64
|
+
|
|
65
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(2) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-last self-demote unchanged');
|
|
66
|
+
|
|
67
|
+
select pg_temp.actor(1);
|
|
68
|
+
set local role authenticated;
|
|
69
|
+
|
|
70
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$select * from agent_board.leave_board(pg_temp.b(2),pg_temp.a(1))$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-last leave refused');
|
|
71
|
+
|
|
72
|
+
reset role;
|
|
73
|
+
select set_config('request.jwt.claims', '', true);
|
|
74
|
+
|
|
75
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)), 'joined', 'LO-last leave unchanged');
|
|
76
|
+
|
|
77
|
+
select pg_temp.actor(1);
|
|
78
|
+
set local role authenticated;
|
|
79
|
+
|
|
80
|
+
select lives_ok($$select * from agent_board.leave_board(pg_temp.b(1),pg_temp.a(1))$$, 'LO-two owners leave allowed');
|
|
81
|
+
|
|
82
|
+
reset role;
|
|
83
|
+
select set_config('request.jwt.claims', '', true);
|
|
84
|
+
|
|
85
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.b(1) and agent_id=pg_temp.a(1)), 'left', 'LO-two owners leave state');
|
|
86
|
+
|
|
87
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(1) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-two owners leave keeps one');
|
|
88
|
+
|
|
89
|
+
set local role service_role;
|
|
90
|
+
select set_config('request.jwt.claims','{"role":"service_role"}',true);
|
|
91
|
+
|
|
92
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$select * from agent_board.update_board_membership(pg_temp.b(2),pg_temp.a(1),'removed')$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-last removal service refused');
|
|
93
|
+
|
|
94
|
+
reset role;
|
|
95
|
+
select set_config('request.jwt.claims', '', true);
|
|
96
|
+
|
|
97
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)), 'joined', 'LO-last removal unchanged');
|
|
98
|
+
|
|
99
|
+
update agent_board.board_memberships set role='owner',membership_state='joined',left_at=null where board_id=pg_temp.b(1) and agent_id=pg_temp.a(1);
|
|
100
|
+
|
|
101
|
+
set local role service_role;
|
|
102
|
+
select set_config('request.jwt.claims','{"role":"service_role"}',true);
|
|
103
|
+
|
|
104
|
+
select lives_ok($$select * from agent_board.update_board_membership(pg_temp.b(1),pg_temp.a(1),'removed')$$, 'LO-two owners removal allowed');
|
|
105
|
+
|
|
106
|
+
reset role;
|
|
107
|
+
select set_config('request.jwt.claims', '', true);
|
|
108
|
+
|
|
109
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.b(1) and agent_id=pg_temp.a(1)), 'removed', 'LO-two owners removal state');
|
|
110
|
+
|
|
111
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(1) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-two owners removal keeps one');
|
|
112
|
+
|
|
113
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$delete from agent_board.board_memberships where board_id=pg_temp.b(5) and agent_id=pg_temp.a(1)$probe$)$$,'55000','last_active_board_owner: acquire board authority lock before owner transition','LO-raw owner transition requires prior board lock');
|
|
114
|
+
|
|
115
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(5) and role='owner' and membership_state in ('joined','subscribed')), 2, 'LO-unlocked raw transition unchanged');
|
|
116
|
+
|
|
117
|
+
select agent_board._lock_board_authority_ceiling(pg_temp.b(5));
|
|
118
|
+
|
|
119
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$delete from agent_board.board_memberships where board_id=pg_temp.b(5) and role='owner'$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-bulk owners delete refused');
|
|
120
|
+
|
|
121
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(5) and role='owner' and membership_state in ('joined','subscribed')), 2, 'LO-bulk owners delete atomic rollback');
|
|
122
|
+
|
|
123
|
+
select lives_ok($$delete from agent_board.board_memberships where board_id=pg_temp.b(5) and agent_id=pg_temp.a(1)$$, 'LO-two owners raw delete allowed');
|
|
124
|
+
|
|
125
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(5) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-two owners raw delete state');
|
|
126
|
+
|
|
127
|
+
select agent_board._lock_board_authority_ceiling(pg_temp.b(2));
|
|
128
|
+
|
|
129
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$delete from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-raw-delete refused');
|
|
130
|
+
|
|
131
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(2) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-raw-delete unchanged');
|
|
132
|
+
|
|
133
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$update agent_board.board_memberships set role='member' where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-raw-role refused');
|
|
134
|
+
|
|
135
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(2) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-raw-role unchanged');
|
|
136
|
+
|
|
137
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$update agent_board.board_memberships set board_id=pg_temp.b(3) where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-raw-move refused');
|
|
138
|
+
|
|
139
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(2) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-raw-move unchanged');
|
|
140
|
+
|
|
141
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$select * from agent_board.update_board_membership(pg_temp.b(4),pg_temp.a(1),'left')$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-subscribed-leave refused');
|
|
142
|
+
|
|
143
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(4) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-subscribed-leave unchanged');
|
|
144
|
+
|
|
145
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$delete from agent_board.agents where id=pg_temp.a(2)$probe$)$$,'23514','last_active_board_owner: cannot remove the last active owner','LO-cascade-agent refused');
|
|
146
|
+
|
|
147
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(1) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-cascade-agent unchanged');
|
|
148
|
+
|
|
149
|
+
select lives_ok($$delete from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id=pg_temp.a(2)$$, 'LO-pending owner delete allowed');
|
|
150
|
+
|
|
151
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id=pg_temp.a(2)),0,'LO-pending owner delete state');
|
|
152
|
+
|
|
153
|
+
select lives_ok($$update agent_board.board_memberships set metadata='{"unrelated":true}' where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)$$, 'LO-owner metadata allowed');
|
|
154
|
+
|
|
155
|
+
select is((select metadata->>'unrelated' from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)),'true','LO-owner metadata state');
|
|
156
|
+
|
|
157
|
+
select lives_ok($$update agent_board.board_memberships set metadata='{"unrelated":true}' where board_id=pg_temp.b(3)$$, 'LO-ownerless unrelated update allowed');
|
|
158
|
+
|
|
159
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(3) and metadata->>'unrelated'='true'),4,'LO-ownerless unrelated state');
|
|
160
|
+
|
|
161
|
+
select pg_temp.actor(5);
|
|
162
|
+
set local role authenticated;
|
|
163
|
+
|
|
164
|
+
select lives_ok($$select * from agent_board.update_board_membership(pg_temp.b(3),pg_temp.a(5),'left')$$, 'LO-ownerless member leave allowed');
|
|
165
|
+
|
|
166
|
+
reset role;
|
|
167
|
+
select set_config('request.jwt.claims', '', true);
|
|
168
|
+
|
|
169
|
+
select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.b(3) and agent_id=pg_temp.a(5)), 'left', 'LO-ownerless member leave state');
|
|
170
|
+
|
|
171
|
+
select pg_temp.actor(3);
|
|
172
|
+
set local role authenticated;
|
|
173
|
+
|
|
174
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$select * from agent_board.grant_board_role(pg_temp.b(3),pg_temp.a(3),'owner')$probe$)$$, '42501', 'grant exceeds grantor role or board authority ceiling', 'LO-auth recovery refused');
|
|
175
|
+
|
|
176
|
+
select ok(not has_function_privilege('authenticated','agent_board.bootstrap_board_owner_memberships()','EXECUTE'),'LO-bootstrap recovery service-only');
|
|
177
|
+
|
|
178
|
+
reset role;
|
|
179
|
+
select set_config('request.jwt.claims', '', true);
|
|
180
|
+
|
|
181
|
+
set local role service_role;
|
|
182
|
+
select set_config('request.jwt.claims','{"role":"service_role"}',true);
|
|
183
|
+
|
|
184
|
+
select lives_ok($$select * from agent_board.bootstrap_board_owner_memberships()$$, 'LO-service recovery allowed');
|
|
185
|
+
|
|
186
|
+
reset role;
|
|
187
|
+
select set_config('request.jwt.claims', '', true);
|
|
188
|
+
|
|
189
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(3) and role='owner' and membership_state in ('joined','subscribed')), 1, 'LO-service recovery state');
|
|
190
|
+
|
|
191
|
+
select lives_ok($$delete from agent_board.boards where id=pg_temp.b(4)$$, 'LO-board deletion allowed');
|
|
192
|
+
|
|
193
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(4)),0,'LO-board deletion cascades');
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
-- Refusal-only behaviour: the trigger must return the proposed row verbatim.
|
|
197
|
+
select is((select jsonb_build_object('role',role,'state',membership_state,'agent',agent_id,'board',board_id,'metadata',metadata)
|
|
198
|
+
from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)),
|
|
199
|
+
jsonb_build_object('role','owner','state','joined','agent',pg_temp.a(1),'board',pg_temp.b(2),'metadata','{"unrelated":true}'::jsonb),
|
|
200
|
+
'LO-refusal-only preserves proposed owner row');
|
|
201
|
+
|
|
202
|
+
insert into agent_board.agents(id,agent_type,handle,display_name,status)
|
|
203
|
+
values (pg_temp.a(7),'agent','last-owner-replacement','Replacement','active');
|
|
204
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$update agent_board.board_memberships set agent_id=pg_temp.a(2) where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)$probe$)$$,
|
|
205
|
+
'23514','last_active_board_owner: cannot remove the last active owner','LO-last owner reassignment refused');
|
|
206
|
+
select is((select agent_id from agent_board.board_memberships where board_id=pg_temp.b(2) and role='owner' and membership_state='joined'),pg_temp.a(1),'LO-last owner reassignment unchanged');
|
|
207
|
+
insert into agent_board.board_memberships(board_id,agent_id,role,membership_state,created_by_agent_id)
|
|
208
|
+
values (pg_temp.b(2),pg_temp.a(7),'owner','joined',pg_temp.a(1));
|
|
209
|
+
select lives_ok($$update agent_board.board_memberships set agent_id=pg_temp.a(2) where board_id=pg_temp.b(2) and agent_id=pg_temp.a(1)$$,'LO-two owners reassignment allowed');
|
|
210
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=pg_temp.b(2) and agent_id in (pg_temp.a(2),pg_temp.a(7)) and role='owner' and membership_state='joined'),2,'LO-two owners reassignment state');
|
|
211
|
+
|
|
212
|
+
-- Real SID holder: the parent is narrowed, and alias linking would demote the
|
|
213
|
+
-- child's sole owner membership. Refusal must roll back alias and authority.
|
|
214
|
+
create temp table _lo_sid(root_id uuid,owner_id uuid,board_id uuid,child_id uuid,key_id uuid,child_auth uuid,epoch bigint);
|
|
215
|
+
create function pg_temp.lo_sid_setup() returns void language plpgsql as $fixture$
|
|
216
|
+
declare v_root uuid:=gen_random_uuid(); v_board uuid:=gen_random_uuid();
|
|
217
|
+
v_owner_auth uuid; v_root_auth uuid; v_owner uuid; v_child record; v_challenge record; v_epoch bigint;
|
|
218
|
+
begin
|
|
219
|
+
v_owner_auth:=public.ensure_github_user('lo-owner-'||v_root,'lo-owner-'||v_root||'@test.invalid','lo-owner-'||v_root,'Last owner human');
|
|
220
|
+
v_root_auth:=public.ensure_github_user('lo-root-'||v_root,'lo-root-'||v_root||'@test.invalid','lo-root-'||v_root,'Last owner root');
|
|
221
|
+
insert into agent_board.agents(agent_type,handle,display_name,status,auth_user_id)
|
|
222
|
+
values ('human','lo-human-'||v_root,'Last owner human','active',v_owner_auth) returning id into v_owner;
|
|
223
|
+
insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id,owner_user_id)
|
|
224
|
+
values (v_root,'agent','lo-root-'||v_root,'Last owner root','active',v_root_auth,v_owner_auth);
|
|
225
|
+
insert into agent_board.session_identities(agent_id,root_agent_id,depth,approved_risky_digest,current_risky_digest)
|
|
226
|
+
values (v_root,v_root,0,'approved','approved');
|
|
227
|
+
insert into agent_board.boards(id,slug,name,created_by_agent_id) values (v_board,'lo-link-'||v_board,'Last owner link',v_root);
|
|
228
|
+
insert into agent_board.board_memberships(board_id,agent_id,role,membership_state) values (v_board,v_root,'owner','joined'),(v_board,v_owner,'owner','joined');
|
|
229
|
+
perform agent_board.upsert_delegation_policy(v_root,'root_minted','["read","write"]',200,interval '7 days',true);
|
|
230
|
+
select * into v_child from agent_board.mint_delegated_child(v_root,'lo-child-'||v_root,'["read","write"]',public.test_pubkey('lo-child-'||v_root),interval '8 hours',false,'claude','lo-child',null,
|
|
231
|
+
jsonb_build_array(jsonb_build_object('board_id',v_board,'role','owner')),'{"resolution":"resolved","risky_digest":"approved"}');
|
|
232
|
+
select * into v_challenge from agent_board.issue_signing_key_challenge(v_child.child_agent_id,'delegated-ephemeral');
|
|
233
|
+
select identity_lease_epoch into v_epoch from agent_board.mint_fleet_agent_session(v_challenge.challenge_id,split_part(v_challenge.challenge,':',3),v_child.signing_key_id,v_child.child_agent_id,900,null,'lo-child');
|
|
234
|
+
perform agent_board._lock_board_authority_ceiling(v_board);
|
|
235
|
+
update agent_board.board_memberships set role='admin' where board_id=v_board and agent_id in (v_root,v_owner);
|
|
236
|
+
insert into _lo_sid select v_root,v_owner,v_board,v_child.child_agent_id,v_child.signing_key_id,a.auth_user_id,v_epoch from agent_board.agents a where a.id=v_child.child_agent_id;
|
|
237
|
+
end $fixture$;
|
|
238
|
+
select set_config('request.jwt.claims','{"role":"service_role"}',true);
|
|
239
|
+
select set_config('request.jwt.claim.role','service_role',true);
|
|
240
|
+
select lives_ok($$select pg_temp.lo_sid_setup()$$,'LO-link fixture allowed');
|
|
241
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=(select board_id from _lo_sid) and role='owner' and membership_state in ('joined','subscribed')),1,'LO-link fixture has sole child owner');
|
|
242
|
+
grant select on _lo_sid to service_role;
|
|
243
|
+
select set_config('request.jwt.claim.sub',(select child_auth::text from _lo_sid),true);
|
|
244
|
+
select set_config('request.jwt.claim.identity_lease_epoch',(select epoch::text from _lo_sid),true);
|
|
245
|
+
set local role service_role;
|
|
246
|
+
select throws_ok($$select pg_temp.refusal_probe($probe$select agent_board.link_session_alias((select child_id from _lo_sid),(select epoch::text from _lo_sid),'lo-last-owner-refused')$probe$)$$,
|
|
247
|
+
'23514','last_active_board_owner: cannot remove the last active owner','LO-link sole-owner narrowing refused');
|
|
248
|
+
reset role;
|
|
249
|
+
select is((select role from agent_board.board_memberships where board_id=(select board_id from _lo_sid) and agent_id=(select child_id from _lo_sid)),'owner','LO-link refused membership unchanged');
|
|
250
|
+
select is((select count(*)::int from agent_board.session_id_aliases where root_agent_id=(select root_id from _lo_sid) and session_id='lo-last-owner-refused'),0,'LO-link refusal rolls back alias');
|
|
251
|
+
update agent_board.board_memberships set role='owner' where board_id=(select board_id from _lo_sid) and agent_id=(select owner_id from _lo_sid);
|
|
252
|
+
set local role service_role;
|
|
253
|
+
select lives_ok($$select agent_board.link_session_alias((select child_id from _lo_sid),(select epoch::text from _lo_sid),'lo-two-owners-allowed')$$,'LO-link narrowing with coowner allowed');
|
|
254
|
+
reset role;
|
|
255
|
+
select is((select role from agent_board.board_memberships where board_id=(select board_id from _lo_sid) and agent_id=(select child_id from _lo_sid)),'admin','LO-link allowed narrows child role');
|
|
256
|
+
select is((select count(*)::int from agent_board.board_memberships where board_id=(select board_id from _lo_sid) and role='owner' and membership_state in ('joined','subscribed')),1,'LO-link allowed retains coowner');
|
|
257
|
+
select set_config('request.jwt.claims','',true);
|
|
258
|
+
select set_config('request.jwt.claim.role','',true);
|
|
259
|
+
select set_config('request.jwt.claim.sub','',true);
|
|
260
|
+
select set_config('request.jwt.claim.identity_lease_epoch','',true);
|
|
261
|
+
|
|
262
|
+
select alike(obj_description('agent_board._lock_board_authority_ceiling(uuid)'::regprocedure,'pg_proc'),'%per-board departure mutex%','CLASS-board lock COMMENT documents departure mutex');
|
|
263
|
+
|
|
264
|
+
create temp view _roleguard_writers as
|
|
265
|
+
select p.oid,case when n.nspname='agent_board' then p.proname::text
|
|
266
|
+
else n.nspname||'.'||p.proname end as name,
|
|
267
|
+
agent_board_test.stripped_body(p.oid,true) as b,
|
|
268
|
+
agent_board_test.stripped_body(p.oid,true) ~ agent_board_test.col_write_re('board_memberships','role') as role_writer,
|
|
269
|
+
agent_board_test.stripped_body(p.oid,true) ~ agent_board_test.col_write_re('board_memberships','membership_state') as state_writer
|
|
270
|
+
from pg_proc p join pg_namespace n on n.oid=p.pronamespace
|
|
271
|
+
where n.nspname not in ('pg_catalog','information_schema','agent_board_test')
|
|
272
|
+
and n.nspname !~ '^pg_' and p.prokind in ('f','p')
|
|
273
|
+
and p.prolang <> (select oid from pg_language where lanname='internal')
|
|
274
|
+
and agent_board_test.stripped_body(p.oid,true) ~ agent_board_test.write_re('board_memberships');
|
|
275
|
+
|
|
276
|
+
select is((select string_agg(name,',' order by name) from _roleguard_writers where role_writer),'_fedsession_autojoin_default_board,_redeem_unbounded_invite_for_agent,_upsert_board_membership_state,bootstrap_board_owner_memberships,create_board,grant_board_role,link_session_alias,mint_delegated_child,readmit_session_identity','CLASS-role writers: a new role writer fails and names itself');
|
|
277
|
+
|
|
278
|
+
select is((select string_agg(name,',' order by name) from _roleguard_writers where state_writer),'_fedsession_autojoin_default_board,_redeem_unbounded_invite_for_agent,_upsert_board_membership_state,bootstrap_board_owner_memberships,create_board','CLASS-state writers: a new state writer fails and names itself');
|
|
279
|
+
|
|
280
|
+
select is((select string_agg(name,',' order by name) from _roleguard_writers),'_fedsession_autojoin_default_board,_redeem_unbounded_invite_for_agent,_upsert_board_membership_state,bootstrap_board_owner_memberships,create_board,grant_board_role,link_session_alias,mint_delegated_child,readmit_session_identity,set_board_member_label','CLASS-all table writers: new deletes and other writers name themselves');
|
|
281
|
+
|
|
282
|
+
-- TRUNCATE has no row trigger; application roles must never acquire that door.
|
|
283
|
+
select is((select coalesce(string_agg(r,',' order by r),'')
|
|
284
|
+
from unnest(array['anon','authenticated','service_role','agent_board_delegate']) r
|
|
285
|
+
where has_table_privilege(r,'agent_board.board_memberships','TRUNCATE')),
|
|
286
|
+
'', 'CLASS-no application TRUNCATE bypass of row invariant');
|
|
287
|
+
|
|
288
|
+
select ok((select count(*)=1 and bool_and(tgtype=27 and tgenabled='O' and tgqual is null and tgfoid=to_regprocedure('agent_board._board_memberships_last_owner_guard()')) from pg_trigger where tgrelid='agent_board.board_memberships'::regclass and tgname='trg_board_memberships_last_owner_guard'),'CLASS-table invariant: unconditional enabled BEFORE ROW UPDATE/DELETE trigger covers every writer');
|
|
289
|
+
|
|
290
|
+
select is((select count(*)::int from pg_proc p,aclexplode(coalesce(p.proacl,acldefault('f',p.proowner))) a where p.oid=to_regprocedure('agent_board._board_memberships_last_owner_guard()') and a.privilege_type='EXECUTE' and a.grantee<>p.proowner),0,'CLASS-trigger ACL: no API role or PUBLIC can execute internal trigger');
|
|
291
|
+
|
|
292
|
+
select isnt(obj_description(to_regprocedure('agent_board._board_memberships_last_owner_guard()'),'pg_proc'),null,'CLASS-trigger COMMENT exists');
|
|
293
|
+
|
|
294
|
+
select is((select md5(prosrc) from pg_proc where oid=to_regprocedure('agent_board.grant_board_role(uuid,uuid,text,uuid)')),'de7d1079623e95dcc376e1881492a44b','CLASS-main grant body unchanged'); -- pgtap-allow-raise-checked: non-raising catalog identity lookup; no RPC is executed.
|
|
295
|
+
|
|
296
|
+
select ok(strpos(agent_board_test.stripped_body('agent_board.update_board_membership(uuid,uuid,text,jsonb)'::regprocedure),'agent_board._lock_board_authority_ceiling(')>0 and strpos(agent_board_test.stripped_body('agent_board.update_board_membership(uuid,uuid,text,jsonb)'::regprocedure),'agent_board._lock_board_authority_ceiling(')<strpos(agent_board_test.stripped_body('agent_board.update_board_membership(uuid,uuid,text,jsonb)'::regprocedure),'for update'),'CLASS-departure advisory before membership row lock');
|
|
297
|
+
|
|
298
|
+
select ok((select b like '%select p_provider.default_board_id, p_agent_id, ''member'', ''joined''%' from _roleguard_writers where name='_fedsession_autojoin_default_board'),'CLASS-positive _fedsession_autojoin_default_board writer behavior');
|
|
299
|
+
|
|
300
|
+
select ok((select b like '%values (v_row.board_id, v_row.owner_agent_id, ''joined'', ''owner'', v_row.owner_agent_id)%' from _roleguard_writers where name='bootstrap_board_owner_memberships'),'CLASS-positive bootstrap_board_owner_memberships writer behavior');
|
|
301
|
+
|
|
302
|
+
select ok((select b like '%values (v_board.id, v_board.created_by_agent_id, ''joined'', ''owner'', v_board.created_by_agent_id)%' from _roleguard_writers where name='create_board'),'CLASS-positive create_board writer behavior');
|
|
303
|
+
|
|
304
|
+
select ok((select b like '%when agent_board._role_rank(excluded.role) > agent_board._role_rank(agent_board.board_memberships.role)%' from _roleguard_writers where name='_redeem_unbounded_invite_for_agent'),'CLASS-positive _redeem_unbounded_invite_for_agent writer behavior');
|
|
305
|
+
|
|
306
|
+
select ok((select b like '%v_membership.membership_state not in (''joined'', ''subscribed'')
|
|
307
|
+
or v_currently_removed
|
|
308
|
+
)
|
|
309
|
+
then ''member''
|
|
310
|
+
else bm.role%' from _roleguard_writers where name='_upsert_board_membership_state'),'CLASS-positive _upsert_board_membership_state writer behavior');
|
|
311
|
+
|
|
312
|
+
select ok((select b like '%update agent_board.board_memberships set role = v_role where id = v_membership.id%' from _roleguard_writers where name='mint_delegated_child'),'CLASS-positive mint_delegated_child writer behavior');
|
|
313
|
+
|
|
314
|
+
select ok((select b like '%update agent_board.board_memberships set role = v_role where id = v_membership.id%' from _roleguard_writers where name='readmit_session_identity'),'CLASS-positive readmit_session_identity writer behavior');
|
|
315
|
+
|
|
316
|
+
select ok((select b like '%update agent_board.board_memberships set role = v_role where id = v_membership.id%' from _roleguard_writers where name='link_session_alias'),'CLASS-positive link_session_alias writer behavior');
|
|
317
|
+
|
|
318
|
+
select finish();
|
|
319
|
+
rollback;
|
|
@@ -76,6 +76,10 @@ begin
|
|
|
76
76
|
select board_id into v_open
|
|
77
77
|
from agent_board.create_board(v_owner, 'rr-open-' || v_suffix, 'RR Open', 'rolereset open', '{}'::jsonb, 'open');
|
|
78
78
|
|
|
79
|
+
-- A second owner preserves the role-reset test without violating last-owner integrity.
|
|
80
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
|
|
81
|
+
values (v_open, v_admin, 'joined', 'owner', v_owner);
|
|
82
|
+
|
|
79
83
|
-- Board PRIVATE: hosts the per-origin clamp matrix, driven service_role so the
|
|
80
84
|
-- transition itself is never the thing under test.
|
|
81
85
|
select board_id into v_priv
|
|
@@ -284,17 +288,16 @@ select ok(
|
|
|
284
288
|
limit 1),
|
|
285
289
|
'grant_board_role carries the active-state predicate exactly THREE times (#674: step-6 target, step-6 grantor join, UPDATE -- the TOCTOU half)');
|
|
286
290
|
|
|
287
|
-
-- 16.
|
|
288
|
-
--
|
|
289
|
-
-- erase create_board's owner seed and ROOMLINK's admin preset.
|
|
291
|
+
-- 16. The only user trigger is the UPDATE/DELETE refusal guard. It cannot fire
|
|
292
|
+
-- on INSERT seeders, where a role-reset trigger would erase owner/admin presets.
|
|
290
293
|
select is(
|
|
291
|
-
(select
|
|
294
|
+
(select string_agg(t.tgname || ':' || t.tgtype::text, ',' order by t.tgname) from pg_trigger t
|
|
292
295
|
join pg_class c on c.oid = t.tgrelid
|
|
293
296
|
join pg_namespace n on n.oid = c.relnamespace
|
|
294
297
|
where n.nspname = 'agent_board' and c.relname = 'board_memberships'
|
|
295
298
|
and not t.tgisinternal),
|
|
296
|
-
|
|
297
|
-
'
|
|
299
|
+
'trg_board_memberships_last_owner_guard:27',
|
|
300
|
+
'only the last-owner UPDATE/DELETE trigger exists; the role clamp remains mutator-sited');
|
|
298
301
|
|
|
299
302
|
-- 17. STRUCTURAL: the mutator's superseding comment no longer claims it never writes role.
|
|
300
303
|
select ok(
|
|
@@ -331,35 +334,22 @@ select is(
|
|
|
331
334
|
'member',
|
|
332
335
|
'owner self-leave -> self-rejoin on an OPEN board returns at ''member'', not ''owner'' (#79 repro B closed)');
|
|
333
336
|
|
|
334
|
-
-- 21.
|
|
335
|
-
-- checked UNCONDITIONALLY -- no service_role bypass -- so an operator must nominate a
|
|
336
|
-
-- grantor that already holds rank, and in a sole-owner lockout none exists.
|
|
337
|
-
-- NOTE (CR correction): this does NOT mean "no RPC recovery" exists, as an earlier draft
|
|
338
|
-
-- of this suite and the CHANGELOG both claimed. bootstrap_board_owner_memberships IS a
|
|
339
|
-
-- service_role RPC and raw-inserts joined/owner for a creator with no ACTIVE membership,
|
|
340
|
-
-- so leave-again + bootstrap is a genuine RPC recovery path. Assertion 22 proves it.
|
|
337
|
+
-- 21. A sole owner cannot now create the former lockout state.
|
|
341
338
|
select set_config('request.jwt.claim.role', '', true);
|
|
342
339
|
select set_config('request.jwt.claim.sub', '', true);
|
|
343
|
-
select agent_board.leave_board(lockout_board, lockout_owner) from _rr_fixture;
|
|
344
|
-
select agent_board.join_board(lockout_board, lockout_owner, 'joined') from _rr_fixture;
|
|
345
340
|
select throws_ok(
|
|
346
|
-
(select format($q$select agent_board.
|
|
347
|
-
'
|
|
348
|
-
'
|
|
349
|
-
'sole-owner
|
|
350
|
-
|
|
351
|
-
-- 22. The
|
|
352
|
-
-- draft did `update ... set role='owner'` and then read it back, which is tautological -- it
|
|
353
|
-
-- proved SQL works, not that a recovery path exists. Leave again so the creator has no ACTIVE
|
|
354
|
-
-- membership, then call bootstrap_board_owner_memberships(); it raw-inserts joined/owner.
|
|
355
|
-
select agent_board.leave_board(lockout_board, lockout_owner) from _rr_fixture;
|
|
356
|
-
select agent_board.bootstrap_board_owner_memberships();
|
|
341
|
+
(select format($q$select agent_board.leave_board(%L::uuid, %L::uuid)$q$, lockout_board, lockout_owner) from _rr_fixture),
|
|
342
|
+
'23514',
|
|
343
|
+
'last_active_board_owner: cannot remove the last active owner',
|
|
344
|
+
'ROLEGUARD sole-owner departure is refused even to service_role');
|
|
345
|
+
|
|
346
|
+
-- 22. The refused departure preserves the owner; legacy recovery is tested in role_guard.
|
|
357
347
|
select is(
|
|
358
348
|
(select role from agent_board.board_memberships bm
|
|
359
349
|
join _rr_fixture f on bm.board_id = f.lockout_board and bm.agent_id = f.lockout_owner
|
|
360
350
|
where bm.membership_state in ('joined','subscribed')),
|
|
361
351
|
'owner',
|
|
362
|
-
'
|
|
352
|
+
'ROLEGUARD refused departure preserves the active owner');
|
|
363
353
|
|
|
364
354
|
select set_config('request.jwt.claim.role', '', true);
|
|
365
355
|
select set_config('request.jwt.claim.sub', '', true);
|
|
@@ -36,6 +36,7 @@ declare
|
|
|
36
36
|
v_requester uuid := gen_random_uuid();
|
|
37
37
|
v_nomember uuid := gen_random_uuid();
|
|
38
38
|
v_suspended uuid := gen_random_uuid();
|
|
39
|
+
v_coowner uuid := gen_random_uuid();
|
|
39
40
|
v_board uuid;
|
|
40
41
|
v_slug text := 'roomdeleg-board-' || v_suffix;
|
|
41
42
|
begin
|
|
@@ -54,6 +55,9 @@ begin
|
|
|
54
55
|
(v_requester, 'agent', 'rd-requester-' || v_suffix, 'RD Requester', 'active', null, null),
|
|
55
56
|
(v_nomember, 'agent', 'rd-nomember-' || v_suffix, 'RD NoMember', 'active', null, null);
|
|
56
57
|
|
|
58
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
59
|
+
values (v_coowner, 'agent', 'rd-coowner-' || v_suffix, 'RD Coowner', 'active');
|
|
60
|
+
|
|
57
61
|
-- Alice creates board (service_role null-actor here -> supplied arg honored):
|
|
58
62
|
-- create_board seeds Alice's joined owner membership (ROOMOWN).
|
|
59
63
|
select board_id into v_board
|
|
@@ -66,6 +70,9 @@ begin
|
|
|
66
70
|
(v_board, v_bob, 'joined', v_alice, 'member'),
|
|
67
71
|
(v_board, v_requester, 'requested', v_alice, 'member');
|
|
68
72
|
|
|
73
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
74
|
+
values (v_board, v_coowner, 'joined', v_alice, 'owner');
|
|
75
|
+
|
|
69
76
|
insert into _roomdeleg_fixture values
|
|
70
77
|
(v_board, v_alice, v_bob, v_target, v_requester, v_nomember, v_suspended,
|
|
71
78
|
v_alice_user, v_bob_user, v_suspended_user);
|
|
@@ -127,6 +134,7 @@ select lives_ok(
|
|
|
127
134
|
|
|
128
135
|
-- Demote the creator to member (bare statement, not an assertion). created_by is
|
|
129
136
|
-- immutable, so this models a creator who has lost owner authority.
|
|
137
|
+
select agent_board._lock_board_authority_ceiling(board) from _roomdeleg_fixture;
|
|
130
138
|
update agent_board.board_memberships bm
|
|
131
139
|
set role = 'member'
|
|
132
140
|
from _roomdeleg_fixture f
|
|
@@ -73,7 +73,9 @@ begin
|
|
|
73
73
|
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
74
74
|
values
|
|
75
75
|
(v_board, v_member, 'joined', v_alice, 'member'),
|
|
76
|
-
(v_board, v_grantee, 'joined', v_alice, 'observer')
|
|
76
|
+
(v_board, v_grantee, 'joined', v_alice, 'observer'),
|
|
77
|
+
-- Keep another owner for the creator self-demotion / slug-collision control.
|
|
78
|
+
(v_board, v_b2owner, 'joined', v_alice, 'owner');
|
|
77
79
|
|
|
78
80
|
-- Board B2: an owner-LESS board (direct insert, no membership) for the bootstrap sweep.
|
|
79
81
|
insert into agent_board.boards(id, slug, name, description, created_by_agent_id, visibility)
|
|
@@ -323,6 +323,9 @@ begin
|
|
|
323
323
|
v_left_creator, 'rowid-bootstrap-left-' || s, 'ROWID Bootstrap Left',
|
|
324
324
|
'leave-again recovery', '{}'::jsonb, 'open'
|
|
325
325
|
);
|
|
326
|
+
-- ROLEGUARD: this creator may depart while another active owner remains.
|
|
327
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role)
|
|
328
|
+
values (v_left_board, v_writer_owner, 'joined', 'owner');
|
|
326
329
|
|
|
327
330
|
-- Writer 1: a genuine create_board insert.
|
|
328
331
|
select board_id into v_writer_board
|
|
@@ -660,7 +663,7 @@ select is(
|
|
|
660
663
|
on bm.board_id = f.left_board and bm.agent_id = f.left_creator
|
|
661
664
|
where bm.membership_state in ('joined', 'subscribed')),
|
|
662
665
|
'member',
|
|
663
|
-
'left recovery: ROLERESET rejoin clamps the
|
|
666
|
+
'left recovery: ROLERESET rejoin clamps the departing creator to member');
|
|
664
667
|
select lives_ok(
|
|
665
668
|
(select format(
|
|
666
669
|
$q$select agent_board.update_board_membership(%L::uuid, %L::uuid, 'left')$q$,
|
|
@@ -109,6 +109,15 @@
|
|
|
109
109
|
"authenticated": true
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
"name": "_board_memberships_last_owner_guard",
|
|
114
|
+
"signature": "_board_memberships_last_owner_guard()",
|
|
115
|
+
"executable_by": {
|
|
116
|
+
"anon": false,
|
|
117
|
+
"service_role": false,
|
|
118
|
+
"authenticated": false
|
|
119
|
+
}
|
|
120
|
+
},
|
|
112
121
|
{
|
|
113
122
|
"name": "_build_fetch_descriptor",
|
|
114
123
|
"signature": "_build_fetch_descriptor(p_surface text, p_target_id uuid, p_view text, p_reason text, p_filters jsonb)",
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
"signature": "_board_membership_events_provenance_guard()",
|
|
50
50
|
"declares_p_metadata": false
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
"signature": "_board_memberships_last_owner_guard()",
|
|
54
|
+
"declares_p_metadata": false
|
|
55
|
+
},
|
|
52
56
|
{
|
|
53
57
|
"signature": "_build_fetch_descriptor(p_surface text, p_target_id uuid, p_view text, p_reason text, p_filters jsonb)",
|
|
54
58
|
"declares_p_metadata": false
|