@consiliency/agent-board-schema 1.64.0 → 1.68.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.
@@ -0,0 +1,189 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+ begin;
4
+ select plan(41);
5
+ create function pg_temp.a(n integer) returns uuid language sql immutable as $$
6
+ select ('58900000-0000-4000-8000-'||lpad(n::text,12,'0'))::uuid
7
+ $$;
8
+ create function pg_temp.claim(n integer) returns void language plpgsql as $$
9
+ begin perform set_config('request.jwt.claims',json_build_object('role','authenticated','sub',pg_temp.a(n))::text,true); end $$;
10
+ insert into auth.users(id,instance_id,aud,role,email)
11
+ select pg_temp.a(n),'00000000-0000-0000-0000-000000000000','authenticated','authenticated','rls589-'||n||'@test.invalid' from generate_series(1,9)n;
12
+ insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id)
13
+ select pg_temp.a(n),'agent','rls589-'||n,'RLS '||n,'active',pg_temp.a(n) from generate_series(1,9)n;
14
+ insert into agent_board.boards(id,slug,name,created_by_agent_id,visibility) values(pg_temp.a(200),'rls589','RLS',pg_temp.a(1),'private');
15
+ insert into agent_board.board_memberships(id,board_id,agent_id,membership_state,role,created_by_agent_id)
16
+ values(pg_temp.a(201),pg_temp.a(200),pg_temp.a(1),'joined','owner',pg_temp.a(1)),
17
+ (pg_temp.a(202),pg_temp.a(200),pg_temp.a(3),'joined','member',pg_temp.a(3));
18
+ insert into agent_board.threads(id,created_by_agent_id,subject)
19
+ select pg_temp.a(n),pg_temp.a(1),'RLS '||n from generate_series(100,106)n;
20
+ insert into agent_board.messages(id,thread_id,author_agent_id,message_sequence,body)
21
+ values(pg_temp.a(300),pg_temp.a(100),pg_temp.a(2),1,'Board-visible message'),
22
+ (pg_temp.a(301),pg_temp.a(103),pg_temp.a(1),1,'Unrelated message');
23
+ insert into agent_board.tasks(id,thread_id,created_by_agent_id,assigned_agent_id,title,parent_task_id,root_task_id)
24
+ values(pg_temp.a(400),pg_temp.a(101),pg_temp.a(1),pg_temp.a(5),'Assigned task',null,null),
25
+ (pg_temp.a(401),pg_temp.a(102),pg_temp.a(6),pg_temp.a(6),'Parent',null,null),
26
+ (pg_temp.a(402),pg_temp.a(102),pg_temp.a(7),pg_temp.a(7),'Child',pg_temp.a(401),pg_temp.a(401)),
27
+ (pg_temp.a(403),pg_temp.a(103),pg_temp.a(1),pg_temp.a(1),'Unrelated parent',null,null),
28
+ (pg_temp.a(404),pg_temp.a(103),pg_temp.a(1),pg_temp.a(1),'Unrelated child',pg_temp.a(403),pg_temp.a(403)),
29
+ (pg_temp.a(405),pg_temp.a(105),pg_temp.a(1),pg_temp.a(1),'Board-delivered task',null,null);
30
+ insert into agent_board.delegations(id,parent_task_id,child_task_id,delegated_by_agent_id,delegated_to_agent_id)
31
+ values(pg_temp.a(500),pg_temp.a(401),pg_temp.a(402),pg_temp.a(8),pg_temp.a(9)),
32
+ (pg_temp.a(501),pg_temp.a(403),pg_temp.a(404),pg_temp.a(1),pg_temp.a(1));
33
+ insert into agent_board.delivery_recipients(delivery_mode,recipient_agent_id,source_kind,source_message_id,source_task_id,board_id,board_membership_id)
34
+ values('board',pg_temp.a(1),'message',pg_temp.a(300),null,pg_temp.a(200),pg_temp.a(201)),
35
+ ('direct',pg_temp.a(9),'message',pg_temp.a(300),null,null,null),
36
+ ('tasking',pg_temp.a(1),'task',null,pg_temp.a(405),pg_temp.a(200),pg_temp.a(201));
37
+ insert into agent_board.board_authority_ceilings(board_id) values(pg_temp.a(200));
38
+ insert into agent_board.thread_memberships(id,thread_id,agent_id,granted_by_agent_id)
39
+ values(pg_temp.a(600),pg_temp.a(100),pg_temp.a(2),pg_temp.a(1));
40
+ insert into agent_board.read_receipts(thread_id,agent_id) values(pg_temp.a(104),pg_temp.a(3));
41
+ create temp table captured(label text,id uuid);
42
+ grant select,insert on captured to authenticated;
43
+
44
+ -- Compare all policies, deriving FK/key column pairs from the constraint catalog.
45
+ create temp table policy_sites as
46
+ select p.oid,p.polname,c.relname,p.polroles,
47
+ agent_board_test.strip_sql(coalesce(pg_get_expr(p.polqual,p.polrelid),'')||' '||coalesce(pg_get_expr(p.polwithcheck,p.polrelid),'')) as expr
48
+ from pg_policy p join pg_class c on c.oid=p.polrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='agent_board';
49
+ with pairs as (
50
+ select distinct a.attname::text as fk,b.attname::text as key
51
+ from pg_constraint c join pg_class r on r.oid=c.conrelid join pg_namespace n on n.oid=r.relnamespace
52
+ cross join lateral unnest(c.conkey,c.confkey) k(src,dst)
53
+ join pg_attribute a on a.attrelid=c.conrelid and a.attnum=k.src
54
+ join pg_attribute b on b.attrelid=c.confrelid and b.attnum=k.dst
55
+ where c.contype='f' and n.nspname='agent_board'
56
+ ), refs as (
57
+ select s.relname||'.'||s.polname||': '||m[1]||'.'||m[2]||' = '||m[1]||'.'||m[3] as site,trim(both '"' from m[2]) as lhs,trim(both '"' from m[3]) as rhs
58
+ from policy_sites s cross join lateral regexp_matches(s.expr,agent_board_test.re('('||agent_board_test.ident_any_frag()||')\s*\.\s*('||agent_board_test.ident_any_frag()||')\s*=\s*\1\s*\.\s*('||agent_board_test.ident_any_frag()||')'),'g')m
59
+ )
60
+ select is((select coalesce(string_agg(distinct site,', ' order by site),'') from refs where lhs=rhs or exists(select 1 from pairs where (lhs=fk and rhs=key) or (rhs=fk and lhs=key))),'','RLS-1 no policy self-comparison or same-alias foreign-key/key comparison; offenders named');
61
+ create temp table policy_functions as
62
+ select distinct s.relname,s.polname,f.oid,f.oid::regprocedure::text as signature,f.prosecdef
63
+ from policy_sites s join pg_depend d on d.classid='pg_policy'::regclass and d.objid=s.oid and d.refclassid='pg_proc'::regclass
64
+ join pg_proc f on f.oid=d.refobjid join pg_namespace n on n.oid=f.pronamespace
65
+ where n.nspname='agent_board' and exists(select 1 from unnest(s.polroles) r(role_id)
66
+ where case when role_id=0 then true else pg_has_role('authenticated',role_id,'USAGE') end);
67
+ select is((select coalesce(string_agg(relname||'.'||polname||': '||signature,', ' order by relname,polname,signature),'') from policy_functions where not has_function_privilege('authenticated',oid,'EXECUTE')),'','RLS-2 every authenticated policy function is executable; offenders named');
68
+ select cmp_ok((select count(*) from policy_sites),'>',0::bigint,'RLS-3 catalog policy class is nonempty');
69
+ select is((select coalesce(string_agg(distinct signature,', ' order by signature),'') from policy_functions where prosecdef and has_function_privilege('anon',oid,'EXECUTE')),'','RLS-4 authenticated policy definers are not anonymous-callable');
70
+
71
+ select pg_temp.claim(2);
72
+ set local role authenticated;
73
+ select lives_ok($$insert into captured select 'message-author',id from agent_board.threads where id in (pg_temp.a(100),pg_temp.a(103))$$,'RLS-5 message-author SELECT executes');
74
+ reset role;
75
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='message-author'),array[pg_temp.a(100)]::uuid[],'RLS-6 message-author visible rows are scoped');
76
+
77
+ select pg_temp.claim(3);
78
+ set local role authenticated;
79
+ select lives_ok($$insert into captured select 'board-member',id from agent_board.threads where id in (pg_temp.a(100),pg_temp.a(103))$$,'RLS-7 board-member SELECT executes');
80
+ reset role;
81
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='board-member'),array[pg_temp.a(100)]::uuid[],'RLS-8 board-member visible rows are scoped');
82
+
83
+ select pg_temp.claim(5);
84
+ set local role authenticated;
85
+ select lives_ok($$insert into captured select 'task-assignee',id from agent_board.threads where id in (pg_temp.a(101),pg_temp.a(103))$$,'RLS-9 task-assignee SELECT executes');
86
+ reset role;
87
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='task-assignee'),array[pg_temp.a(101)]::uuid[],'RLS-10 task-assignee visible rows are scoped');
88
+
89
+ select pg_temp.claim(6);
90
+ set local role authenticated;
91
+ select lives_ok($$insert into captured select 'parent-task-owner',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-11 parent-task-owner SELECT executes');
92
+ reset role;
93
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='parent-task-owner'),array[pg_temp.a(500)]::uuid[],'RLS-12 parent-task-owner visible rows are scoped');
94
+
95
+ select pg_temp.claim(7);
96
+ set local role authenticated;
97
+ select lives_ok($$insert into captured select 'child-task-owner',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-13 child-task-owner SELECT executes');
98
+ reset role;
99
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='child-task-owner'),array[pg_temp.a(500)]::uuid[],'RLS-14 child-task-owner visible rows are scoped');
100
+
101
+ select pg_temp.claim(8);
102
+ set local role authenticated;
103
+ select lives_ok($$insert into captured select 'delegator',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-15 delegator SELECT executes');
104
+ reset role;
105
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='delegator'),array[pg_temp.a(500)]::uuid[],'RLS-16 delegator visible rows are scoped');
106
+
107
+ select pg_temp.claim(9);
108
+ set local role authenticated;
109
+ select lives_ok($$insert into captured select 'delegate-recipient',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-17 delegate-recipient SELECT executes');
110
+ reset role;
111
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='delegate-recipient'),array[pg_temp.a(500)]::uuid[],'RLS-18 delegate-recipient visible rows are scoped');
112
+
113
+ select pg_temp.claim(1);
114
+ set local role authenticated;
115
+ select lives_ok($$insert into captured select 'thread-creator',id from agent_board.threads where id=pg_temp.a(106)$$,'RLS-19 thread-creator SELECT executes');
116
+ reset role;
117
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='thread-creator'),array[pg_temp.a(106)]::uuid[],'RLS-20 thread-creator visible rows are scoped');
118
+
119
+ select pg_temp.claim(4);
120
+ set local role authenticated;
121
+ select lives_ok($$insert into captured select 'outsider-threads',id from agent_board.threads where id between pg_temp.a(100) and pg_temp.a(105)$$,'RLS-21 outsider-threads SELECT executes');
122
+ reset role;
123
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-threads'),array[]::uuid[],'RLS-22 outsider-threads visible rows are scoped');
124
+
125
+ select pg_temp.claim(4);
126
+ set local role authenticated;
127
+ select lives_ok($$insert into captured select 'outsider-delegations',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-23 outsider-delegations SELECT executes');
128
+ reset role;
129
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-delegations'),array[]::uuid[],'RLS-24 outsider-delegations visible rows are scoped');
130
+
131
+ select pg_temp.claim(3);
132
+ set local role authenticated;
133
+ select lives_ok($$insert into captured select 'member-ceiling',board_id from agent_board.board_authority_ceilings where board_id=pg_temp.a(200)$$,'RLS-25 member-ceiling SELECT executes');
134
+ reset role;
135
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='member-ceiling'),array[pg_temp.a(200)]::uuid[],'RLS-26 member-ceiling visible rows are scoped');
136
+
137
+ select pg_temp.claim(4);
138
+ set local role authenticated;
139
+ select lives_ok($$insert into captured select 'outsider-ceiling',board_id from agent_board.board_authority_ceilings where board_id=pg_temp.a(200)$$,'RLS-27 outsider-ceiling SELECT executes');
140
+ reset role;
141
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-ceiling'),array[]::uuid[],'RLS-28 outsider-ceiling visible rows are scoped');
142
+
143
+ select pg_temp.claim(3);
144
+ set local role authenticated;
145
+ select lives_ok($$insert into captured select 'member-thread-acl',id from agent_board.thread_memberships where id=pg_temp.a(600)$$,'RLS-29 member-thread-acl SELECT executes');
146
+ reset role;
147
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='member-thread-acl'),array[pg_temp.a(600)]::uuid[],'RLS-30 member-thread-acl visible rows are scoped');
148
+
149
+ select pg_temp.claim(4);
150
+ set local role authenticated;
151
+ select lives_ok($$insert into captured select 'outsider-thread-acl',id from agent_board.thread_memberships where id=pg_temp.a(600)$$,'RLS-31 outsider-thread-acl SELECT executes');
152
+ reset role;
153
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='outsider-thread-acl'),array[]::uuid[],'RLS-32 outsider-thread-acl visible rows are scoped');
154
+
155
+ select pg_temp.claim(3);
156
+ set local role authenticated;
157
+ select lives_ok($$insert into captured select 'receipt-reader',id from agent_board.threads where id=pg_temp.a(104)$$,'RLS-33 receipt-reader SELECT executes');
158
+ reset role;
159
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='receipt-reader'),array[pg_temp.a(104)]::uuid[],'RLS-34 receipt-reader visible rows are scoped');
160
+
161
+ select pg_temp.claim(3);
162
+ set local role authenticated;
163
+ select lives_ok($$insert into captured select 'member-unrelated-delegation',id from agent_board.delegations where id in (pg_temp.a(500),pg_temp.a(501))$$,'RLS-35 member-unrelated-delegation SELECT executes');
164
+ reset role;
165
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='member-unrelated-delegation'),array[]::uuid[],'RLS-36 member-unrelated-delegation visible rows are scoped');
166
+
167
+ select pg_temp.claim(3);
168
+ set local role authenticated;
169
+ select lives_ok($$insert into captured select 'board-task-delivery',id from agent_board.threads where id in (pg_temp.a(105),pg_temp.a(103))$$,'RLS-37 board-task-delivery SELECT executes');
170
+ reset role;
171
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='board-task-delivery'),array[pg_temp.a(105)]::uuid[],'RLS-38 board-task-delivery visible rows are scoped');
172
+
173
+ select pg_temp.claim(9);
174
+ set local role authenticated;
175
+ select lives_ok($$insert into captured select 'direct-message-recipient',id from agent_board.threads where id in (pg_temp.a(100),pg_temp.a(103))$$,'RLS-39 direct-message-recipient SELECT executes');
176
+ reset role;
177
+ select is((select coalesce(array_agg(id order by id),array[]::uuid[]) from captured where label='direct-message-recipient'),array[pg_temp.a(100)]::uuid[],'RLS-40 direct-message-recipient visible rows are scoped');
178
+
179
+
180
+ -- Catalog provenance is part of the read contract: grants must not erase the ruling.
181
+ select ok(coalesce(
182
+ obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%message-board#291%'
183
+ and obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%REVERSING A RULING%'
184
+ and obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%restricted%is UNREACHABLE%'
185
+ and obj_description('agent_board._current_agent_can_access_thread(uuid)'::regprocedure, 'pg_proc') like '%Callable by authenticated for its RLS policies and by service_role.%',
186
+ false), 'RLS-41 thread access comment preserves the ratified claims ruling and dormant ACL');
187
+
188
+ select * from finish();
189
+ rollback;
@@ -0,0 +1,96 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+ begin;
4
+ select plan(33);
5
+ create function pg_temp.a(n integer) returns uuid language sql immutable as $$
6
+ select ('68000000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid
7
+ $$;
8
+ create function pg_temp.claim(n integer) returns void language plpgsql as $$
9
+ begin
10
+ perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',pg_temp.a(n))::text,true);
11
+ end $$;
12
+ insert into auth.users(id, instance_id, aud, role, email)
13
+ select pg_temp.a(n),'00000000-0000-0000-0000-000000000000','authenticated','authenticated','removed680-'||n||'@test.invalid'
14
+ from generate_series(1,9) n;
15
+ insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id)
16
+ select pg_temp.a(n),'agent','removed680-'||n,'Removed '||n,'active',pg_temp.a(n)
17
+ from generate_series(1,9) n;
18
+ insert into agent_board.boards(id,slug,name,created_by_agent_id,visibility)
19
+ values (pg_temp.a(100),'removed680-open','Open',pg_temp.a(1),'open'),
20
+ (pg_temp.a(101),'removed680-link','Link',pg_temp.a(1),'invite_only'),
21
+ (pg_temp.a(102),'removed680-private','Private',pg_temp.a(1),'private');
22
+ insert into agent_board.board_memberships(board_id,agent_id,membership_state,role,created_by_agent_id)
23
+ select pg_temp.a(n),pg_temp.a(1),'joined','owner',pg_temp.a(1) from generate_series(100,102) n;
24
+ insert into agent_board.board_memberships(board_id,agent_id,membership_state,role,created_by_agent_id,left_at,metadata)
25
+ values (pg_temp.a(100),pg_temp.a(2),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
26
+ (pg_temp.a(101),pg_temp.a(2),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
27
+ (pg_temp.a(100),pg_temp.a(3),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
28
+ (pg_temp.a(100),pg_temp.a(3),'left','member',pg_temp.a(1),now(),'{"left":true}'),
29
+ (pg_temp.a(100),pg_temp.a(5),'left','member',pg_temp.a(5),now(),'{}'),
30
+ (pg_temp.a(100),pg_temp.a(6),'requested','member',pg_temp.a(6),null,'{}'),
31
+ (pg_temp.a(100),pg_temp.a(7),'removed','member',pg_temp.a(1),now(),'{"eviction":true}'),
32
+ (pg_temp.a(100),pg_temp.a(7),'joined','member',pg_temp.a(7),null,'{"active":true}'),
33
+ (pg_temp.a(100),pg_temp.a(8),'subscribed','member',pg_temp.a(8),null,'{"active":true}');
34
+ update agent_board.board_memberships set created_at=now()-interval '1 day'
35
+ where board_id=pg_temp.a(100) and agent_id=pg_temp.a(3) and membership_state='removed';
36
+ select pg_temp.claim(1);
37
+ create temp table live_link as select * from agent_board.create_invite_link(pg_temp.a(101),'member','joined',null,null,pg_temp.a(1));
38
+
39
+ create temp table request_sites as
40
+ select p.oid,p.proname::text as name,agent_board_test.stripped_body(p.oid) as body,
41
+ agent_board_test.stripped_body(p.oid,true) as literals
42
+ from pg_proc p join pg_namespace n on n.oid=p.pronamespace
43
+ where n.nspname='agent_board' and has_function_privilege('authenticated',p.oid,'EXECUTE')
44
+ and agent_board_test.stripped_body(p.oid,true) ~ agent_board_test.re(
45
+ agent_board_test.call_frag('_upsert_board_membership_state') || '[^,;]*,[^,;]*,\s*''requested''\s*,');
46
+ select is((select string_agg(name,',' order by name) from request_sites),'request_join_board','REM-1 literal-request sink callers enumerated from catalog');
47
+ select is((select coalesce(string_agg(name,','),'') from request_sites where body !~ agent_board_test.row_lock_re('board_memberships')),'','REM-2 every request writer locks membership rows; offenders named');
48
+ select is((select coalesce(string_agg(name,','),'') from request_sites
49
+ where regexp_instr(body,agent_board_test.row_lock_re('board_memberships'))=0
50
+ or regexp_instr(body,agent_board_test.row_lock_re('board_memberships')) >= regexp_instr(body,agent_board_test.call_re('_upsert_board_membership_state'))
51
+ or regexp_instr(body,agent_board_test.row_lock_re('board_memberships')) >= regexp_instr(body,agent_board_test.re(agent_board_test.ci_re('if') || '\s+' || agent_board_test.ci_re('exists') || '\s*\('))
52
+ or literals !~ 'board join request refused for removed standing'),
53
+ '','REM-3 every request writer checks removed standing before its sink');
54
+ select is((select coalesce(string_agg(name,','),'') from request_sites where has_function_privilege('anon',oid,'EXECUTE') or not has_function_privilege('service_role',oid,'EXECUTE') or obj_description(oid,'pg_proc') is null),'','REM-4 request writer ACL and comment');
55
+
56
+ select pg_temp.claim(2);
57
+ select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(2),'{"overwrite":true}')$$,'42501',null,'REM-5 removed open-board request refused');
58
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),'removed','REM-6 eviction persists after request');
59
+ select is((select count(*)::integer from agent_board.board_membership_events where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),0,'REM-7 refused request emits no membership event');
60
+ select throws_ok($$select agent_board.join_board(pg_temp.a(100),pg_temp.a(2))$$,'P0002',null,'REM-8 request cannot unlock open self-join');
61
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),'removed','REM-9 self-join leaves eviction intact');
62
+ select throws_ok($$select agent_board.request_join_board(pg_temp.a(101),pg_temp.a(2))$$,'42501',null,'REM-10 removed invite-only request refused');
63
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(2)),'removed','REM-11 invite-only eviction persists');
64
+ select throws_ok($$select agent_board.redeem_invite_link((select code from live_link))$$,'42501',null,'REM-12 request cannot unlock live-link redemption');
65
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(2)),'removed','REM-13 live link leaves eviction intact');
66
+ select is((select uses_count from agent_board.board_invite_tokens where id=(select invite_id from live_link)),0,'REM-14 refused redemption consumes no use');
67
+
68
+ select pg_temp.claim(3);
69
+ select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(3),'{"overwrite":true}')$$,'42501',null,'REM-15 removed plus left standing refused');
70
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(3)),'left,removed','REM-16 whole row set preserved');
71
+ select is((select metadata from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(3) and membership_state='removed'),'{"eviction":true}'::jsonb,'REM-17 denied request preserves eviction metadata');
72
+ select pg_temp.claim(4);
73
+ select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(4))$$,'REM-18 first request succeeds');
74
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(4)),'requested','REM-19 first request stored');
75
+ select pg_temp.claim(5);
76
+ select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(5))$$,'REM-20 voluntary leaver can request');
77
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(5)),'requested','REM-21 voluntary request stored');
78
+ select pg_temp.claim(6);
79
+ select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(6))$$,'REM-22 repeated pending request succeeds');
80
+ select is((select count(*)::integer from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(6) and membership_state='requested'),1,'REM-23 repeat preserves one pending row');
81
+ select pg_temp.claim(7);
82
+ select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(7),'{"overwrite":true}')$$,'REM-24 active plus removed remains a no-op');
83
+ select is((select metadata from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(7) and membership_state='joined'),'{"active":true}'::jsonb,'REM-25 active row remains unchanged');
84
+ select pg_temp.claim(8);
85
+ select lives_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(8))$$,'REM-26 subscribed request remains a no-op');
86
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(8)),'subscribed','REM-27 subscription preserved');
87
+ select set_config('request.jwt.claims','{"role":"service_role"}',true);
88
+ select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(2))$$,'42501',null,'REM-28 service request cannot erase removal');
89
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(2)),'removed','REM-29 service refusal preserves eviction');
90
+ select pg_temp.claim(9);
91
+ select throws_ok($$select agent_board.request_join_board(pg_temp.a(102),pg_temp.a(9))$$,'P0002',null,'REM-30 private board request still hidden');
92
+ select is((select count(*)::integer from agent_board.board_memberships where board_id=pg_temp.a(102) and agent_id=pg_temp.a(9)),0,'REM-31 hidden request writes nothing');
93
+ select throws_ok($$select agent_board.request_join_board(pg_temp.a(100),pg_temp.a(4))$$,'42501',null,'REM-32 request remains self-only');
94
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(4)),'requested','REM-33 actor mismatch preserves target');
95
+ select * from finish();
96
+ rollback;
@@ -0,0 +1,190 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+ begin;
4
+ select plan(76);
5
+ create function pg_temp.a(n integer) returns uuid language sql immutable as $$
6
+ select ('66800000-0000-4000-8000-'||lpad(n::text,12,'0'))::uuid
7
+ $$;
8
+ create function pg_temp.claim(n integer) returns void language plpgsql as $$
9
+ begin perform set_config('request.jwt.claims',json_build_object('role','authenticated','sub',pg_temp.a(n))::text,true); end $$;
10
+ insert into auth.users(id,instance_id,aud,role,email)
11
+ select pg_temp.a(n),'00000000-0000-0000-0000-000000000000','authenticated','authenticated','resolve668-'||n||'@test.invalid' from generate_series(1,39)n;
12
+ insert into agent_board.agents(id,agent_type,handle,display_name,status,auth_user_id)
13
+ select pg_temp.a(n),'agent','resolve668-'||n,'Resolver '||n,'active',pg_temp.a(n) from generate_series(1,39)n;
14
+ insert into agent_board.boards(id,slug,name,created_by_agent_id,visibility,role_gate_enforced)
15
+ values(pg_temp.a(100),'resolve668-off','Gate off',pg_temp.a(1),'open',false),
16
+ (pg_temp.a(101),'resolve668-on','Gate on',pg_temp.a(1),'open',true),
17
+ (pg_temp.a(102),'resolve668-other','Other board',pg_temp.a(6),'private',false);
18
+ insert into agent_board.board_memberships(board_id,agent_id,membership_state,role,created_by_agent_id,left_at)
19
+ select pg_temp.a(b),pg_temp.a(a),state,role,pg_temp.a(1),case when state in ('left','removed') then now() else null end
20
+ from generate_series(100,101)b cross join (values
21
+ (1,'joined','observer'),(2,'joined','owner'),(3,'subscribed','admin'),
22
+ (4,'joined','member'),(5,'joined','observer'),(7,'left','owner'),(8,'removed','admin')
23
+ )r(a,state,role);
24
+ insert into agent_board.board_memberships(board_id,agent_id,membership_state,role,created_by_agent_id)
25
+ values(pg_temp.a(102),pg_temp.a(6),'joined','owner',pg_temp.a(6));
26
+ -- Real subject-authored requests establish consent for approval-side probes.
27
+ do $$declare b integer;a integer; begin
28
+ for b in 100..101 loop for a in 21..39 loop
29
+ perform pg_temp.claim(a);
30
+ perform agent_board.request_join_board(pg_temp.a(b),pg_temp.a(a));
31
+ end loop; end loop;
32
+ end $$;
33
+ create temp table resolution_sites as
34
+ select p.oid,p.proname::text as name,agent_board_test.stripped_body(p.oid) as body,agent_board_test.stripped_body(p.oid,true) as literals
35
+ from pg_proc p join pg_namespace n on n.oid=p.pronamespace
36
+ where n.nspname='agent_board' and has_function_privilege('authenticated',p.oid,'EXECUTE')
37
+ and 'p_membership_state'=any(p.proargnames)
38
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_upsert_board_membership_state')
39
+ and agent_board_test.stripped_body(p.oid,true) ~ agent_board_test.re(agent_board_test.col_ref_frag('membership_state')||'\s+'||agent_board_test.ci_re('in')||'\s*\(\s*(?:''requested''\s*,\s*''invited''|''invited''\s*,\s*''requested'')\s*\)');
40
+ select is((select string_agg(name,',' order by name) from resolution_sites),'resolve_board_request','AUTH-1 catalog-derived pending resolver population');
41
+ select is((select coalesce(string_agg(name,','),'') from resolution_sites where body ~ agent_board_test.ident_re('role_gate_enforced')),'','AUTH-2 pending resolution never depends on the role gate; offenders named');
42
+ select is((select coalesce(string_agg(name,','),'') from resolution_sites where literals !~ agent_board_test.re(agent_board_test.col_ref_frag('role')||'\s+'||agent_board_test.ci_re('in')||'\s*\(\s*''owner''\s*,\s*''admin''\s*\)')),'','AUTH-3 every resolver requires owner/admin standing');
43
+ select is((select coalesce(string_agg(name,','),'') from resolution_sites where has_function_privilege('anon',oid,'EXECUTE') or not has_function_privilege('service_role',oid,'EXECUTE') or obj_description(oid,'pg_proc') is null),'','AUTH-4 resolver ACL and comment');
44
+
45
+ select pg_temp.claim(2);
46
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(21),'joined',pg_temp.a(2))$$,'AUTH-5 gate off: active owner resolves joined');
47
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(21)),'joined','AUTH-6 gate off: allowed result stored');
48
+
49
+ select pg_temp.claim(2);
50
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(22),'removed',pg_temp.a(2))$$,'AUTH-7 gate off: active owner resolves removed');
51
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(22)),'left','AUTH-8 gate off: allowed result stored');
52
+
53
+ select pg_temp.claim(3);
54
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(23),'subscribed',pg_temp.a(3))$$,'AUTH-9 gate off: active admin resolves subscribed');
55
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(23)),'subscribed','AUTH-10 gate off: allowed result stored');
56
+
57
+ select pg_temp.claim(3);
58
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(24),'removed',pg_temp.a(3))$$,'AUTH-11 gate off: active admin resolves removed');
59
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(24)),'left','AUTH-12 gate off: allowed result stored');
60
+
61
+ select pg_temp.claim(4);
62
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(25),'joined',pg_temp.a(4))$$,'42501',null,'AUTH-13 gate off: member cannot resolve joined');
63
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(25)),'requested','AUTH-14 gate off: denied request unchanged');
64
+
65
+ select pg_temp.claim(4);
66
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(26),'removed',pg_temp.a(4))$$,'42501',null,'AUTH-15 gate off: member cannot resolve removed');
67
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(26)),'requested','AUTH-16 gate off: denied request unchanged');
68
+
69
+ select pg_temp.claim(5);
70
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(27),'joined',pg_temp.a(5))$$,'42501',null,'AUTH-17 gate off: observer cannot resolve joined');
71
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(27)),'requested','AUTH-18 gate off: denied request unchanged');
72
+
73
+ select pg_temp.claim(5);
74
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(28),'removed',pg_temp.a(5))$$,'42501',null,'AUTH-19 gate off: observer cannot resolve removed');
75
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(28)),'requested','AUTH-20 gate off: denied request unchanged');
76
+
77
+ select pg_temp.claim(1);
78
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(29),'joined',pg_temp.a(1))$$,'42501',null,'AUTH-21 gate off: demoted creator cannot resolve joined');
79
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(29)),'requested','AUTH-22 gate off: denied request unchanged');
80
+
81
+ select pg_temp.claim(1);
82
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(30),'removed',pg_temp.a(1))$$,'42501',null,'AUTH-23 gate off: demoted creator cannot resolve removed');
83
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(30)),'requested','AUTH-24 gate off: denied request unchanged');
84
+
85
+ select pg_temp.claim(6);
86
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(31),'joined',pg_temp.a(6))$$,'42501',null,'AUTH-25 gate off: outsider cannot resolve joined');
87
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(31)),'requested','AUTH-26 gate off: denied request unchanged');
88
+
89
+ select pg_temp.claim(6);
90
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(32),'removed',pg_temp.a(6))$$,'42501',null,'AUTH-27 gate off: outsider cannot resolve removed');
91
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(32)),'requested','AUTH-28 gate off: denied request unchanged');
92
+
93
+ select pg_temp.claim(7);
94
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(33),'joined',pg_temp.a(7))$$,'42501',null,'AUTH-29 gate off: departed owner cannot resolve joined');
95
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(33)),'requested','AUTH-30 gate off: denied request unchanged');
96
+
97
+ select pg_temp.claim(7);
98
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(34),'removed',pg_temp.a(7))$$,'42501',null,'AUTH-31 gate off: departed owner cannot resolve removed');
99
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(34)),'requested','AUTH-32 gate off: denied request unchanged');
100
+
101
+ select pg_temp.claim(8);
102
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(35),'joined',pg_temp.a(8))$$,'42501',null,'AUTH-33 gate off: removed admin cannot resolve joined');
103
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(35)),'requested','AUTH-34 gate off: denied request unchanged');
104
+
105
+ select pg_temp.claim(8);
106
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(36),'removed',pg_temp.a(8))$$,'42501',null,'AUTH-35 gate off: removed admin cannot resolve removed');
107
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(36)),'requested','AUTH-36 gate off: denied request unchanged');
108
+
109
+ select pg_temp.claim(2);
110
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(21),'joined',pg_temp.a(2))$$,'AUTH-37 gate on: active owner resolves joined');
111
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(21)),'joined','AUTH-38 gate on: allowed result stored');
112
+
113
+ select pg_temp.claim(2);
114
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(22),'removed',pg_temp.a(2))$$,'AUTH-39 gate on: active owner resolves removed');
115
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(22)),'left','AUTH-40 gate on: allowed result stored');
116
+
117
+ select pg_temp.claim(3);
118
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(23),'subscribed',pg_temp.a(3))$$,'AUTH-41 gate on: active admin resolves subscribed');
119
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(23)),'subscribed','AUTH-42 gate on: allowed result stored');
120
+
121
+ select pg_temp.claim(3);
122
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(24),'removed',pg_temp.a(3))$$,'AUTH-43 gate on: active admin resolves removed');
123
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(24)),'left','AUTH-44 gate on: allowed result stored');
124
+
125
+ select pg_temp.claim(4);
126
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(25),'joined',pg_temp.a(4))$$,'42501',null,'AUTH-45 gate on: member cannot resolve joined');
127
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(25)),'requested','AUTH-46 gate on: denied request unchanged');
128
+
129
+ select pg_temp.claim(4);
130
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(26),'removed',pg_temp.a(4))$$,'42501',null,'AUTH-47 gate on: member cannot resolve removed');
131
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(26)),'requested','AUTH-48 gate on: denied request unchanged');
132
+
133
+ select pg_temp.claim(5);
134
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(27),'joined',pg_temp.a(5))$$,'42501',null,'AUTH-49 gate on: observer cannot resolve joined');
135
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(27)),'requested','AUTH-50 gate on: denied request unchanged');
136
+
137
+ select pg_temp.claim(5);
138
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(28),'removed',pg_temp.a(5))$$,'42501',null,'AUTH-51 gate on: observer cannot resolve removed');
139
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(28)),'requested','AUTH-52 gate on: denied request unchanged');
140
+
141
+ select pg_temp.claim(1);
142
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(29),'joined',pg_temp.a(1))$$,'42501',null,'AUTH-53 gate on: demoted creator cannot resolve joined');
143
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(29)),'requested','AUTH-54 gate on: denied request unchanged');
144
+
145
+ select pg_temp.claim(1);
146
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(30),'removed',pg_temp.a(1))$$,'42501',null,'AUTH-55 gate on: demoted creator cannot resolve removed');
147
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(30)),'requested','AUTH-56 gate on: denied request unchanged');
148
+
149
+ select pg_temp.claim(6);
150
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(31),'joined',pg_temp.a(6))$$,'42501',null,'AUTH-57 gate on: outsider cannot resolve joined');
151
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(31)),'requested','AUTH-58 gate on: denied request unchanged');
152
+
153
+ select pg_temp.claim(6);
154
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(32),'removed',pg_temp.a(6))$$,'42501',null,'AUTH-59 gate on: outsider cannot resolve removed');
155
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(32)),'requested','AUTH-60 gate on: denied request unchanged');
156
+
157
+ select pg_temp.claim(7);
158
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(33),'joined',pg_temp.a(7))$$,'42501',null,'AUTH-61 gate on: departed owner cannot resolve joined');
159
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(33)),'requested','AUTH-62 gate on: denied request unchanged');
160
+
161
+ select pg_temp.claim(7);
162
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(34),'removed',pg_temp.a(7))$$,'42501',null,'AUTH-63 gate on: departed owner cannot resolve removed');
163
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(34)),'requested','AUTH-64 gate on: denied request unchanged');
164
+
165
+ select pg_temp.claim(8);
166
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(35),'joined',pg_temp.a(8))$$,'42501',null,'AUTH-65 gate on: removed admin cannot resolve joined');
167
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(35)),'requested','AUTH-66 gate on: denied request unchanged');
168
+
169
+ select pg_temp.claim(8);
170
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(36),'removed',pg_temp.a(8))$$,'42501',null,'AUTH-67 gate on: removed admin cannot resolve removed');
171
+ select is((select string_agg(membership_state,',' order by membership_state) from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(36)),'requested','AUTH-68 gate on: denied request unchanged');
172
+
173
+ select set_config('request.jwt.claims','{"role":"service_role"}',true);
174
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(37),'removed',pg_temp.a(2))$$,'42501',null,'AUTH-69 unresolved service resolver has no owner identity');
175
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(37)),'requested','AUTH-70 unresolved service refusal preserves request');
176
+
177
+ select set_config('request.jwt.claims','{"role":"service_role"}',true);
178
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(101),pg_temp.a(37),'removed',pg_temp.a(2))$$,'42501',null,'AUTH-71 unresolved service resolver has no owner identity');
179
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(101) and agent_id=pg_temp.a(37)),'requested','AUTH-72 unresolved service refusal preserves request');
180
+
181
+ select set_config('request.jwt.claims',json_build_object('role','service_role','sub',pg_temp.a(2))::text,true);
182
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(38),'joined',pg_temp.a(2))$$,'AUTH-73 resolved service owner may approve');
183
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(38)),'joined','AUTH-74 service owner approval stored');
184
+
185
+ select set_config('request.jwt.claims','{"role":"authenticated"}',true);
186
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100),pg_temp.a(39),'removed',pg_temp.a(2))$$,'42501',null,'AUTH-75 unresolved authenticated resolver refused');
187
+ select is((select membership_state from agent_board.board_memberships where board_id=pg_temp.a(100) and agent_id=pg_temp.a(39)),'requested','AUTH-76 unresolved authenticated refusal preserves request');
188
+
189
+ select * from finish();
190
+ rollback;
@@ -0,0 +1,106 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+ begin;
4
+ select plan(32);
5
+
6
+ create function pg_temp.a(n integer) returns uuid language sql immutable as $$
7
+ select ('66500000-0000-4000-8000-' || lpad(n::text, 12, '0'))::uuid
8
+ $$;
9
+ create function pg_temp.claim(n integer) returns void language plpgsql as $$
10
+ begin
11
+ perform set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub', pg_temp.a(n))::text, true);
12
+ end
13
+ $$;
14
+ insert into auth.users(id, instance_id, aud, role, email)
15
+ select pg_temp.a(n), '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'prov665-' || n || '@test.invalid'
16
+ from generate_series(1, 15) n;
17
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id)
18
+ select pg_temp.a(n), 'agent', 'prov665-' || n, 'Provenance ' || n, 'active', pg_temp.a(n)
19
+ from generate_series(1, 15) n;
20
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility)
21
+ values (pg_temp.a(100), 'prov665', 'Provenance', pg_temp.a(1), 'open');
22
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id)
23
+ values (pg_temp.a(100), pg_temp.a(1), 'joined', 'owner', pg_temp.a(1)),
24
+ (pg_temp.a(100), pg_temp.a(5), 'requested', 'member', pg_temp.a(5)),
25
+ (pg_temp.a(100), pg_temp.a(6), 'requested', 'member', pg_temp.a(6)),
26
+ (pg_temp.a(100), pg_temp.a(6), 'joined', 'member', pg_temp.a(6)),
27
+ (pg_temp.a(100), pg_temp.a(10), 'requested', 'member', pg_temp.a(10)),
28
+ (pg_temp.a(100), pg_temp.a(15), 'requested', 'member', pg_temp.a(15)),
29
+ (pg_temp.a(100), pg_temp.a(15), 'joined', 'member', pg_temp.a(15));
30
+ -- Subject-authored requests establish the consent an approval needs (#82).
31
+ do $$ declare a integer; begin
32
+ for a in 13..14 loop
33
+ perform pg_temp.claim(a);
34
+ perform agent_board.request_join_board(pg_temp.a(100), pg_temp.a(a));
35
+ end loop;
36
+ end $$;
37
+ create temp table active_before as
38
+ select id, created_by_agent_id from agent_board.board_memberships
39
+ where board_id = pg_temp.a(100) and agent_id = pg_temp.a(15) and membership_state = 'joined';
40
+
41
+ -- The population is catalog-derived. No exemption list or hard-coded RPC scan.
42
+ create temp table provenance_sites as
43
+ select p.oid, p.oid::regprocedure::text as signature,
44
+ agent_board_test.stripped_body(p.oid) as body
45
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
46
+ where n.nspname = 'agent_board'
47
+ and 'p_created_by_agent_id' = any(p.proargnames)
48
+ and has_function_privilege('authenticated', p.oid, 'EXECUTE')
49
+ and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('agent_board._upsert_board_membership_state');
50
+ select is((select string_agg(p.proname::text, ',' order by p.proname) from provenance_sites s join pg_proc p on p.oid = s.oid),
51
+ 'invite_board_member,join_board,resolve_board_request', 'PROV-1 membership provenance class population');
52
+ select is((select coalesce(string_agg(signature, ', '), '') from provenance_sites where body ~ agent_board_test.ident_re('p_created_by_agent_id')),
53
+ '', 'PROV-2 no membership RPC consumes caller-asserted provenance; offenders named');
54
+ select is((select coalesce(string_agg(signature, ', '), '') from provenance_sites
55
+ where has_function_privilege('anon', oid, 'EXECUTE') or not has_function_privilege('service_role', oid, 'EXECUTE')),
56
+ '', 'PROV-3 membership provenance class has restricted ACLs');
57
+ select is((select coalesce(string_agg(signature, ', '), '') from provenance_sites where obj_description(oid, 'pg_proc') is null),
58
+ '', 'PROV-4 membership provenance class documents attribution');
59
+
60
+ select pg_temp.claim(1);
61
+ select lives_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(3), pg_temp.a(2))$$, 'PROV-5 invite accepts compatibility attribution');
62
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(3)), pg_temp.a(1), 'PROV-6 invite attribution is session actor');
63
+ select lives_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(4), null)$$, 'PROV-7 invite accepts NULL attribution');
64
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(4)), pg_temp.a(1), 'PROV-8 NULL invite attribution is session actor');
65
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(5), 'removed', pg_temp.a(2))$$, 'PROV-9 resolution accepts compatibility attribution');
66
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(5) and membership_state = 'left'), pg_temp.a(1), 'PROV-10 resolution attribution is session actor');
67
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(6), 'joined', pg_temp.a(2))$$, 'PROV-11 redundant pending retirement succeeds');
68
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(6) and membership_state = 'left'), pg_temp.a(1), 'PROV-12 retirement attribution is session actor');
69
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(6) and membership_state = 'joined'), pg_temp.a(6), 'PROV-13 no-op preserves active row attribution');
70
+
71
+ select pg_temp.claim(7);
72
+ select lives_ok($$select agent_board.join_board(pg_temp.a(100), pg_temp.a(7), 'joined', pg_temp.a(2))$$, 'PROV-14 self-join accepts compatibility attribution');
73
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(7)), pg_temp.a(7), 'PROV-15 join attribution is session actor');
74
+ select pg_temp.claim(8);
75
+ select lives_ok($$select agent_board.join_board(pg_temp.a(100), pg_temp.a(8), 'joined', null)$$, 'PROV-16 self-join accepts NULL attribution');
76
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(8)), pg_temp.a(8), 'PROV-17 NULL join attribution is session actor');
77
+
78
+ select set_config('request.jwt.claims', '{"role":"service_role"}', true);
79
+ select lives_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(9), pg_temp.a(2))$$, 'PROV-18 unresolved service invitation succeeds');
80
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(9)), null::uuid, 'PROV-19 service invitation does not invent agent attribution');
81
+ -- #668 (owner-confirmed 2026-10-26): resolution requires a RESOLVED same-board owner/admin, so an
82
+ -- unresolved service-role session is refused and the pending row keeps its state and provenance.
83
+ select throws_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(10), 'removed', pg_temp.a(2))$$, '42501', null, 'PROV-20 unresolved service resolution refused');
84
+ select is((select string_agg(membership_state || ':' || coalesce(created_by_agent_id::text, 'null'), ',') from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(10)),
85
+ 'requested:' || pg_temp.a(10)::text, 'PROV-21 refused service resolution leaves the pending row and its provenance unchanged');
86
+ select lives_ok($$select agent_board.join_board(pg_temp.a(100), pg_temp.a(11), 'joined', pg_temp.a(2))$$, 'PROV-22 unresolved service join succeeds');
87
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(11)), null::uuid, 'PROV-23 service join does not invent agent attribution');
88
+ select set_config('request.jwt.claims', '{"role":"authenticated"}', true);
89
+ select throws_ok($$select agent_board.invite_board_member(pg_temp.a(100), pg_temp.a(12), pg_temp.a(1))$$, '42501', null, 'PROV-24 unresolved authenticated invite denied');
90
+ select is((select count(*)::integer from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(12)), 0, 'PROV-25 denied invitation writes nothing');
91
+
92
+ -- A RESOLVED service-role owner session: attribution is that session's agent, never the forged
93
+ -- compatibility argument (pg_temp.a(2) is not even a member).
94
+ select set_config('request.jwt.claims', json_build_object('role', 'service_role', 'sub', pg_temp.a(1))::text, true);
95
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(13), 'joined', pg_temp.a(2))$$, 'PROV-26 resolved service owner approves with forged attribution');
96
+ select is((select string_agg(membership_state || ':' || coalesce(created_by_agent_id::text, 'null'), ',') from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(13)),
97
+ 'joined:' || pg_temp.a(1)::text, 'PROV-27 service owner approval attribution is the session actor');
98
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(14), 'removed', pg_temp.a(2))$$, 'PROV-28 resolved service owner rejects with forged attribution');
99
+ select is((select string_agg(membership_state || ':' || coalesce(created_by_agent_id::text, 'null'), ',') from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(14)),
100
+ 'left:' || pg_temp.a(1)::text, 'PROV-29 service owner rejection attribution is the session actor');
101
+ select lives_ok($$select agent_board.resolve_board_request(pg_temp.a(100), pg_temp.a(15), 'joined', pg_temp.a(2))$$, 'PROV-30 resolved service owner no-op retires the redundant request');
102
+ select is((select created_by_agent_id from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(15) and membership_state = 'left'), pg_temp.a(1), 'PROV-31 service owner retirement attribution is the session actor');
103
+ select is((select array_agg(row(id, created_by_agent_id)::text) from agent_board.board_memberships where board_id = pg_temp.a(100) and agent_id = pg_temp.a(15) and membership_state = 'joined'),
104
+ (select array_agg(row(id, created_by_agent_id)::text) from active_before), 'PROV-32 no-op keeps the active row identity and attribution');
105
+ select * from finish();
106
+ rollback;