@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.
- package/fixtures/already-applied.json +4 -0
- package/migrations/20261003000100_agent_board_room_roster.sql +115 -0
- package/package.json +1 -1
- package/schema-manifest.json +12 -3
- package/src/tooling.test.mjs +4 -2
- package/tests/agent_board_agent_liveness.test.sql +3 -1
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +33 -36
- package/tests/agent_board_authbound_boundary.test.sql +4 -3
- package/tests/agent_board_board_claim_policy.test.sql +17 -6
- package/tests/agent_board_claim_subject_gate.test.sql +7 -8
- package/tests/agent_board_claimkey.test.sql +9 -6
- package/tests/agent_board_claimpolicy_race.test.sql +1466 -342
- package/tests/agent_board_consent_emission.test.sql +5 -4
- package/tests/agent_board_consent_gating.test.sql +20 -9
- package/tests/agent_board_enroll_resumption.test.sql +1 -1
- package/tests/agent_board_enrollment_audit_content.test.sql +5 -4
- package/tests/agent_board_federated_identity_injective_slug.test.sql +3 -1
- package/tests/agent_board_federated_lock_order_class.test.sql +69 -52
- package/tests/agent_board_fedsession.test.sql +3 -1
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -4
- package/tests/agent_board_fedsession_stored_identity_gate.test.sql +18 -6
- package/tests/agent_board_fk_lock_edges.test.sql +64 -5
- package/tests/agent_board_fleet_domain_reclaim.test.sql +2 -4
- package/tests/agent_board_fleet_machine_hijack.test.sql +2 -4
- package/tests/agent_board_fleet_machine_reactivation.test.sql +5 -1
- package/tests/agent_board_fleet_machine_reclaim.test.sql +2 -4
- package/tests/agent_board_invite_no_downgrade.test.sql +24 -18
- package/tests/agent_board_ledger_authority.test.sql +29 -21
- package/tests/agent_board_mint_harden.test.sql +5 -5
- package/tests/agent_board_outbox_wake_ordering.test.sql +4 -3
- package/tests/agent_board_ownerless_renew_quota.test.sql +7 -2
- package/tests/agent_board_portal_offline_invariant.test.sql +5 -4
- package/tests/agent_board_receipt_subject_gate.test.sql +7 -8
- package/tests/agent_board_rolereset.test.sql +5 -3
- package/tests/agent_board_room_roster.test.sql +466 -0
- package/tests/agent_board_rowid.test.sql +39 -19
- package/tests/agent_board_signer_lifecycle.test.sql +24 -13
- package/tests/agent_board_source_strip_helper.test.sql +2 -1
- package/tests/agent_board_task_event_origin_posture.test.sql +21 -9
- package/tests/agent_board_wake_recovery.test.sql +53 -26
- package/tests/agent_board_write_enforcement_class.test.sql +130 -12
- package/tests/catalog/function_execute_privileges.json +1932 -0
- package/tests/catalog/p_metadata_parameters.json +4 -0
- 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
|
|
66
|
+
when statement ~ agent_board_test.stmt_re('from', 'board_memberships', false)
|
|
62
67
|
then 'board_memberships'
|
|
63
|
-
when statement
|
|
68
|
+
when statement ~ agent_board_test.stmt_re('from', 'tasks', false)
|
|
64
69
|
then 'tasks'
|
|
65
|
-
when statement
|
|
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,
|
|
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
|
|
74
|
-
|
|
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
|
|
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
|
|
92
|
+
statement ~ agent_board_test.stmt_re('from', 'board_memberships', false)
|
|
80
93
|
or (
|
|
81
94
|
proname = 'create_tasking_delivery'
|
|
82
|
-
and statement
|
|
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
|
|
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:
|
|
497
|
-
-- readers. request_join_board joined the set in MBCONSENT-B(iii)
|
|
498
|
-
--
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
580
|
-
|
|
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
|
|