@consiliency/agent-board-schema 1.60.0 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/fixtures/already-applied.json +12 -0
- package/migrations/20261011000100_agent_board_session_identities.sql +1964 -0
- package/migrations/20261012000100_agent_board_sidowner.sql +209 -0
- package/migrations/20261014000100_agent_board_grant_target_role.sql +218 -0
- package/package.json +1 -1
- package/schema-manifest.json +30 -3
- package/src/tooling.test.mjs +6 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -4
- package/tests/agent_board_consent_emission.test.sql +30 -11
- package/tests/agent_board_consent_gating.test.sql +16 -2
- package/tests/agent_board_delegate_role_class.test.sql +1 -1
- package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
- package/tests/agent_board_delegation_enforcement.test.sql +1 -1
- package/tests/agent_board_enroll_resumption.test.sql +22 -4
- package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
- package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
- package/tests/agent_board_fk_lock_edges.test.sql +74 -14
- package/tests/agent_board_grant_target_role.test.sql +1118 -0
- package/tests/agent_board_invite_no_downgrade.test.sql +10 -8
- package/tests/agent_board_ledger_authority.test.sql +70 -12
- package/tests/agent_board_membership_current_row_class.test.sql +9 -1
- package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
- package/tests/agent_board_mint_harden.test.sql +38 -27
- package/tests/agent_board_rolereset.test.sql +8 -2
- package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
- package/tests/agent_board_rowid.test.sql +3 -1
- package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
- package/tests/agent_board_session_identity_backfill.test.sql +82 -0
- package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
- package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
- package/tests/agent_board_session_identity_class.test.sql +118 -0
- package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
- package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
- package/tests/agent_board_session_identity_lease.test.sql +235 -0
- package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
- package/tests/agent_board_session_identity_limits.test.sql +208 -0
- package/tests/agent_board_session_identity_lineage.test.sql +64 -0
- package/tests/agent_board_session_identity_link.test.sql +201 -0
- package/tests/agent_board_session_identity_mint.test.sql +233 -0
- package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
- package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
- package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
- package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
- package/tests/agent_board_sidowner_leases.test.sql +107 -0
- package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
- package/tests/agent_board_sidowner_read_class.test.sql +171 -0
- package/tests/agent_board_sidowner_reads.test.sql +136 -0
- package/tests/agent_board_signer_lifecycle.test.sql +26 -4
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
- package/tests/agent_board_write_enforcement_class.test.sql +74 -15
- package/tests/catalog/function_execute_privileges.json +218 -2
- package/tests/catalog/p_metadata_parameters.json +98 -2
- package/tests/isolation/expected/grant_target_role_race.out +348 -0
- package/tests/isolation/expected/session_identity_limits.out +165 -0
- package/tests/isolation/expected/session_identity_links.out +235 -0
- package/tests/isolation/expected/session_identity_reclaim.out +253 -0
- package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
- package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
- package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
- package/tests/isolation/specs/grant_target_role_race.spec +207 -0
- package/tests/isolation/specs/session_identity_limits.spec +81 -0
- package/tests/isolation/specs/session_identity_links.spec +80 -0
- package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
- package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
- package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
- package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +108 -0
- package/tests/seeds/20261011000100_agent_board_session_identities.sql.pre.sql +47 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
-- SIDOWNER read surface: catalog-derived projection and ACL claims.
|
|
2
|
+
create extension if not exists pgtap with schema extensions;
|
|
3
|
+
set search_path = extensions, public, agent_board;
|
|
4
|
+
begin;
|
|
5
|
+
select plan(24);
|
|
6
|
+
|
|
7
|
+
select has_function('agent_board', 'list_session_children', array['uuid'],
|
|
8
|
+
'EC-SIDOWNER-1: scoped children reader exists');
|
|
9
|
+
select has_function('agent_board', 'get_session_identity_state', array['uuid'],
|
|
10
|
+
'EC-SIDOWNER-1: scoped state reader exists');
|
|
11
|
+
select ok(has_function_privilege('authenticated', 'agent_board.list_session_children(uuid)', 'EXECUTE')
|
|
12
|
+
and has_function_privilege('service_role', 'agent_board.list_session_children(uuid)', 'EXECUTE')
|
|
13
|
+
and not has_function_privilege('public', 'agent_board.list_session_children(uuid)', 'EXECUTE')
|
|
14
|
+
and not has_function_privilege('anon', 'agent_board.list_session_children(uuid)', 'EXECUTE'),
|
|
15
|
+
'EC-SIDOWNER-1: children reader is authenticated/service-only');
|
|
16
|
+
select ok(has_function_privilege('authenticated', 'agent_board.get_session_identity_state(uuid)', 'EXECUTE')
|
|
17
|
+
and has_function_privilege('service_role', 'agent_board.get_session_identity_state(uuid)', 'EXECUTE')
|
|
18
|
+
and not has_function_privilege('public', 'agent_board.get_session_identity_state(uuid)', 'EXECUTE')
|
|
19
|
+
and not has_function_privilege('anon', 'agent_board.get_session_identity_state(uuid)', 'EXECUTE'),
|
|
20
|
+
'EC-SIDOWNER-1: state reader is authenticated/service-only');
|
|
21
|
+
select is(has_table_privilege('authenticated', 'agent_board.session_identities', 'SELECT'), false,
|
|
22
|
+
'EC-SIDOWNER-1: readers do not relax identity table ACL');
|
|
23
|
+
select is(has_table_privilege('authenticated', 'agent_board.session_id_aliases', 'SELECT'), false,
|
|
24
|
+
'EC-SIDOWNER-1: readers do not expose aliases');
|
|
25
|
+
select ok(agent_board_test.stripped_body('agent_board.list_session_children(uuid)'::regprocedure, true)
|
|
26
|
+
like '%parent_agent_id%' and agent_board_test.stripped_body('agent_board.list_session_children(uuid)'::regprocedure, true)
|
|
27
|
+
not like '%forked_from%',
|
|
28
|
+
'EC-SIDOWNER-1: children traversal uses only parent lineage');
|
|
29
|
+
select ok(pg_get_function_result('agent_board.get_session_identity_state(uuid)'::regprocedure)
|
|
30
|
+
!~ 'holder_secret_hash|credential_epoch|session_id_aliases|agent_credentials',
|
|
31
|
+
'EC-SIDOWNER-1: state projection excludes secrets and credential material');
|
|
32
|
+
select ok(agent_board_test.stripped_body('agent_board._session_identity_read_allowed(uuid)'::regprocedure, true)
|
|
33
|
+
like '%_session_identity_owner%' and agent_board_test.stripped_body('agent_board._session_identity_read_allowed(uuid)'::regprocedure, true)
|
|
34
|
+
like '%current_agent_id%',
|
|
35
|
+
'EC-SIDOWNER-1: reader authorizes verified owner or lineage actor');
|
|
36
|
+
select is(
|
|
37
|
+
(select coalesce(string_agg(p.oid::regprocedure::text, ', ' order by p.oid::regprocedure::text), '')
|
|
38
|
+
from pg_proc p
|
|
39
|
+
where p.pronamespace = 'agent_board'::regnamespace
|
|
40
|
+
and p.proname in ('list_session_children', 'get_session_identity_state')
|
|
41
|
+
and (has_function_privilege('public', p.oid, 'EXECUTE') or has_function_privilege('anon', p.oid, 'EXECUTE'))),
|
|
42
|
+
'', 'EC-SIDOWNER-1: catalog reader class has no public or anon route');
|
|
43
|
+
select ok((select count(*) = 2 from pg_proc p where p.pronamespace = 'agent_board'::regnamespace
|
|
44
|
+
and p.proname in ('list_session_children', 'get_session_identity_state')),
|
|
45
|
+
'EC-SIDOWNER-1: catalog reader population is exact');
|
|
46
|
+
select ok((select count(*) = 2 from pg_proc p where p.pronamespace = 'agent_board'::regnamespace
|
|
47
|
+
and p.proname in ('list_session_children', 'get_session_identity_state') and obj_description(p.oid, 'pg_proc') is not null),
|
|
48
|
+
'EC-SIDOWNER-1: every public SIDOWNER reader has a catalog contract');
|
|
49
|
+
|
|
50
|
+
-- Discover table users and literal callers by effect, including private helpers.
|
|
51
|
+
-- current_agent_id exposes only the existing fenced principal UUID, not readings;
|
|
52
|
+
-- its positive classification is asserted below, not exempted from discovery.
|
|
53
|
+
create temp table _owner_fns as
|
|
54
|
+
select p.*, agent_board_test.stripped_body(p.oid) as body,
|
|
55
|
+
agent_board_test.stripped_body(p.oid,true) as literals
|
|
56
|
+
from pg_proc p where p.pronamespace='agent_board'::regnamespace and p.prokind='f';
|
|
57
|
+
create temp table _owner_calls as select * from agent_board_test.fn_call_closure();
|
|
58
|
+
create temp view _owner_direct as
|
|
59
|
+
select f.* from _owner_fns f where f.body ~ agent_board_test.re(
|
|
60
|
+
agent_board_test.qname_frag('session_identities',agent_board_test.bare_ok(f.oid)) || '|'
|
|
61
|
+
|| agent_board_test.qname_frag('session_id_aliases',agent_board_test.bare_ok(f.oid)));
|
|
62
|
+
create temp table _owner_dml as
|
|
63
|
+
select distinct f.oid from _owner_fns f join pg_class t on t.relnamespace='agent_board'::regnamespace
|
|
64
|
+
and t.relkind in ('r','p') where f.body ~ agent_board_test.re(
|
|
65
|
+
agent_board_test.qname_frag(t.relname,agent_board_test.bare_ok(f.oid)))
|
|
66
|
+
and f.body ~ agent_board_test.re(
|
|
67
|
+
agent_board_test.write_frag(t.relname,agent_board_test.bare_ok(f.oid)));
|
|
68
|
+
create temp view _owner_mutators as
|
|
69
|
+
select distinct c.root_oid as oid from _owner_calls c
|
|
70
|
+
join _owner_dml w on w.oid=c.reached_oid join _owner_fns f on f.oid=c.root_oid
|
|
71
|
+
where not has_function_privilege('authenticated',f.oid,'EXECUTE')
|
|
72
|
+
or pg_get_function_result(f.oid) not in ('json','jsonb','record');
|
|
73
|
+
create temp view _owner_principal as
|
|
74
|
+
select f.oid from _owner_direct f where f.proname='current_agent_id'
|
|
75
|
+
and pg_get_function_result(f.oid)='uuid'
|
|
76
|
+
and f.body ~ agent_board_test.call_re('auth.uid',false)
|
|
77
|
+
and f.literals like '%identity_kind is distinct from ''session'' or c.lease_epoch = si.credential_epoch::text%';
|
|
78
|
+
create temp view _owner_internal as
|
|
79
|
+
select f.oid from _owner_direct f
|
|
80
|
+
where pg_get_function_result(f.oid) in ('boolean','void','trigger','timestamp with time zone')
|
|
81
|
+
and not has_function_privilege('authenticated',f.oid,'EXECUTE')
|
|
82
|
+
and not has_function_privilege('public',f.oid,'EXECUTE')
|
|
83
|
+
and not has_function_privilege('anon',f.oid,'EXECUTE');
|
|
84
|
+
create temp view _owner_scoped as
|
|
85
|
+
select f.oid from _owner_fns f where f.proname in ('get_session_identity_state','list_session_children')
|
|
86
|
+
and f.prosecdef and f.provolatile='s' and f.proconfig @> array['search_path=""']
|
|
87
|
+
and f.body ~ agent_board_test.call_re('_session_identity_read_allowed')
|
|
88
|
+
and f.literals like '%raise exception ''identity_read_forbidden'' using errcode = ''42501''%';
|
|
89
|
+
create temp view _owner_unclassified_direct as
|
|
90
|
+
select f.oid,f.proname from _owner_direct f
|
|
91
|
+
where f.oid not in (select oid from _owner_mutators union select oid from _owner_principal
|
|
92
|
+
union select oid from _owner_internal union select oid from _owner_scoped);
|
|
93
|
+
create temp view _owner_routes as
|
|
94
|
+
select distinct f.oid,f.proname from _owner_fns f join _owner_calls c on c.root_oid=f.oid
|
|
95
|
+
join _owner_direct d on d.oid=c.reached_oid
|
|
96
|
+
where d.oid not in (select oid from _owner_principal);
|
|
97
|
+
create temp view _owner_unclassified_routes as
|
|
98
|
+
select f.* from _owner_routes f
|
|
99
|
+
where f.oid not in (select oid from _owner_mutators union select oid from _owner_internal union select oid from _owner_scoped);
|
|
100
|
+
select is((select count(*) from _owner_principal),1::bigint,
|
|
101
|
+
'SIDOWNER-principal-positive: existing resolver returns only the session-epoch-fenced actor UUID');
|
|
102
|
+
select ok((select count(*)>=20 from _owner_direct),
|
|
103
|
+
'SIDOWNER-reader-population: catalog discovery includes existing table users, not just two names');
|
|
104
|
+
select is((select coalesce(string_agg(proname,', ' order by proname),'') from _owner_unclassified_direct),'',
|
|
105
|
+
'SIDOWNER-reader-class: every direct table user is positively classified; a new reader names itself');
|
|
106
|
+
select is((select coalesce(string_agg(proname,', ' order by proname),'') from _owner_unclassified_routes),'',
|
|
107
|
+
'SIDOWNER-reader-route-class: every sensitive literal caller is positively classified; a new wrapper names itself');
|
|
108
|
+
select is((select coalesce(string_agg(proname,', ' order by proname),'') from _owner_routes
|
|
109
|
+
where has_function_privilege('public',oid,'EXECUTE') or has_function_privilege('anon',oid,'EXECUTE')),'',
|
|
110
|
+
'SIDOWNER-reader-route-acl: discovered sensitive routes have no public or anonymous execution');
|
|
111
|
+
select is((select count(*) from _owner_scoped),2::bigint,
|
|
112
|
+
'SIDOWNER-scoped-positive: both scoped readers are stable definers with empty paths and named authorization');
|
|
113
|
+
select is((select coalesce(string_agg(distinct c.root_name,', ' order by c.root_name),'')
|
|
114
|
+
from _owner_calls c join _owner_dml d on d.oid=c.reached_oid
|
|
115
|
+
where c.root_name in ('get_session_identity_state','list_session_children')),'',
|
|
116
|
+
'SIDOWNER-select-only: neither scoped reader nor any literal callee writes any board table');
|
|
117
|
+
select ok((select count(*)>=5 from _owner_internal),
|
|
118
|
+
'SIDOWNER-internal-positive: internal table readers return nonprojecting scalar/trigger results and are not authenticated callable');
|
|
119
|
+
create function agent_board.sidowner_unclassified_reader() returns jsonb
|
|
120
|
+
language sql security definer set search_path='' as $probe$
|
|
121
|
+
SELECT to_jsonb(si) FROM agent_board.session_identities si LIMIT 1;
|
|
122
|
+
$probe$;
|
|
123
|
+
revoke all on function agent_board.sidowner_unclassified_reader() from public,anon;
|
|
124
|
+
grant execute on function agent_board.sidowner_unclassified_reader() to authenticated,service_role;
|
|
125
|
+
create function agent_board.sidowner_unclassified_wrapper() returns jsonb
|
|
126
|
+
language sql security definer set search_path='' as $probe$
|
|
127
|
+
SELECT agent_board.sidowner_unclassified_reader(/* no direct table reference */);
|
|
128
|
+
$probe$;
|
|
129
|
+
revoke all on function agent_board.sidowner_unclassified_wrapper() from public,anon;
|
|
130
|
+
grant execute on function agent_board.sidowner_unclassified_wrapper() to authenticated,service_role;
|
|
131
|
+
create function agent_board.sidowner_unclassified_mutating_reader() returns jsonb
|
|
132
|
+
language plpgsql security definer set search_path='' as $probe$
|
|
133
|
+
begin
|
|
134
|
+
UPDATE agent_board.presence SET availability=availability WHERE false;
|
|
135
|
+
return (SELECT to_jsonb(si) FROM agent_board.session_identities si LIMIT 1);
|
|
136
|
+
end;
|
|
137
|
+
$probe$;
|
|
138
|
+
revoke all on function agent_board.sidowner_unclassified_mutating_reader() from public,anon;
|
|
139
|
+
grant execute on function agent_board.sidowner_unclassified_mutating_reader() to authenticated,service_role;
|
|
140
|
+
-- Refresh from the actual catalog after adding the canaries, including their
|
|
141
|
+
-- transitive call graph. Caching avoids repeatedly stripping every routine.
|
|
142
|
+
insert into _owner_fns
|
|
143
|
+
select p.*, agent_board_test.stripped_body(p.oid) as body,
|
|
144
|
+
agent_board_test.stripped_body(p.oid,true) as literals
|
|
145
|
+
from pg_proc p where p.oid in ('agent_board.sidowner_unclassified_reader()'::regprocedure,
|
|
146
|
+
'agent_board.sidowner_unclassified_wrapper()'::regprocedure,
|
|
147
|
+
'agent_board.sidowner_unclassified_mutating_reader()'::regprocedure);
|
|
148
|
+
truncate _owner_calls;
|
|
149
|
+
insert into _owner_calls select * from agent_board_test.fn_call_closure();
|
|
150
|
+
truncate _owner_dml;
|
|
151
|
+
insert into _owner_dml
|
|
152
|
+
select distinct f.oid from _owner_fns f join pg_class t on t.relnamespace='agent_board'::regnamespace
|
|
153
|
+
and t.relkind in ('r','p') where f.body ~ agent_board_test.re(
|
|
154
|
+
agent_board_test.qname_frag(t.relname,agent_board_test.bare_ok(f.oid)))
|
|
155
|
+
and f.body ~ agent_board_test.re(
|
|
156
|
+
agent_board_test.write_frag(t.relname,agent_board_test.bare_ok(f.oid)));
|
|
157
|
+
select is((select string_agg(proname,', ' order by proname) from _owner_unclassified_direct
|
|
158
|
+
where proname='sidowner_unclassified_reader'),'sidowner_unclassified_reader',
|
|
159
|
+
'SIDOWNER-reader-canary: new authenticated uppercase reader names itself without a PUBLIC grant');
|
|
160
|
+
select is((select string_agg(proname,', ' order by proname) from _owner_unclassified_routes
|
|
161
|
+
where proname in ('sidowner_unclassified_reader','sidowner_unclassified_wrapper')),
|
|
162
|
+
'sidowner_unclassified_reader, sidowner_unclassified_wrapper',
|
|
163
|
+
'SIDOWNER-wrapper-canary: indirect reader also names itself in literal-call closure');
|
|
164
|
+
select is((select count(*) from _owner_mutators where oid in (
|
|
165
|
+
'agent_board.sidowner_unclassified_reader()'::regprocedure,'agent_board.sidowner_unclassified_wrapper()'::regprocedure)),0::bigint,
|
|
166
|
+
'SIDOWNER-positive-classification-canary: a SELECT-only secret reader is not mislabeled as a lifecycle writer');
|
|
167
|
+
select is((select proname::text from _owner_unclassified_direct
|
|
168
|
+
where proname='sidowner_unclassified_mutating_reader'),'sidowner_unclassified_mutating_reader',
|
|
169
|
+
'SIDOWNER-unrelated-write-canary: unrelated no-op DML cannot classify an authenticated JSON reader as safe');
|
|
170
|
+
select * from finish();
|
|
171
|
+
rollback;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
create extension if not exists pgtap with schema extensions;
|
|
2
|
+
set search_path = extensions, public, agent_board;
|
|
3
|
+
begin;
|
|
4
|
+
grant usage on schema extensions to authenticated, service_role;
|
|
5
|
+
select plan(31);
|
|
6
|
+
create temp table _owner_fixture(label text primary key, agent_id uuid, auth_id uuid);
|
|
7
|
+
do $$
|
|
8
|
+
declare v_owner uuid; v_root uuid; v_parent uuid; v_id uuid; v_auth uuid; v_label text;
|
|
9
|
+
begin
|
|
10
|
+
foreach v_label in array array['owner','other-owner','root','parent','sibling','grandchild','other-root'] loop
|
|
11
|
+
v_id := gen_random_uuid();
|
|
12
|
+
v_auth := public.ensure_github_user('sidowner-' || v_id, v_id || '@example.test', v_id::text, v_label);
|
|
13
|
+
if v_label = 'owner' then v_owner := v_auth; end if;
|
|
14
|
+
if v_label = 'root' then v_root := v_id; end if;
|
|
15
|
+
if v_label = 'parent' then v_parent := v_id; end if;
|
|
16
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id, parent_agent_id, root_agent_id)
|
|
17
|
+
values (v_id, case when v_label in ('owner','other-owner') then 'human' else 'agent' end,
|
|
18
|
+
'sidowner-' || v_id, v_label, 'active', v_auth,
|
|
19
|
+
case when v_label in ('root','parent','sibling','grandchild') then v_owner else v_auth end,
|
|
20
|
+
case when v_label in ('parent','sibling') then v_root when v_label = 'grandchild' then v_parent else null end,
|
|
21
|
+
case when v_label in ('parent','sibling','grandchild') then v_root else null end);
|
|
22
|
+
insert into _owner_fixture values (v_label, v_id, v_auth);
|
|
23
|
+
end loop;
|
|
24
|
+
end;
|
|
25
|
+
$$;
|
|
26
|
+
set constraints all immediate;
|
|
27
|
+
set constraints all deferred;
|
|
28
|
+
grant select on _owner_fixture to authenticated, service_role;
|
|
29
|
+
create function pg_temp.owner_actor(p_label text) returns void language plpgsql as $$
|
|
30
|
+
begin
|
|
31
|
+
perform set_config('request.jwt.claims', '{}', true);
|
|
32
|
+
perform set_config('request.jwt.claim.sub', coalesce((select auth_id::text from _owner_fixture where label=p_label), ''), true);
|
|
33
|
+
perform set_config('request.jwt.claim.role', 'authenticated', true);
|
|
34
|
+
perform set_config('request.jwt.claim.agent_credential_id', '', true);
|
|
35
|
+
end;
|
|
36
|
+
$$;
|
|
37
|
+
select pg_temp.owner_actor('owner');
|
|
38
|
+
set local role authenticated;
|
|
39
|
+
create temp table _owner_children as select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='root')) with no data;
|
|
40
|
+
select lives_ok($$insert into _owner_children select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='root'))$$,
|
|
41
|
+
'SIDOWNER-owner-control: human owner enters scoped children reader');
|
|
42
|
+
select is((select count(*) from _owner_children), 3::bigint,
|
|
43
|
+
'SIDOWNER-subtree: all strict descendants at both depths returned');
|
|
44
|
+
select is((select array_agg(depth order by ctid) from _owner_children), array[1,1,2]::smallint[],
|
|
45
|
+
'SIDOWNER-order: children ordered by depth and identity');
|
|
46
|
+
select is((select array_agg(agent_id order by ctid) from _owner_children),
|
|
47
|
+
(select array_agg(agent_id order by depth,agent_id) from _owner_children),
|
|
48
|
+
'SIDOWNER-identity-order: emitted child rows retain depth then UUID ordering');
|
|
49
|
+
select is((select count(*) from _owner_children where agent_id=(select agent_id from _owner_fixture where label='root')), 0::bigint,
|
|
50
|
+
'SIDOWNER-parent-excluded: selected parent is never included');
|
|
51
|
+
select is((select count(*) from _owner_children where root_agent_id<>(select agent_id from _owner_fixture where label='root')), 0::bigint,
|
|
52
|
+
'SIDOWNER-root-filter: unrelated root is never returned');
|
|
53
|
+
select is((select string_agg(attname, ',' order by attnum) from pg_attribute where attrelid='_owner_children'::regclass and attnum>0 and not attisdropped),
|
|
54
|
+
'agent_id,parent_agent_id,root_agent_id,handle,depth,harness,lease_state,resolution_state,fingerprint_resolution,current_risky_digest,approved_risky_digest,recorded_fingerprint,tombstoned_at,tombstone_reason',
|
|
55
|
+
'SIDOWNER-safe-columns: exact normalized projection excludes credential material');
|
|
56
|
+
create temp table _owner_root as select * from agent_board.get_session_identity_state((select agent_id from _owner_fixture where label='root')) with no data;
|
|
57
|
+
select lives_ok($$insert into _owner_root select * from agent_board.get_session_identity_state((select agent_id from _owner_fixture where label='root'))$$,
|
|
58
|
+
'SIDOWNER-owner-state-control: owner inspects root');
|
|
59
|
+
select is((select string_agg(attname, ',' order by attnum) from pg_attribute where attrelid='_owner_root'::regclass and attnum>0 and not attisdropped),
|
|
60
|
+
'agent_id,parent_agent_id,root_agent_id,handle,depth,harness,lease_state,resolution_state,fingerprint_resolution,current_risky_digest,approved_risky_digest,recorded_fingerprint,tombstoned_at,tombstone_reason',
|
|
61
|
+
'SIDOWNER-state-safe-columns: single-state reader has the same exact safe projection');
|
|
62
|
+
select is((select lease_state from _owner_root), 'not_applicable', 'SIDOWNER-legacy-lease: root is not a session lease');
|
|
63
|
+
select is((select fingerprint_resolution from _owner_root), 'unresolved', 'SIDOWNER-missing-reading: backfill reading is unresolved');
|
|
64
|
+
reset role;
|
|
65
|
+
select pg_temp.owner_actor('root');
|
|
66
|
+
set local role authenticated;
|
|
67
|
+
select lives_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='parent'))$$,
|
|
68
|
+
'SIDOWNER-ancestor-control: strict ancestor reads parent subtree');
|
|
69
|
+
select lives_ok($$select * from agent_board.get_session_identity_state((select agent_id from _owner_fixture where label='grandchild'))$$,
|
|
70
|
+
'SIDOWNER-ancestor-state: ancestor reads depth two');
|
|
71
|
+
reset role;
|
|
72
|
+
select pg_temp.owner_actor('parent');
|
|
73
|
+
set local role authenticated;
|
|
74
|
+
select lives_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='parent'))$$,
|
|
75
|
+
'SIDOWNER-self-control: parent reads own children');
|
|
76
|
+
select throws_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='sibling'))$$,
|
|
77
|
+
'42501','identity_read_forbidden','SIDOWNER-sibling: same-root sibling cannot read subtree');
|
|
78
|
+
select throws_ok($$select * from agent_board.get_session_identity_state((select agent_id from _owner_fixture where label='root'))$$,
|
|
79
|
+
'42501','identity_read_forbidden','SIDOWNER-descendant: descendant cannot read ancestor');
|
|
80
|
+
reset role;
|
|
81
|
+
update agent_board.session_identities set resolution_state='frozen' where agent_id=(select agent_id from _owner_fixture where label='parent');
|
|
82
|
+
set local role authenticated;
|
|
83
|
+
select lives_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='parent'))$$,
|
|
84
|
+
'SIDOWNER-frozen-reader: freeze does not deny diagnosis');
|
|
85
|
+
reset role;
|
|
86
|
+
update agent_board.session_identities set tombstoned_at=now(), tombstone_reason='revoked'
|
|
87
|
+
where agent_id=(select agent_id from _owner_fixture where label='grandchild');
|
|
88
|
+
set local role authenticated;
|
|
89
|
+
create temp table _owner_tombstone as select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='parent')) with no data;
|
|
90
|
+
select lives_ok($$insert into _owner_tombstone select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='parent'))$$,
|
|
91
|
+
'SIDOWNER-tombstone-control: parent reads tombstone');
|
|
92
|
+
select is((select tombstone_reason from _owner_tombstone), 'revoked', 'SIDOWNER-tombstone: tombstoned descendants remain visible');
|
|
93
|
+
reset role;
|
|
94
|
+
select pg_temp.owner_actor('other-owner');
|
|
95
|
+
set local role authenticated;
|
|
96
|
+
select throws_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='root'))$$,
|
|
97
|
+
'42501','identity_read_forbidden','SIDOWNER-owner-filter: unrelated human cannot inspect root');
|
|
98
|
+
select throws_ok($$select * from agent_board.get_session_identity_state((select agent_id from _owner_fixture where label='grandchild'))$$,
|
|
99
|
+
'42501','identity_read_forbidden','SIDOWNER-other-owner-state: unrelated human cannot inspect state');
|
|
100
|
+
reset role;
|
|
101
|
+
select pg_temp.owner_actor('other-root');
|
|
102
|
+
set local role authenticated;
|
|
103
|
+
select throws_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='root'))$$,
|
|
104
|
+
'42501','identity_read_forbidden','SIDOWNER-other-root: unrelated agent cannot inspect subtree');
|
|
105
|
+
create temp table _owner_empty as select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='other-root')) with no data;
|
|
106
|
+
select lives_ok($$insert into _owner_empty select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='other-root'))$$,
|
|
107
|
+
'SIDOWNER-empty-control: leaf is an authorized parent');
|
|
108
|
+
select is((select count(*) from _owner_empty), 0::bigint,'SIDOWNER-empty: authorized leaf returns an empty array');
|
|
109
|
+
select throws_ok($$select * from agent_board.list_session_children('00000000-0000-0000-0000-000000000000')$$,
|
|
110
|
+
'42501','identity_read_forbidden','SIDOWNER-missing: missing parent uses same refusal');
|
|
111
|
+
select throws_ok($$select * from agent_board.get_session_identity_state('00000000-0000-0000-0000-000000000000')$$,
|
|
112
|
+
'42501','identity_read_forbidden','SIDOWNER-missing-state: missing state uses same refusal');
|
|
113
|
+
reset role;
|
|
114
|
+
select pg_temp.owner_actor('owner');
|
|
115
|
+
select set_config('request.jwt.claim.agent_credential_id', gen_random_uuid()::text, true);
|
|
116
|
+
set local role authenticated;
|
|
117
|
+
select throws_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='root'))$$,
|
|
118
|
+
'42501','identity_read_forbidden','SIDOWNER-invalid-owner-credential: owner predicate cannot bypass invalid credential');
|
|
119
|
+
reset role;
|
|
120
|
+
select pg_temp.owner_actor('parent');
|
|
121
|
+
update agent_board.agents set status='revoked' where id=(select agent_id from _owner_fixture where label='parent');
|
|
122
|
+
set local role authenticated;
|
|
123
|
+
select throws_ok($$select * from agent_board.list_session_children((select agent_id from _owner_fixture where label='parent'))$$,
|
|
124
|
+
'42501','identity_read_forbidden','SIDOWNER-revoked-reader: revoked parent cannot inspect children');
|
|
125
|
+
reset role;
|
|
126
|
+
select pg_temp.owner_actor('none');
|
|
127
|
+
set local role authenticated;
|
|
128
|
+
select throws_ok($$select * from agent_board.get_session_identity_state((select agent_id from _owner_fixture where label='root'))$$,
|
|
129
|
+
'42501','identity_read_forbidden','SIDOWNER-null-actor: missing actor cannot read state');
|
|
130
|
+
reset role;
|
|
131
|
+
select is((select count(*) from agent_board.session_identities where root_agent_id=(select agent_id from _owner_fixture where label='root')), 4::bigint,
|
|
132
|
+
'SIDOWNER-read-only: reads preserve identity population');
|
|
133
|
+
select is((select resolution_state from agent_board.session_identities where agent_id=(select agent_id from _owner_fixture where label='parent')), 'frozen',
|
|
134
|
+
'SIDOWNER-read-state: diagnosis preserves freeze');
|
|
135
|
+
select * from finish();
|
|
136
|
+
rollback;
|
|
@@ -38,7 +38,7 @@ set search_path = extensions, public, agent_board;
|
|
|
38
38
|
|
|
39
39
|
begin;
|
|
40
40
|
|
|
41
|
-
select plan(
|
|
41
|
+
select plan(50);
|
|
42
42
|
|
|
43
43
|
create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
|
|
44
44
|
begin
|
|
@@ -693,7 +693,25 @@ select ok(
|
|
|
693
693
|
-- would silently drop a site, which is the direction that hides a defect.
|
|
694
694
|
-- ===========================================================================
|
|
695
695
|
|
|
696
|
-
|
|
696
|
+
create temp view _sid_instance_withdrawal as
|
|
697
|
+
select p.oid,agent_board_test.stripped_body(p.oid,true) as b
|
|
698
|
+
from pg_proc p where p.oid='agent_board.tombstone_delegate_instance(text,text,text,text)'::regprocedure;
|
|
699
|
+
select ok((select b like '%perform agent_board.update_board_membership(v_board, v_row.agent_id, ''left'');%'
|
|
700
|
+
and b like '%if v_row.owner_user_id is distinct from v_owner then%'
|
|
701
|
+
and b like '%where di.instance_id = p_instance_id%'
|
|
702
|
+
and (select count(*) from regexp_matches(b,agent_board_test.call_re('update_board_membership'),'g'))=1
|
|
703
|
+
from _sid_instance_withdrawal),
|
|
704
|
+
'EC-SIDSCHEMA-5-instance-withdrawal-positive: the verified-instance revoker writes only left memberships for its bound principal, never a grant or admission; real closure tests check state and append-only provenance');
|
|
705
|
+
select ok((select strpos(b, 'perform agent_board._lock_board_authority_ceiling(v_board);') > 0
|
|
706
|
+
and strpos(b, 'perform agent_board._lock_board_authority_ceiling(v_board);')
|
|
707
|
+
< strpos(b, 'v_owner := agent_board._ensure_agent_auth_user(v_owner_slug);')
|
|
708
|
+
and strpos(b, 'perform agent_board._lock_board_authority_ceiling(v_board);')
|
|
709
|
+
< strpos(b, 'insert into agent_board.delegate_instances')
|
|
710
|
+
and b like '%select b.id from agent_board.boards b order by b.id%'
|
|
711
|
+
from _sid_instance_withdrawal
|
|
712
|
+
where oid='agent_board.tombstone_delegate_instance(text,text,text,text)'::regprocedure),
|
|
713
|
+
'SID-INTEGRITY-lock-order: tombstone_delegate_instance locks the board universe in UUID order before owner/instance and identity-family locks');
|
|
714
|
+
select is( -- pgtap-allow-raise-checked: catalog text comparison only; no RPC executes in this value expression.
|
|
697
715
|
(with recursive fns as (
|
|
698
716
|
select p.oid, p.proname, agent_board_test.stripped_body(p.oid) as b
|
|
699
717
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
@@ -736,10 +754,14 @@ select is(
|
|
|
736
754
|
-- assertion deliberately requires mutual exclusion, which survives a lock being added
|
|
737
755
|
-- downstream, and NOT a re-derived order, which does not.
|
|
738
756
|
'grant_board_role => serialised by the board advisory lock',
|
|
757
|
+
'link_session_alias => serialised by the board advisory lock',
|
|
758
|
+
'mint_delegated_child => serialised by the board advisory lock',
|
|
739
759
|
'onboard_federated_fleet_agent => serialised by the board advisory lock',
|
|
740
|
-
'
|
|
760
|
+
'readmit_session_identity => serialised by the board advisory lock',
|
|
761
|
+
'signed_grant_board_role => serialised by the board advisory lock',
|
|
762
|
+
'tombstone_delegate_instance => serialised by the board advisory lock'
|
|
741
763
|
]::text[],
|
|
742
|
-
'every
|
|
764
|
+
'every discovered agent+membership locker takes the board advisory lock (transitive; a new one names itself). FK-implied locks are covered by agent_board_fk_lock_edges.test.sql');
|
|
743
765
|
|
|
744
766
|
select finish();
|
|
745
767
|
|
|
@@ -56,7 +56,7 @@ select has_function(
|
|
|
56
56
|
'agent_board', 'issue_signing_key_challenge', array['uuid', 'text', 'interval'],
|
|
57
57
|
'issue_signing_key_challenge exists');
|
|
58
58
|
select has_function(
|
|
59
|
-
'agent_board', 'mint_fleet_agent_session', array['uuid', 'text', 'uuid', 'uuid', 'integer'],
|
|
59
|
+
'agent_board', 'mint_fleet_agent_session', array['uuid', 'text', 'uuid', 'uuid', 'integer', 'text', 'text'],
|
|
60
60
|
'mint_fleet_agent_session exists');
|
|
61
61
|
|
|
62
62
|
-- Seed one agent + key, then fill the key's sixteen pending slots.
|
|
@@ -332,7 +332,7 @@ set search_path = extensions, public, agent_board;
|
|
|
332
332
|
|
|
333
333
|
begin;
|
|
334
334
|
|
|
335
|
-
select plan(
|
|
335
|
+
select plan(30);
|
|
336
336
|
|
|
337
337
|
-- ===========================================================================================
|
|
338
338
|
-- THE CLASS, derived from pg_catalog. Nothing below is a list of names.
|
|
@@ -430,10 +430,12 @@ select distinct p.oid,
|
|
|
430
430
|
-- 60 -> 62 with HOSTED's delegate_instances and delegate_allowed_operations (20261008000100).
|
|
431
431
|
-- Both are written only by service_role-only definers or the migration itself, so neither joins
|
|
432
432
|
-- the `authenticated` writer population below.
|
|
433
|
+
-- SIDSCHEMA adds identities and aliases. Human-owner readmission writes them and
|
|
434
|
+
-- delegation_grants; these remain in the derived class, not an exemption list.
|
|
433
435
|
select is(
|
|
434
436
|
(select count(*)::text from _wec_target where relkind = 'r'),
|
|
435
|
-
'
|
|
436
|
-
'#325 canary: agent_board has
|
|
437
|
+
'64',
|
|
438
|
+
'#325 canary: agent_board has 64 base tables — the universe the write scan runs over, pinned so an empty violating set below cannot mean an empty universe');
|
|
437
439
|
|
|
438
440
|
-- WEC-2. The relations the class writes, as an EXACT SET. A new one names itself.
|
|
439
441
|
select is(
|
|
@@ -459,6 +461,7 @@ select is(
|
|
|
459
461
|
'callback_subscriptions',
|
|
460
462
|
'claims',
|
|
461
463
|
'conflicts',
|
|
464
|
+
'delegation_grants',
|
|
462
465
|
'delegation_policies',
|
|
463
466
|
'delegations',
|
|
464
467
|
'delivery_recipients',
|
|
@@ -476,11 +479,13 @@ select is(
|
|
|
476
479
|
'presence',
|
|
477
480
|
'read_receipts',
|
|
478
481
|
'send_authz_telemetry',
|
|
482
|
+
'session_id_aliases',
|
|
483
|
+
'session_identities',
|
|
479
484
|
'task_events',
|
|
480
485
|
'tasks',
|
|
481
486
|
'threads'
|
|
482
487
|
]::text[],
|
|
483
|
-
'#325: these
|
|
488
|
+
'#325: these 40 agent_board relations are written by an authenticated-executable function, directly or through a callee — SIDSCHEMA readmission adds grants, identities and aliases without excluding their writers');
|
|
484
489
|
|
|
485
490
|
-- WEC-3. The class itself, as an EXACT SET of SIGNATURES, and it is signatures rather than
|
|
486
491
|
-- `count(*) = 59` for two reasons. The routine response to a red count is to bump the count,
|
|
@@ -495,6 +500,8 @@ select is( -- pgtap-allow-raise-checked
|
|
|
495
500
|
-- HOSTED (20261008000100): the owner places its hosted delegate through the sink.
|
|
496
501
|
'agent_board.add_delegate_to_board(uuid,uuid)',
|
|
497
502
|
'agent_board.append_task_event(uuid,uuid,text,jsonb,text,uuid,timestamp with time zone)',
|
|
503
|
+
-- Approval's measured fingerprint is service-only until SIDSERVER binds it.
|
|
504
|
+
-- Its ACL and real human/ancestor actors are pinned in the fingerprint suite.
|
|
498
505
|
'agent_board.block_board_inviter(uuid,uuid,uuid,jsonb)',
|
|
499
506
|
'agent_board.create_board(uuid,text,text,text,jsonb,text)',
|
|
500
507
|
'agent_board.create_fleet_onboarding_invite(uuid,uuid,text,text,text,text,text,text,text,integer,timestamp with time zone,uuid,jsonb)',
|
|
@@ -509,6 +516,7 @@ select is( -- pgtap-allow-raise-checked
|
|
|
509
516
|
'agent_board.disable_callback_endpoint(uuid,uuid,text,jsonb)',
|
|
510
517
|
'agent_board.grant_board_role(uuid,uuid,text,uuid)',
|
|
511
518
|
'agent_board.heartbeat(uuid,uuid,text,text,interval,jsonb)',
|
|
519
|
+
'agent_board.heartbeat_session_identity(uuid)',
|
|
512
520
|
'agent_board.invite_board_member(uuid,uuid,uuid,jsonb)',
|
|
513
521
|
'agent_board.issue_signing_key_challenge(uuid,text,interval)',
|
|
514
522
|
'agent_board.join_board(uuid,uuid,text,uuid,jsonb)',
|
|
@@ -520,13 +528,17 @@ select is( -- pgtap-allow-raise-checked
|
|
|
520
528
|
'agent_board.provision_agent(text,text,jsonb,uuid)',
|
|
521
529
|
'agent_board.raise_conflict(uuid,uuid,text,text,text,uuid,jsonb)',
|
|
522
530
|
'agent_board.raise_escalation(uuid,uuid,text,text,text,uuid,uuid,jsonb)',
|
|
531
|
+
'agent_board.readmit_session_identity(uuid,text,text,jsonb,jsonb,interval)',
|
|
523
532
|
'agent_board.record_signing_key_proof_result(uuid,uuid,text,boolean,text,jsonb)',
|
|
524
533
|
'agent_board.redeem_invite_link(text,jsonb)',
|
|
525
534
|
'agent_board.register_agent(text,text,text,text,jsonb)',
|
|
526
535
|
'agent_board.register_callback_endpoint(uuid,text,jsonb)',
|
|
527
536
|
'agent_board.register_fleet_machine(uuid,uuid,text,text,text,jsonb)',
|
|
528
537
|
'agent_board.release_claim(uuid)',
|
|
538
|
+
'agent_board.rename_agent(uuid,text)',
|
|
529
539
|
'agent_board.renew_claim(uuid,timestamp with time zone)',
|
|
540
|
+
-- Fresh renewal attestation is service-role-only; lifetime's trust assertion
|
|
541
|
+
-- positively pins the ACL, rather than excluding a reachable writer here.
|
|
530
542
|
'agent_board.request_join_board(uuid,uuid,jsonb)',
|
|
531
543
|
'agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb)',
|
|
532
544
|
'agent_board.resolve_conflict(uuid,uuid,jsonb)',
|
|
@@ -535,6 +547,7 @@ select is( -- pgtap-allow-raise-checked
|
|
|
535
547
|
'agent_board.revoke_fleet_machine_agent_binding(uuid,uuid,text,jsonb)',
|
|
536
548
|
'agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb)',
|
|
537
549
|
'agent_board.revoke_invite_link(text,jsonb)',
|
|
550
|
+
'agent_board.revoke_session_identity(uuid)',
|
|
538
551
|
'agent_board.rotate_callback_signing_key(uuid,uuid,jsonb)',
|
|
539
552
|
'agent_board.self_enroll_human(text)',
|
|
540
553
|
'agent_board.send_direct_message(uuid,uuid,jsonb,text,text,text,text,text,text,jsonb)',
|
|
@@ -543,6 +556,7 @@ select is( -- pgtap-allow-raise-checked
|
|
|
543
556
|
'agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)',
|
|
544
557
|
'agent_board.set_board_authority_ceiling(uuid,text,uuid)',
|
|
545
558
|
'agent_board.set_board_claim_policy(uuid,text)',
|
|
559
|
+
'agent_board.set_board_member_label(uuid,uuid,text)',
|
|
546
560
|
'agent_board.set_board_role_gate(uuid,boolean)',
|
|
547
561
|
'agent_board.set_fleet_receive_policy(uuid,text,text,boolean,uuid,uuid,jsonb)',
|
|
548
562
|
'agent_board.subscribe_callback_endpoint(uuid,uuid,text,text,uuid,jsonb)',
|
|
@@ -553,7 +567,7 @@ select is( -- pgtap-allow-raise-checked
|
|
|
553
567
|
'agent_board.upsert_delegation_policy(uuid,text,jsonb,integer,interval,boolean)',
|
|
554
568
|
'agent_board.verify_callback_endpoint(uuid,uuid,jsonb)'
|
|
555
569
|
]::text[],
|
|
556
|
-
'#325: the effect-derived class is exactly these
|
|
570
|
+
'#325: the effect-derived class is exactly these 66 authenticated-executable writer OVERLOADS — per-OID, and TRANSITIVE, so a delegating wrapper whose own body holds no DML is a member rather than a blind spot');
|
|
557
571
|
|
|
558
572
|
-- WEC-4. THE DELEGATION-CHAIN LIVENESS PIN, and the mitigation for the fail-open inversion the
|
|
559
573
|
-- header describes. Everything above depends on `fn_call_closure()` actually resolving edges; a
|
|
@@ -614,8 +628,8 @@ select is(
|
|
|
614
628
|
(select coalesce(array_agg(p.sig order by p.sig), array[]::text[])
|
|
615
629
|
from _wec_pop p
|
|
616
630
|
where agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.call_re('auth.uid', false)),
|
|
617
|
-
array['agent_board.self_enroll_human(text)']::text[],
|
|
618
|
-
'#325: exactly
|
|
631
|
+
array['agent_board.rename_agent(uuid,text)', 'agent_board.self_enroll_human(text)']::text[],
|
|
632
|
+
'#325: exactly these writers derive from raw auth.uid() — self enrollment creates the human identity; rename requires its active human owner, positively exercised by WEC-26..29');
|
|
619
633
|
|
|
620
634
|
-- ===========================================================================================
|
|
621
635
|
-- HALF B — every writer REFUSES SOMEBODY. Labelled honestly: it pins that a refusal exists on the
|
|
@@ -673,7 +687,6 @@ select is( -- pgtap-allow-raise-checked
|
|
|
673
687
|
'agent_board.issue_signing_key_challenge(uuid,text,interval) -> _resolved_actor_agent_id',
|
|
674
688
|
'agent_board.provision_agent(text,text,jsonb,uuid) -> _require_platform_admin_user_id',
|
|
675
689
|
'agent_board.register_callback_endpoint(uuid,text,jsonb) -> _resolved_actor_agent_id',
|
|
676
|
-
'agent_board.revoke_agent(uuid,text) -> _require_platform_admin_user_id',
|
|
677
690
|
'agent_board.rotate_callback_signing_key(uuid,uuid,jsonb) -> _resolved_actor_agent_id',
|
|
678
691
|
'agent_board.subscribe_callback_endpoint(uuid,uuid,text,text,uuid,jsonb) -> _resolved_actor_agent_id',
|
|
679
692
|
'agent_board.unblock_board_inviter(uuid,uuid,uuid) -> _resolved_actor_agent_id',
|
|
@@ -682,7 +695,7 @@ select is( -- pgtap-allow-raise-checked
|
|
|
682
695
|
'agent_board.upsert_delegation_policy(uuid,text,jsonb,integer,interval,boolean) -> _resolved_actor_agent_id',
|
|
683
696
|
'agent_board.verify_callback_endpoint(uuid,uuid,jsonb) -> _resolved_actor_agent_id'
|
|
684
697
|
]::text[],
|
|
685
|
-
'#325 HALF B, part 1: exactly these
|
|
698
|
+
'#325 HALF B, part 1: exactly these 15 writers reach NO shared authz predicate and raise no 42501 of their own, and each NAMES the raising resolver it relies on — 14 the session resolver, 1 the platform-admin one; revoke_agent now refuses an unresolved actor in its own body; a new ungated writer joins this set, and a member that loses or switches its resolver reds here');
|
|
686
699
|
|
|
687
700
|
-- WEC-9. The union, and the only assertion here that is a claim about the WHOLE class rather than
|
|
688
701
|
-- about a category. Not spelled over the CLOSURE for 42501: that form is satisfied by 59 of 59
|
|
@@ -875,7 +888,7 @@ select is(
|
|
|
875
888
|
cross join unnest(array['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE']) as pv
|
|
876
889
|
where has_table_privilege(r, t.reloid, pv)),
|
|
877
890
|
array[]::text[],
|
|
878
|
-
'#325: NEITHER authenticated NOR anon holds a direct write privilege on ANY of the
|
|
891
|
+
'#325: NEITHER authenticated NOR anon holds a direct write privilege on ANY of the 40 class relations — so for those two roles every write goes through some agent_board function; WEC-3 is what says which');
|
|
879
892
|
|
|
880
893
|
-- WEC-18. The arm that DOES NOT hold, pinned as an exact set rather than omitted. service_role
|
|
881
894
|
-- writes thirteen of these relations directly, with no function and no body — the structural
|
|
@@ -900,6 +913,7 @@ select is(
|
|
|
900
913
|
'INSERT:board_invite_redemptions',
|
|
901
914
|
'INSERT:board_invite_tokens',
|
|
902
915
|
'INSERT:board_membership_events',
|
|
916
|
+
'INSERT:delegation_grants',
|
|
903
917
|
'INSERT:fleet_harness_registrations',
|
|
904
918
|
'INSERT:fleet_machine_agents',
|
|
905
919
|
'INSERT:fleet_machines',
|
|
@@ -911,6 +925,7 @@ select is(
|
|
|
911
925
|
'UPDATE:board_authority_ceilings',
|
|
912
926
|
'UPDATE:board_invite_redemptions',
|
|
913
927
|
'UPDATE:board_invite_tokens',
|
|
928
|
+
'UPDATE:delegation_grants',
|
|
914
929
|
'UPDATE:fleet_harness_registrations',
|
|
915
930
|
'UPDATE:fleet_machine_agents',
|
|
916
931
|
'UPDATE:fleet_machines',
|
|
@@ -919,7 +934,7 @@ select is(
|
|
|
919
934
|
'UPDATE:fleet_revocation_events',
|
|
920
935
|
'UPDATE:fleet_trust_domains'
|
|
921
936
|
]::text[],
|
|
922
|
-
'#325: service_role holds direct DML on
|
|
937
|
+
'#325: service_role holds direct DML on FOURTEEN of the 40 class relations — grants newly enter this class through readmission, not through a new grant; the exact privilege set bounds the hole');
|
|
923
938
|
|
|
924
939
|
-- WEC-19. The rule-1-correct arm. WEC-17 and WEC-18 are hand lists of three role names between
|
|
925
940
|
-- them, so an INSERT granted to a brand-new role self-names in NEITHER. This reads the ACL itself.
|
|
@@ -952,7 +967,7 @@ select ok(
|
|
|
952
967
|
where c.relowner <> 'postgres'::regrole) = 0
|
|
953
968
|
and has_table_privilege('postgres', 'agent_board.threads', 'INSERT')
|
|
954
969
|
and has_table_privilege('postgres', 'agent_board.threads', 'DELETE'),
|
|
955
|
-
'#325: postgres OWNS every one of the
|
|
970
|
+
'#325: postgres OWNS every one of the 40 class relations (pg_class.relowner, not inferred from a privilege) and holds writes on them — so the WEC-19 exclusion is a fact about ownership, not a carve-out');
|
|
956
971
|
|
|
957
972
|
-- WEC-21. The over-revoke guard. A file that only ever asserts absence would call a schema with
|
|
958
973
|
-- every privilege revoked a success.
|
|
@@ -1006,12 +1021,13 @@ select is(
|
|
|
1006
1021
|
'agent_board.fleet_trust_domains',
|
|
1007
1022
|
'agent_board.inbox_items',
|
|
1008
1023
|
'agent_board.messages',
|
|
1024
|
+
'agent_board.session_identities',
|
|
1009
1025
|
'agent_board.task_events',
|
|
1010
1026
|
'agent_board.tasks',
|
|
1011
1027
|
'agent_board.threads',
|
|
1012
1028
|
'auth.users'
|
|
1013
1029
|
]::text[],
|
|
1014
|
-
'#325: deleting a row in one of these
|
|
1030
|
+
'#325: deleting a row in one of these 20 parents CASCADES or SET NULLs into a class relation — a write path with no privilege check and no function body; one parent is auth.users, i.e. outside agent_board entirely');
|
|
1015
1031
|
|
|
1016
1032
|
-- WEC-23. The edge count. This IS a bare count and the hazard is stated rather than left for a
|
|
1017
1033
|
-- reviewer: the routine response to a red count is to bump it, which absorbs a new cascade edge
|
|
@@ -1023,8 +1039,8 @@ select is(
|
|
|
1023
1039
|
from _wec_class_tables t
|
|
1024
1040
|
join pg_constraint con on con.conrelid = t.reloid
|
|
1025
1041
|
where con.contype = 'f' and con.confdeltype in ('c', 'n', 'd')),
|
|
1026
|
-
'
|
|
1027
|
-
'#325:
|
|
1042
|
+
'92',
|
|
1043
|
+
'#325: 92 writing referential actions (78 CASCADE + 14 SET NULL) reach the 40 class relations — the four SIDSCHEMA class additions are positively pinned in WEC-30');
|
|
1028
1044
|
|
|
1029
1045
|
-- ===========================================================================================
|
|
1030
1046
|
-- WEC-24/25 — THE CROSS-CHECK (Consiliency/message-board#416 / #417 / #424, plan change 9).
|
|
@@ -1138,6 +1154,49 @@ select is(
|
|
|
1138
1154
|
'mark_retention_metadata',
|
|
1139
1155
|
'WEC-25 (change 9): the plpgsql bodies WEC-24 skips for dynamic SQL are exactly these — mark_retention_metadata writes board_memberships through format(), which neither write_re nor the parser can see; a second dynamic writer fails here by name');
|
|
1140
1156
|
|
|
1157
|
+
-- WEC-26..29: raw auth.uid() for rename is a human-ownership check, not agent
|
|
1158
|
+
-- self-authentication or a service-role assertion. Exercise the real API role.
|
|
1159
|
+
create temp table _wec_owner(user_id uuid, original_handle text) on commit drop;
|
|
1160
|
+
do $$ declare u uuid; begin
|
|
1161
|
+
u := public.ensure_github_user('wec-owner-' || gen_random_uuid(), 'wec-owner@example.test',
|
|
1162
|
+
'wec-owner-' || gen_random_uuid(), 'WEC owner');
|
|
1163
|
+
insert into agent_board.agents(agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
1164
|
+
values ('human', 'wec-human-' || gen_random_uuid(), 'WEC owner', 'active', u, u);
|
|
1165
|
+
update agent_board.agents set owner_user_id = u
|
|
1166
|
+
where id = (select agent_id from _wec_agent where label = 'a');
|
|
1167
|
+
insert into _wec_owner select u, handle from agent_board.agents
|
|
1168
|
+
where id = (select agent_id from _wec_agent where label = 'a');
|
|
1169
|
+
end $$;
|
|
1170
|
+
grant select on _wec_owner, _wec_agent to authenticated;
|
|
1171
|
+
grant usage on schema extensions to authenticated;
|
|
1172
|
+
set local role authenticated;
|
|
1173
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
1174
|
+
select set_config('request.jwt.claim.sub', (select user_id from _wec_owner)::text, true);
|
|
1175
|
+
select lives_ok($$select agent_board.rename_agent((select agent_id from _wec_agent where label = 'a'), 'WEC renamed')$$,
|
|
1176
|
+
'WEC-26: active authenticated human owner can rename its agent');
|
|
1177
|
+
reset role;
|
|
1178
|
+
select is((select display_name || '/' || (handle = original_handle)::text
|
|
1179
|
+
from agent_board.agents cross join _wec_owner where id = (select agent_id from _wec_agent where label = 'a')),
|
|
1180
|
+
'WEC renamed/true', 'WEC-27: raw-human rename writes only the intended display name and preserves handle');
|
|
1181
|
+
-- The nearest invalid owner: ownership matches the agent's auth user, but that
|
|
1182
|
+
-- principal is not a human. A stranger would hide loss of the human predicate.
|
|
1183
|
+
update agent_board.agents set owner_user_id = (select user_id from _wec_agent where label = 'a')
|
|
1184
|
+
where id = (select agent_id from _wec_agent where label = 'a');
|
|
1185
|
+
set local role authenticated;
|
|
1186
|
+
select set_config('request.jwt.claim.sub', (select user_id from _wec_agent where label = 'a')::text, true);
|
|
1187
|
+
select throws_ok($$select agent_board.rename_agent((select agent_id from _wec_agent where label = 'a'), 'WEC agent renamed')$$,
|
|
1188
|
+
'42501', 'agent rename requires the human owner', 'WEC-28: the agent credential is not its human owner');
|
|
1189
|
+
reset role;
|
|
1190
|
+
select is((select display_name from agent_board.agents where id = (select agent_id from _wec_agent where label = 'a')),
|
|
1191
|
+
'WEC renamed', 'WEC-29: refused agent rename leaves the human-owned state unchanged');
|
|
1192
|
+
select is((select array_agg(c.conname::text || ':' || c.confdeltype::text order by c.conname::text)
|
|
1193
|
+
from _wec_class_tables t join pg_constraint c on c.conrelid = t.reloid
|
|
1194
|
+
where t.relname in ('delegation_grants', 'session_identities', 'session_id_aliases')
|
|
1195
|
+
and c.contype = 'f' and c.confdeltype in ('c', 'n', 'd')),
|
|
1196
|
+
array['delegation_grants_child_agent_id_fkey:c', 'delegation_grants_root_agent_id_fkey:c',
|
|
1197
|
+
'session_id_aliases_identity_agent_id_fkey:c', 'session_identities_agent_id_fkey:c']::text[],
|
|
1198
|
+
'WEC-30: all four newly class-visible referential writes are the intended principal/grant/identity/alias cascades');
|
|
1199
|
+
|
|
1141
1200
|
select * from finish();
|
|
1142
1201
|
|
|
1143
1202
|
rollback;
|