@consiliency/agent-board-schema 1.51.0 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/fixtures/already-applied.json +4 -0
  2. package/migrations/20261003000100_agent_board_room_roster.sql +115 -0
  3. package/package.json +1 -1
  4. package/schema-manifest.json +12 -3
  5. package/src/tooling.test.mjs +4 -2
  6. package/tests/agent_board_agent_liveness.test.sql +3 -1
  7. package/tests/agent_board_atomic_fleet_onboarding.test.sql +33 -36
  8. package/tests/agent_board_authbound_boundary.test.sql +4 -3
  9. package/tests/agent_board_board_claim_policy.test.sql +17 -6
  10. package/tests/agent_board_claim_subject_gate.test.sql +7 -8
  11. package/tests/agent_board_claimkey.test.sql +9 -6
  12. package/tests/agent_board_claimpolicy_race.test.sql +1466 -342
  13. package/tests/agent_board_consent_emission.test.sql +5 -4
  14. package/tests/agent_board_consent_gating.test.sql +20 -9
  15. package/tests/agent_board_enroll_resumption.test.sql +1 -1
  16. package/tests/agent_board_enrollment_audit_content.test.sql +5 -4
  17. package/tests/agent_board_federated_identity_injective_slug.test.sql +3 -1
  18. package/tests/agent_board_federated_lock_order_class.test.sql +69 -52
  19. package/tests/agent_board_fedsession.test.sql +3 -1
  20. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -4
  21. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +18 -6
  22. package/tests/agent_board_fk_lock_edges.test.sql +64 -5
  23. package/tests/agent_board_fleet_domain_reclaim.test.sql +2 -4
  24. package/tests/agent_board_fleet_machine_hijack.test.sql +2 -4
  25. package/tests/agent_board_fleet_machine_reactivation.test.sql +5 -1
  26. package/tests/agent_board_fleet_machine_reclaim.test.sql +2 -4
  27. package/tests/agent_board_invite_no_downgrade.test.sql +24 -18
  28. package/tests/agent_board_ledger_authority.test.sql +29 -21
  29. package/tests/agent_board_mint_harden.test.sql +5 -5
  30. package/tests/agent_board_outbox_wake_ordering.test.sql +4 -3
  31. package/tests/agent_board_ownerless_renew_quota.test.sql +7 -2
  32. package/tests/agent_board_portal_offline_invariant.test.sql +5 -4
  33. package/tests/agent_board_receipt_subject_gate.test.sql +7 -8
  34. package/tests/agent_board_rolereset.test.sql +5 -3
  35. package/tests/agent_board_room_roster.test.sql +466 -0
  36. package/tests/agent_board_rowid.test.sql +39 -19
  37. package/tests/agent_board_signer_lifecycle.test.sql +24 -13
  38. package/tests/agent_board_source_strip_helper.test.sql +2 -1
  39. package/tests/agent_board_task_event_origin_posture.test.sql +21 -9
  40. package/tests/agent_board_wake_recovery.test.sql +53 -26
  41. package/tests/agent_board_write_enforcement_class.test.sql +130 -12
  42. package/tests/catalog/function_execute_privileges.json +1932 -0
  43. package/tests/catalog/p_metadata_parameters.json +4 -0
  44. package/tests/fixtures/test-prereqs.sql +644 -237
@@ -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;
@@ -48,7 +48,8 @@ statements as (
48
48
  proname,
49
49
  args,
50
50
  statement_ordinality::int as statement_ordinality,
51
- statement
51
+ -- bound through `def` so the source-scan lint can follow `statement` to its producer
52
+ split.statement as statement
52
53
  from defs
53
54
  cross join lateral regexp_split_to_table(def, ';')
54
55
  with ordinality as split(statement, statement_ordinality)
@@ -57,33 +58,45 @@ select
57
58
  proname,
58
59
  args,
59
60
  statement_ordinality,
61
+ -- `from <rel>` through stmt_re: the verb is a word, the relation is schema-qualified
62
+ -- (`bare_ok => false`, as the hand-rolled `agent_board\.` was), case folded by the family
63
+ -- rather than by `~*`. The ORDER BY operands are `created_at` and `id`, qualified or bare
64
+ -- (col_ref_frag: `m.created_at desc` is the common spelling).
60
65
  case
61
- when statement ~* 'from\s+agent_board\.board_memberships'
66
+ when statement ~ agent_board_test.stmt_re('from', 'board_memberships', false)
62
67
  then 'board_memberships'
63
- when statement ~* 'from\s+agent_board\.tasks'
68
+ when statement ~ agent_board_test.stmt_re('from', 'tasks', false)
64
69
  then 'tasks'
65
- when statement ~* 'from\s+agent_board\.messages'
70
+ when statement ~ agent_board_test.stmt_re('from', 'messages', false)
66
71
  then 'messages'
67
72
  end as source_relation,
68
73
  (select count(*)::int
69
- from regexp_matches(statement, 'created_at\s+desc', 'gi')) as created_at_desc_count,
74
+ from regexp_matches(statement,
75
+ agent_board_test.re(agent_board_test.col_ref_frag('created_at')
76
+ || '\s+' || agent_board_test.ci_re('desc') || agent_board_test.trail_guard()),
77
+ 'g')) as created_at_desc_count,
70
78
  (select count(*)::int
71
79
  from regexp_matches(
72
80
  statement,
73
- 'created_at\s+desc\s*,\s*(?:[a-z_][a-z_0-9]*\.)?id\s+desc',
74
- 'gi'
81
+ agent_board_test.re(agent_board_test.col_ref_frag('created_at')
82
+ || '\s+' || agent_board_test.ci_re('desc') || agent_board_test.trail_guard() || '\s*,\s*'
83
+ || agent_board_test.col_ref_frag('id')
84
+ || '\s+' || agent_board_test.ci_re('desc') || agent_board_test.trail_guard()),
85
+ 'g'
75
86
  )) as total_order_count
