@consiliency/agent-board-schema 1.52.0 → 1.54.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 +611 -603
- package/migrations/20261003000100_agent_board_room_roster.sql +115 -0
- package/migrations/20261004000100_agent_board_callback_consumer_intent.sql +52 -0
- package/package.json +1 -1
- package/schema-manifest.json +21 -3
- package/src/tooling.test.mjs +5 -2
- package/tests/agent_board_room_roster.test.sql +466 -0
- package/tests/agent_board_rowid.test.sql +5 -4
- package/tests/catalog/function_execute_privileges.json +9 -0
- package/tests/catalog/p_metadata_parameters.json +4 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
-- ROOMCONTRACT pgTAP: owner/admin active roster RPC plus self-membership role.
|
|
2
|
+
--
|
|
3
|
+
-- Mutation controls (the assertion names are part of the evidence contract):
|
|
4
|
+
-- * removing the owner/admin gate fails RC-32 (member denial);
|
|
5
|
+
-- * filtering active state before latest-row selection fails RC-38 (stale row);
|
|
6
|
+
-- * removing the id DESC tiebreak fails RC-39 (equal timestamp);
|
|
7
|
+
-- * omitting role from v_board_memberships fails RC-12 (catalog ordinal);
|
|
8
|
+
-- * removing the roster board predicate fails RC-37 (cross-board isolation).
|
|
9
|
+
--
|
|
10
|
+
-- HAND-COUNTED plan: RC-01..RC-44 = 44 assertions. The runner independently
|
|
11
|
+
-- verifies the plan and emitted assertion counts, so a truncated file cannot pass.
|
|
12
|
+
create extension if not exists pgtap with schema extensions;
|
|
13
|
+
set search_path = extensions, public, agent_board;
|
|
14
|
+
|
|
15
|
+
begin;
|
|
16
|
+
|
|
17
|
+
select plan(44);
|
|
18
|
+
|
|
19
|
+
-- ---------------------------------------------------------------------------
|
|
20
|
+
-- Catalog contract: exact signature, projection, posture, privileges, and view.
|
|
21
|
+
-- ---------------------------------------------------------------------------
|
|
22
|
+
select has_function(
|
|
23
|
+
'agent_board', 'list_board_members', array['uuid'],
|
|
24
|
+
'RC-01: list_board_members(uuid) exists');
|
|
25
|
+
|
|
26
|
+
select is(
|
|
27
|
+
(select pg_get_function_identity_arguments(p.oid)
|
|
28
|
+
from pg_proc p
|
|
29
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
30
|
+
where n.nspname = 'agent_board' and p.proname = 'list_board_members'),
|
|
31
|
+
'p_board_id uuid',
|
|
32
|
+
'RC-02: list_board_members has the exact named input argument');
|
|
33
|
+
|
|
34
|
+
select is(
|
|
35
|
+
(select string_agg(u.arg_name || ' ' || format_type(u.type_oid, null), ', ' order by u.ord)
|
|
36
|
+
from pg_proc p
|
|
37
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
38
|
+
cross join lateral unnest(p.proallargtypes, p.proargnames, p.proargmodes)
|
|
39
|
+
with ordinality as u(type_oid, arg_name, arg_mode, ord)
|
|
40
|
+
where n.nspname = 'agent_board'
|
|
41
|
+
and p.proname = 'list_board_members'
|
|
42
|
+
and u.arg_mode = 't'),
|
|
43
|
+
'membership_id uuid, board_id uuid, agent_id uuid, handle text, display_name text, agent_type text, membership_state text, role text, created_by_agent_id uuid, membership_created_at timestamp with time zone, membership_updated_at timestamp with time zone',
|
|
44
|
+
'RC-03: list_board_members exposes only the frozen roster projection');
|
|
45
|
+
|
|
46
|
+
select is(
|
|
47
|
+
(select l.lanname
|
|
48
|
+
from pg_proc p
|
|
49
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
50
|
+
join pg_language l on l.oid = p.prolang
|
|
51
|
+
where n.nspname = 'agent_board' and p.proname = 'list_board_members'),
|
|
52
|
+
'plpgsql',
|
|
53
|
+
'RC-04: list_board_members is implemented in plpgsql');
|
|
54
|
+
|
|
55
|
+
select is(
|
|
56
|
+
(select p.provolatile::text
|
|
57
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
58
|
+
where n.nspname = 'agent_board' and p.proname = 'list_board_members'),
|
|
59
|
+
's',
|
|
60
|
+
'RC-05: list_board_members is STABLE');
|
|
61
|
+
|
|
62
|
+
select is(
|
|
63
|
+
(select p.prosecdef
|
|
64
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
65
|
+
where n.nspname = 'agent_board' and p.proname = 'list_board_members'),
|
|
66
|
+
true,
|
|
67
|
+
'RC-06: list_board_members is SECURITY DEFINER');
|
|
68
|
+
|
|
69
|
+
select is(
|
|
70
|
+
(select p.proconfig
|
|
71
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
72
|
+
where n.nspname = 'agent_board' and p.proname = 'list_board_members'),
|
|
73
|
+
array['search_path=agent_board, public']::text[],
|
|
74
|
+
'RC-07: list_board_members fixes search_path to agent_board, public');
|
|
75
|
+
|
|
76
|
+
select is(
|
|
77
|
+
has_function_privilege('public', to_regprocedure('agent_board.list_board_members(uuid)'), 'EXECUTE'),
|
|
78
|
+
false,
|
|
79
|
+
'RC-08: PUBLIC cannot execute list_board_members');
|
|
80
|
+
select is(
|
|
81
|
+
has_function_privilege('anon', to_regprocedure('agent_board.list_board_members(uuid)'), 'EXECUTE'),
|
|
82
|
+
false,
|
|
83
|
+
'RC-09: anon cannot execute list_board_members');
|
|
84
|
+
select is(
|
|
85
|
+
has_function_privilege('authenticated', to_regprocedure('agent_board.list_board_members(uuid)'), 'EXECUTE'),
|
|
86
|
+
true,
|
|
87
|
+
'RC-10: authenticated can execute list_board_members');
|
|
88
|
+
select is(
|
|
89
|
+
has_function_privilege('service_role', to_regprocedure('agent_board.list_board_members(uuid)'), 'EXECUTE'),
|
|
90
|
+
true,
|
|
91
|
+
'RC-11: service_role holds EXECUTE even though the user-facing gate denies its null actor');
|
|
92
|
+
|
|
93
|
+
select is(
|
|
94
|
+
(select array_agg(c.column_name::text order by c.ordinal_position)
|
|
95
|
+
from information_schema.columns c
|
|
96
|
+
where c.table_schema = 'agent_board' and c.table_name = 'v_board_memberships'),
|
|
97
|
+
array[
|
|
98
|
+
'board_membership_id', 'board_id', 'board_slug', 'board_name', 'agent_id',
|
|
99
|
+
'agent_handle', 'agent_display_name', 'membership_state', 'created_by_agent_id',
|
|
100
|
+
'created_by_handle', 'left_at', 'created_at', 'updated_at', 'role'
|
|
101
|
+
]::text[],
|
|
102
|
+
'RC-12: v_board_memberships preserves prior ordinals and appends role after updated_at');
|
|
103
|
+
|
|
104
|
+
select is(
|
|
105
|
+
(select 'security_invoker=true' = any(coalesce(c.reloptions, '{}'::text[]))
|
|
106
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
107
|
+
where n.nspname = 'agent_board' and c.relname = 'v_board_memberships'),
|
|
108
|
+
true,
|
|
109
|
+
'RC-13: v_board_memberships remains security_invoker');
|
|
110
|
+
|
|
111
|
+
select is(
|
|
112
|
+
has_table_privilege('authenticated', 'agent_board.v_board_memberships', 'SELECT'),
|
|
113
|
+
true,
|
|
114
|
+
'RC-14: authenticated retains SELECT on v_board_memberships');
|
|
115
|
+
select is(
|
|
116
|
+
has_table_privilege('service_role', 'agent_board.v_board_memberships', 'SELECT'),
|
|
117
|
+
true,
|
|
118
|
+
'RC-15: service_role retains SELECT on v_board_memberships');
|
|
119
|
+
|
|
120
|
+
select is(
|
|
121
|
+
(select count(*)::int
|
|
122
|
+
from pg_proc p
|
|
123
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
124
|
+
cross join lateral unnest(coalesce(p.proargnames, '{}'::text[])) as a(arg_name)
|
|
125
|
+
where n.nspname = 'agent_board'
|
|
126
|
+
and p.proname = 'list_board_members'
|
|
127
|
+
and a.arg_name in (
|
|
128
|
+
'metadata', 'auth_user_id', 'owner_user_id', 'email', 'invite_code',
|
|
129
|
+
'token_hash', 'credential_id'
|
|
130
|
+
)),
|
|
131
|
+
0,
|
|
132
|
+
'RC-16: the roster contract exposes no metadata, auth, credential, email, or invite field');
|
|
133
|
+
|
|
134
|
+
select (
|
|
135
|
+
to_regprocedure('agent_board.list_board_members(uuid)') is not null
|
|
136
|
+
and exists (
|
|
137
|
+
select 1
|
|
138
|
+
from information_schema.columns
|
|
139
|
+
where table_schema = 'agent_board'
|
|
140
|
+
and table_name = 'v_board_memberships'
|
|
141
|
+
and column_name = 'role'
|
|
142
|
+
)
|
|
143
|
+
) as roomcontract_ready \gset
|
|
144
|
+
|
|
145
|
+
\if :roomcontract_ready
|
|
146
|
+
|
|
147
|
+
-- ---------------------------------------------------------------------------
|
|
148
|
+
-- Fixture: two boards and distinct authority, denial, and history actors.
|
|
149
|
+
-- ---------------------------------------------------------------------------
|
|
150
|
+
create temp table _roomcontract_fixture (
|
|
151
|
+
board_a uuid,
|
|
152
|
+
board_b uuid,
|
|
153
|
+
creator uuid,
|
|
154
|
+
owner_agent uuid,
|
|
155
|
+
admin_agent uuid,
|
|
156
|
+
member_agent uuid,
|
|
157
|
+
observer_agent uuid,
|
|
158
|
+
stale_agent uuid,
|
|
159
|
+
tie_agent uuid,
|
|
160
|
+
board_b_agent uuid,
|
|
161
|
+
creator_user uuid,
|
|
162
|
+
owner_user uuid,
|
|
163
|
+
admin_user uuid,
|
|
164
|
+
member_user uuid,
|
|
165
|
+
observer_user uuid,
|
|
166
|
+
nonmember_user uuid,
|
|
167
|
+
suspended_user uuid
|
|
168
|
+
) on commit drop;
|
|
169
|
+
grant all on table _roomcontract_fixture to authenticated, service_role;
|
|
170
|
+
|
|
171
|
+
do $$
|
|
172
|
+
declare
|
|
173
|
+
v_creator_user uuid;
|
|
174
|
+
v_owner_user uuid;
|
|
175
|
+
v_admin_user uuid;
|
|
176
|
+
v_member_user uuid;
|
|
177
|
+
v_observer_user uuid;
|
|
178
|
+
v_nonmember_user uuid;
|
|
179
|
+
v_suspended_user uuid;
|
|
180
|
+
begin
|
|
181
|
+
v_creator_user := public.ensure_github_user('rc-creator', 'rc-creator@example.test', 'rc-creator', 'RC Creator');
|
|
182
|
+
v_owner_user := public.ensure_github_user('rc-owner', 'rc-owner@example.test', 'rc-owner', 'RC Owner');
|
|
183
|
+
v_admin_user := public.ensure_github_user('rc-admin', 'rc-admin@example.test', 'rc-admin', 'RC Admin');
|
|
184
|
+
v_member_user := public.ensure_github_user('rc-member', 'rc-member@example.test', 'rc-member', 'RC Member');
|
|
185
|
+
v_observer_user := public.ensure_github_user('rc-observer', 'rc-observer@example.test', 'rc-observer', 'RC Observer');
|
|
186
|
+
v_nonmember_user := public.ensure_github_user('rc-nonmember', 'rc-nonmember@example.test', 'rc-nonmember', 'RC Nonmember');
|
|
187
|
+
v_suspended_user := public.ensure_github_user('rc-suspended', 'rc-suspended@example.test', 'rc-suspended', 'RC Suspended');
|
|
188
|
+
|
|
189
|
+
insert into agent_board.agents(
|
|
190
|
+
id, agent_type, handle, display_name, status, auth_user_id, owner_user_id
|
|
191
|
+
) values
|
|
192
|
+
('73000000-0000-4000-8000-000000000001', 'human', 'rc-creator', 'RC Creator', 'active', v_creator_user, v_creator_user),
|
|
193
|
+
('73000000-0000-4000-8000-000000000002', 'human', 'rc-owner', 'RC Owner', 'active', v_owner_user, v_owner_user),
|
|
194
|
+
('73000000-0000-4000-8000-000000000003', 'human', 'rc-admin', 'RC Admin', 'active', v_admin_user, v_admin_user),
|
|
195
|
+
('73000000-0000-4000-8000-000000000004', 'agent', 'zulu-rc', 'Zulu RC', 'active', v_member_user, v_member_user),
|
|
196
|
+
('73000000-0000-4000-8000-000000000005', 'agent', 'Alpha-rc', 'Alpha RC', 'active', null, null),
|
|
197
|
+
('73000000-0000-4000-8000-000000000006', 'agent', 'same-rc', 'Same RC lower', 'active', null, null),
|
|
198
|
+
('73000000-0000-4000-8000-000000000007', 'agent', 'SAME-rc', 'Same RC upper', 'active', null, null),
|
|
199
|
+
('73000000-0000-4000-8000-000000000008', 'agent', 'rc-observer', 'RC Observer', 'active', v_observer_user, v_observer_user),
|
|
200
|
+
('73000000-0000-4000-8000-000000000009', 'agent', 'rc-nonmember', 'RC Nonmember', 'active', v_nonmember_user, v_nonmember_user),
|
|
201
|
+
('73000000-0000-4000-8000-00000000000a', 'agent', 'rc-suspended', 'RC Suspended', 'suspended', v_suspended_user, v_suspended_user),
|
|
202
|
+
('73000000-0000-4000-8000-00000000000b', 'agent', 'rc-stale', 'RC Stale', 'active', null, null),
|
|
203
|
+
('73000000-0000-4000-8000-00000000000c', 'agent', 'rc-tie', 'RC Tie', 'active', null, null),
|
|
204
|
+
('73000000-0000-4000-8000-00000000000d', 'agent', 'rc-board-b', 'RC Board B', 'active', null, null);
|
|
205
|
+
|
|
206
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, status)
|
|
207
|
+
values
|
|
208
|
+
('73010000-0000-4000-8000-000000000001', 'roomcontract-a', 'ROOMCONTRACT A', '73000000-0000-4000-8000-000000000001', 'active'),
|
|
209
|
+
('73010000-0000-4000-8000-000000000002', 'roomcontract-b', 'ROOMCONTRACT B', '73000000-0000-4000-8000-000000000001', 'active');
|
|
210
|
+
|
|
211
|
+
-- Board A current active roster: owner, admin, four members, observer.
|
|
212
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
|
|
213
|
+
values
|
|
214
|
+
('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000002', 'joined', 'owner', '73000000-0000-4000-8000-000000000001'),
|
|
215
|
+
('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000003', 'subscribed', 'admin', '73000000-0000-4000-8000-000000000002'),
|
|
216
|
+
('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000004', 'joined', 'member', '73000000-0000-4000-8000-000000000002'),
|
|
217
|
+
('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000005', 'joined', 'member', '73000000-0000-4000-8000-000000000002'),
|
|
218
|
+
('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000006', 'joined', 'member', '73000000-0000-4000-8000-000000000002'),
|
|
219
|
+
('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000007', 'joined', 'member', '73000000-0000-4000-8000-000000000002'),
|
|
220
|
+
('73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000008', 'joined', 'observer', '73000000-0000-4000-8000-000000000002');
|
|
221
|
+
|
|
222
|
+
-- The board creator has historical owner standing but a newer departed row.
|
|
223
|
+
insert into agent_board.board_memberships(
|
|
224
|
+
id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
|
|
225
|
+
) values
|
|
226
|
+
('73020000-0000-4000-8000-000000000001', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000001', 'joined', 'owner', '73000000-0000-4000-8000-000000000001', null, '2026-09-01 00:00:00+00', '2026-09-01 00:00:00+00'),
|
|
227
|
+
('73020000-0000-4000-8000-000000000002', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-000000000001', 'left', 'member', '73000000-0000-4000-8000-000000000001', '2026-09-02 00:00:00+00', '2026-09-02 00:00:00+00', '2026-09-02 00:00:00+00');
|
|
228
|
+
|
|
229
|
+
-- Stale-row control: selecting active rows before choosing latest resurrects this actor.
|
|
230
|
+
insert into agent_board.board_memberships(
|
|
231
|
+
id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
|
|
232
|
+
) values
|
|
233
|
+
('73020000-0000-4000-8000-000000000003', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000b', 'joined', 'member', '73000000-0000-4000-8000-000000000002', null, '2026-09-03 00:00:00+00', '2026-09-03 00:00:00+00'),
|
|
234
|
+
('73020000-0000-4000-8000-000000000004', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000b', 'left', 'member', '73000000-0000-4000-8000-000000000002', '2026-09-04 00:00:00+00', '2026-09-04 00:00:00+00', '2026-09-04 00:00:00+00');
|
|
235
|
+
|
|
236
|
+
-- Total-order control: timestamps tie, so the larger removed-row id must win.
|
|
237
|
+
insert into agent_board.board_memberships(
|
|
238
|
+
id, board_id, agent_id, membership_state, role, created_by_agent_id, left_at, created_at, updated_at
|
|
239
|
+
) values
|
|
240
|
+
('73020000-0000-4000-8000-000000000010', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000c', 'joined', 'member', '73000000-0000-4000-8000-000000000002', null, '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00'),
|
|
241
|
+
('73020000-0000-4000-8000-000000000011', '73010000-0000-4000-8000-000000000001', '73000000-0000-4000-8000-00000000000c', 'removed', 'member', '73000000-0000-4000-8000-000000000002', '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00', '2026-09-05 00:00:00+00');
|
|
242
|
+
|
|
243
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
|
|
244
|
+
values
|
|
245
|
+
('73010000-0000-4000-8000-000000000002', '73000000-0000-4000-8000-00000000000d', 'joined', 'member', '73000000-0000-4000-8000-000000000001');
|
|
246
|
+
|
|
247
|
+
insert into _roomcontract_fixture values (
|
|
248
|
+
'73010000-0000-4000-8000-000000000001',
|
|
249
|
+
'73010000-0000-4000-8000-000000000002',
|
|
250
|
+
'73000000-0000-4000-8000-000000000001',
|
|
251
|
+
'73000000-0000-4000-8000-000000000002',
|
|
252
|
+
'73000000-0000-4000-8000-000000000003',
|
|
253
|
+
'73000000-0000-4000-8000-000000000004',
|
|
254
|
+
'73000000-0000-4000-8000-000000000008',
|
|
255
|
+
'73000000-0000-4000-8000-00000000000b',
|
|
256
|
+
'73000000-0000-4000-8000-00000000000c',
|
|
257
|
+
'73000000-0000-4000-8000-00000000000d',
|
|
258
|
+
v_creator_user, v_owner_user, v_admin_user, v_member_user, v_observer_user,
|
|
259
|
+
v_nonmember_user, v_suspended_user
|
|
260
|
+
);
|
|
261
|
+
end
|
|
262
|
+
$$;
|
|
263
|
+
|
|
264
|
+
-- ---------------------------------------------------------------------------
|
|
265
|
+
-- Real authenticated posture and security-invoker self-view behavior.
|
|
266
|
+
-- ---------------------------------------------------------------------------
|
|
267
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
268
|
+
select set_config('request.jwt.claim.sub', (select member_user::text from _roomcontract_fixture), true);
|
|
269
|
+
set local role authenticated;
|
|
270
|
+
|
|
271
|
+
select is(current_setting('request.jwt.claim.role', true), 'authenticated',
|
|
272
|
+
'RC-17: authenticated probe carries request.jwt.claim.role=authenticated');
|
|
273
|
+
select is(current_setting('request.jwt.claim.sub', true), (select member_user::text from _roomcontract_fixture),
|
|
274
|
+
'RC-18: authenticated probe carries request.jwt.claim.sub for the selected principal');
|
|
275
|
+
select is(agent_board.current_agent_id(), (select member_agent from _roomcontract_fixture),
|
|
276
|
+
'RC-19: current_agent_id resolves the selected active member');
|
|
277
|
+
reset role;
|
|
278
|
+
select is(agent_board._is_service_role(), false,
|
|
279
|
+
'RC-20: the authenticated JWT posture is not classified as service_role');
|
|
280
|
+
set local role authenticated;
|
|
281
|
+
select is((select count(*)::int from agent_board.v_board_memberships), 1,
|
|
282
|
+
'RC-21: v_board_memberships remains self-scoped to one caller row');
|
|
283
|
+
select is((select agent_id from agent_board.v_board_memberships), (select member_agent from _roomcontract_fixture),
|
|
284
|
+
'RC-22: the self-scoped membership row belongs to the caller');
|
|
285
|
+
select is((select role from agent_board.v_board_memberships), 'member',
|
|
286
|
+
'RC-23: the appended self-membership role is readable');
|
|
287
|
+
|
|
288
|
+
reset role;
|
|
289
|
+
|
|
290
|
+
-- ---------------------------------------------------------------------------
|
|
291
|
+
-- Owner/admin roster access and exact active current-row result semantics.
|
|
292
|
+
-- ---------------------------------------------------------------------------
|
|
293
|
+
select set_config('request.jwt.claim.sub', (select owner_user::text from _roomcontract_fixture), true);
|
|
294
|
+
set local role authenticated;
|
|
295
|
+
|
|
296
|
+
select lives_ok(
|
|
297
|
+
(select format($q$select * from agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
298
|
+
'RC-24: an active owner may list the roster');
|
|
299
|
+
select is(
|
|
300
|
+
(select count(*)::int from agent_board.list_board_members((select board_a from _roomcontract_fixture))),
|
|
301
|
+
7,
|
|
302
|
+
'RC-25: owner sees exactly seven current active members');
|
|
303
|
+
select is(
|
|
304
|
+
(select jsonb_build_object(
|
|
305
|
+
'joined', count(*) filter (where membership_state = 'joined'),
|
|
306
|
+
'subscribed', count(*) filter (where membership_state = 'subscribed')
|
|
307
|
+
)
|
|
308
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture))),
|
|
309
|
+
'{"joined": 6, "subscribed": 1}'::jsonb,
|
|
310
|
+
'RC-26: joined and subscribed rows are both included, and no other state is returned');
|
|
311
|
+
select is(
|
|
312
|
+
(select count(*)::int
|
|
313
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture))
|
|
314
|
+
where board_id <> (select board_a from _roomcontract_fixture)),
|
|
315
|
+
0,
|
|
316
|
+
'RC-27: every returned row belongs to the requested board');
|
|
317
|
+
select is(
|
|
318
|
+
(select array_agg(r.agent_id::text order by r.ordinality)
|
|
319
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture))
|
|
320
|
+
with ordinality as r),
|
|
321
|
+
array[
|
|
322
|
+
'73000000-0000-4000-8000-000000000002',
|
|
323
|
+
'73000000-0000-4000-8000-000000000003',
|
|
324
|
+
'73000000-0000-4000-8000-000000000005',
|
|
325
|
+
'73000000-0000-4000-8000-000000000006',
|
|
326
|
+
'73000000-0000-4000-8000-000000000007',
|
|
327
|
+
'73000000-0000-4000-8000-000000000004',
|
|
328
|
+
'73000000-0000-4000-8000-000000000008'
|
|
329
|
+
]::text[],
|
|
330
|
+
'RC-28: roster order is owner, admin, member, observer then lower(handle), agent_id');
|
|
331
|
+
select ok(
|
|
332
|
+
(select bool_and(
|
|
333
|
+
membership_id is not null
|
|
334
|
+
and board_id is not null
|
|
335
|
+
and agent_id is not null
|
|
336
|
+
and handle is not null
|
|
337
|
+
and display_name is not null
|
|
338
|
+
and agent_type is not null
|
|
339
|
+
and membership_state is not null
|
|
340
|
+
and role is not null
|
|
341
|
+
and created_by_agent_id is not null
|
|
342
|
+
and membership_created_at is not null
|
|
343
|
+
and membership_updated_at is not null
|
|
344
|
+
)
|
|
345
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture))),
|
|
346
|
+
'RC-29: every frozen roster projection field is populated for the fixture');
|
|
347
|
+
select ok(
|
|
348
|
+
(select bool_and(not (to_jsonb(r) ?| array[
|
|
349
|
+
'metadata', 'auth_user_id', 'owner_user_id', 'email', 'invite_code',
|
|
350
|
+
'token_hash', 'credential_id'
|
|
351
|
+
]))
|
|
352
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture)) r),
|
|
353
|
+
'RC-30: returned roster rows contain no metadata, auth, credential, email, or invite material');
|
|
354
|
+
|
|
355
|
+
reset role;
|
|
356
|
+
|
|
357
|
+
select set_config('request.jwt.claim.sub', (select admin_user::text from _roomcontract_fixture), true);
|
|
358
|
+
set local role authenticated;
|
|
359
|
+
select lives_ok(
|
|
360
|
+
(select format($q$select * from agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
361
|
+
'RC-31: an active admin may list the roster');
|
|
362
|
+
reset role;
|
|
363
|
+
|
|
364
|
+
-- ---------------------------------------------------------------------------
|
|
365
|
+
-- Denials: member, observer, non-member, null actor, and creator exception.
|
|
366
|
+
-- ---------------------------------------------------------------------------
|
|
367
|
+
select set_config('request.jwt.claim.sub', (select member_user::text from _roomcontract_fixture), true);
|
|
368
|
+
set local role authenticated;
|
|
369
|
+
select throws_ok(
|
|
370
|
+
(select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
371
|
+
'42501', 'only a board owner or admin may list board members',
|
|
372
|
+
'RC-32: an ordinary active member cannot enumerate the roster');
|
|
373
|
+
reset role;
|
|
374
|
+
|
|
375
|
+
select set_config('request.jwt.claim.sub', (select observer_user::text from _roomcontract_fixture), true);
|
|
376
|
+
set local role authenticated;
|
|
377
|
+
select throws_ok(
|
|
378
|
+
(select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
379
|
+
'42501', 'only a board owner or admin may list board members',
|
|
380
|
+
'RC-33: an active observer cannot enumerate the roster');
|
|
381
|
+
reset role;
|
|
382
|
+
|
|
383
|
+
select set_config('request.jwt.claim.sub', (select nonmember_user::text from _roomcontract_fixture), true);
|
|
384
|
+
set local role authenticated;
|
|
385
|
+
select throws_ok(
|
|
386
|
+
(select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
387
|
+
'42501', 'only a board owner or admin may list board members',
|
|
388
|
+
'RC-34: an active non-member cannot enumerate the roster');
|
|
389
|
+
reset role;
|
|
390
|
+
|
|
391
|
+
select set_config('request.jwt.claim.sub', (select suspended_user::text from _roomcontract_fixture), true);
|
|
392
|
+
set local role authenticated;
|
|
393
|
+
select throws_ok(
|
|
394
|
+
(select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
395
|
+
'42501', 'only a board owner or admin may list board members',
|
|
396
|
+
'RC-35: a suspended principal resolves to a null actor and is denied');
|
|
397
|
+
reset role;
|
|
398
|
+
|
|
399
|
+
select set_config('request.jwt.claim.sub', (select creator_user::text from _roomcontract_fixture), true);
|
|
400
|
+
set local role authenticated;
|
|
401
|
+
select throws_ok(
|
|
402
|
+
(select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
403
|
+
'42501', 'only a board owner or admin may list board members',
|
|
404
|
+
'RC-36: board creator identity is not authority after its latest membership departed');
|
|
405
|
+
|
|
406
|
+
reset role;
|
|
407
|
+
|
|
408
|
+
-- ---------------------------------------------------------------------------
|
|
409
|
+
-- Mutation-sensitive result controls, run as the owner.
|
|
410
|
+
-- ---------------------------------------------------------------------------
|
|
411
|
+
select set_config('request.jwt.claim.sub', (select owner_user::text from _roomcontract_fixture), true);
|
|
412
|
+
set local role authenticated;
|
|
413
|
+
select is(
|
|
414
|
+
(select count(*)::int
|
|
415
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture))
|
|
416
|
+
where agent_id = (select board_b_agent from _roomcontract_fixture)),
|
|
417
|
+
0,
|
|
418
|
+
'RC-37: board B membership is absent from board A roster (cross-board isolation)');
|
|
419
|
+
select is(
|
|
420
|
+
(select count(*)::int
|
|
421
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture))
|
|
422
|
+
where agent_id = (select stale_agent from _roomcontract_fixture)),
|
|
423
|
+
0,
|
|
424
|
+
'RC-38: latest left row excludes an older joined row (choose latest before filtering state)');
|
|
425
|
+
select is(
|
|
426
|
+
(select count(*)::int
|
|
427
|
+
from agent_board.list_board_members((select board_a from _roomcontract_fixture))
|
|
428
|
+
where agent_id = (select tie_agent from _roomcontract_fixture)),
|
|
429
|
+
0,
|
|
430
|
+
'RC-39: equal created_at rows use id DESC, so the larger removed row excludes the older joined row');
|
|
431
|
+
reset role;
|
|
432
|
+
|
|
433
|
+
-- ---------------------------------------------------------------------------
|
|
434
|
+
-- service_role is executable at the ACL layer but denied by the user-facing gate.
|
|
435
|
+
-- ---------------------------------------------------------------------------
|
|
436
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
437
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
438
|
+
set local role service_role;
|
|
439
|
+
select is(current_setting('request.jwt.claim.role', true), 'service_role',
|
|
440
|
+
'RC-40: service probe carries request.jwt.claim.role=service_role');
|
|
441
|
+
select is(current_setting('request.jwt.claim.sub', true), '',
|
|
442
|
+
'RC-41: service probe carries an explicitly cleared request.jwt.claim.sub');
|
|
443
|
+
select is(agent_board.current_agent_id(), null::uuid,
|
|
444
|
+
'RC-42: service_role has no current_agent_id in the user-facing path');
|
|
445
|
+
select is(agent_board._is_service_role(), true,
|
|
446
|
+
'RC-43: the service probe is classified as service_role');
|
|
447
|
+
select throws_ok(
|
|
448
|
+
(select format($q$select agent_board.list_board_members(%L::uuid)$q$, board_a) from _roomcontract_fixture),
|
|
449
|
+
'42501', 'only a board owner or admin may list board members',
|
|
450
|
+
'RC-44: service_role receives 42501 rather than bypassing the null-actor gate');
|
|
451
|
+
reset role;
|
|
452
|
+
|
|
453
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
454
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
455
|
+
|
|
456
|
+
\else
|
|
457
|
+
|
|
458
|
+
select * from skip(
|
|
459
|
+
28,
|
|
460
|
+
'ROOMCONTRACT runtime checks require list_board_members and v_board_memberships.role'
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
\endif
|
|
464
|
+
|
|
465
|
+
select * from finish();
|
|
466
|
+
rollback;
|
|
@@ -506,9 +506,9 @@ select is(
|
|
|
506
506
|
true,
|
|
507
507
|
'control: fixture and structural block really runs as service_role');
|
|
508
508
|
|
|
509
|
-
-- 2. Catalog-derived inventory:
|
|
510
|
-
-- readers. request_join_board joined the set in MBCONSENT-B(iii)
|
|
511
|
-
--
|
|
509
|
+
-- 2. Catalog-derived inventory: eight membership readers plus the two task/message idempotency
|
|
510
|
+
-- readers. request_join_board joined the set in MBCONSENT-B(iii), and list_board_members joined
|
|
511
|
+
-- it in ROOMCONTRACT; both read board_memberships with the created_at DESC + id DESC tiebreak.
|
|
512
512
|
select is(
|
|
513
513
|
(select array_agg(proname order by proname)
|
|
514
514
|
from _rowid_function_order_counts),
|
|
@@ -517,13 +517,14 @@ select is(
|
|
|
517
517
|
'_upsert_board_membership_state',
|
|
518
518
|
'create_tasking_delivery',
|
|
519
519
|
'join_board',
|
|
520
|
+
'list_board_members',
|
|
520
521
|
'list_board_pending_memberships',
|
|
521
522
|
'post_board_message',
|
|
522
523
|
'request_join_board',
|
|
523
524
|
'resolve_board_request',
|
|
524
525
|
'update_board_membership'
|
|
525
526
|
]::text[],
|
|
526
|
-
'live catalog exposes
|
|
527
|
+
'live catalog exposes eight membership readers plus two task/message idempotency readers');
|
|
527
528
|
|
|
528
529
|
-- 3-10. COUNT-EQUALITY, never substring containment.
|
|
529
530
|
select is(
|
|
@@ -1063,6 +1063,15 @@
|
|
|
1063
1063
|
"authenticated": true
|
|
1064
1064
|
}
|
|
1065
1065
|
},
|
|
1066
|
+
{
|
|
1067
|
+
"name": "list_board_members",
|
|
1068
|
+
"signature": "list_board_members(p_board_id uuid)",
|
|
1069
|
+
"executable_by": {
|
|
1070
|
+
"anon": false,
|
|
1071
|
+
"service_role": true,
|
|
1072
|
+
"authenticated": true
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1066
1075
|
{
|
|
1067
1076
|
"name": "list_board_pending_memberships",
|
|
1068
1077
|
"signature": "list_board_pending_memberships(p_board_id uuid)",
|
|
@@ -473,6 +473,10 @@
|
|
|
473
473
|
"signature": "leave_board(p_board_id uuid, p_agent_id uuid, p_metadata jsonb)",
|
|
474
474
|
"declares_p_metadata": true
|
|
475
475
|
},
|
|
476
|
+
{
|
|
477
|
+
"signature": "list_board_members(p_board_id uuid)",
|
|
478
|
+
"declares_p_metadata": false
|
|
479
|
+
},
|
|
476
480
|
{
|
|
477
481
|
"signature": "list_board_pending_memberships(p_board_id uuid)",
|
|
478
482
|
"declares_p_metadata": false
|