76
87
  from statements
77
- where statement ~* 'order\s+by.*created_at\s+desc'
88
+ where statement ~ agent_board_test.re('\m' || agent_board_test.ci_re('order by') || '\M.*'
89
+ || agent_board_test.col_ref_frag('created_at')
90
+ || '\s+' || agent_board_test.ci_re('desc') || agent_board_test.trail_guard())
78
91
  and (
79
- statement ~* 'from\s+agent_board\.board_memberships'
92
+ statement ~ agent_board_test.stmt_re('from', 'board_memberships', false)
80
93
  or (
81
94
  proname = 'create_tasking_delivery'
82
- and statement ~* 'from\s+agent_board\.tasks'
95
+ and statement ~ agent_board_test.stmt_re('from', 'tasks', false)
83
96
  )
84
97
  or (
85
98
  proname = 'post_board_message'
86
- and statement ~* 'from\s+agent_board\.messages'
99
+ and statement ~ agent_board_test.stmt_re('from', 'messages', false)
87
100
  )
88
101
  );
89
102
 
@@ -493,9 +506,9 @@ select is(
493
506
  true,
494
507
  'control: fixture and structural block really runs as service_role');
495
508
 
496
- -- 2. Catalog-derived inventory: seven membership readers plus the two task/message idempotency
497
- -- readers. request_join_board joined the set in MBCONSENT-B(iii): its Option-3 active-member
498
- -- no-op reads board_memberships with the created_at DESC + id DESC tiebreak like the others.
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.
499
512
  select is(
500
513
  (select array_agg(proname order by proname)
501
514
  from _rowid_function_order_counts),
@@ -504,13 +517,14 @@ select is(
504
517
  '_upsert_board_membership_state',
505
518
  'create_tasking_delivery',
506
519
  'join_board',
520
+ 'list_board_members',
507
521
  'list_board_pending_memberships',
508
522
  'post_board_message',
509
523
  'request_join_board',
510
524
  'resolve_board_request',
511
525
  'update_board_membership'
512
526
  ]::text[],
513
- 'live catalog exposes seven membership readers plus two task/message idempotency readers');
527
+ 'live catalog exposes eight membership readers plus two task/message idempotency readers');
514
528
 
515
529
  -- 3-10. COUNT-EQUALITY, never substring containment.
516
530
  select is(
@@ -564,7 +578,7 @@ select is(
564
578
 
565
579
  -- 11. The live view is catalog-derived too; its last definer uses DROP + CREATE.
566
580
  with d as (
567
- select pg_get_viewdef(c.oid, true) as def
581
+ select agent_board_test.stripped_viewdef(c.oid) as def
568
582
  from pg_class c
569
583
  join pg_namespace n on n.oid = c.relnamespace
570
584
  where n.nspname = 'agent_board'
@@ -572,12 +586,18 @@ with d as (
572
586
  and c.relkind = 'v'
573
587
  )
574
588
  select is(
575
- (select count(*)::int from d, regexp_matches(def, 'created_at\s+desc', 'gi')),
589
+ (select count(*)::int from d, regexp_matches(def,
590
+ agent_board_test.re(agent_board_test.col_ref_frag('created_at')
591
+ || '\s+' || agent_board_test.ci_re('desc') || agent_board_test.trail_guard()),
592
+ 'g')),
576
593
  (select count(*)::int
577
594
  from d, regexp_matches(
578
595
  def,
579
- 'created_at\s+desc\s*,\s*(?:[a-z_][a-z_0-9]*\.)?id\s+desc',
580
- 'gi'
596
+ agent_board_test.re(agent_board_test.col_ref_frag('created_at')
597
+ || '\s+' || agent_board_test.ci_re('desc') || agent_board_test.trail_guard() || '\s*,\s*'
598
+ || agent_board_test.col_ref_frag('id')
599
+ || '\s+' || agent_board_test.ci_re('desc') || agent_board_test.trail_guard()),
600
+ 'g'
581
601
  )),
582
602
  'v_board_directory live catalog definition gives its membership read the id DESC tiebreak');
583
603