@consiliency/agent-board-schema 1.60.0 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +86 -0
  2. package/fixtures/already-applied.json +16 -0
  3. package/migrations/20261011000100_agent_board_session_identities.sql +1964 -0
  4. package/migrations/20261012000100_agent_board_sidowner.sql +209 -0
  5. package/migrations/20261014000100_agent_board_grant_target_role.sql +218 -0
  6. package/migrations/20261014000200_agent_board_last_owner.sql +239 -0
  7. package/package.json +1 -1
  8. package/schema-manifest.json +39 -3
  9. package/src/tooling.test.mjs +10 -2
  10. package/tests/agent_board_atomic_fleet_onboarding.test.sql +15 -4
  11. package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +7 -0
  12. package/tests/agent_board_consent_emission.test.sql +30 -11
  13. package/tests/agent_board_consent_gating.test.sql +16 -2
  14. package/tests/agent_board_delegate_role_class.test.sql +1 -1
  15. package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
  16. package/tests/agent_board_delegation_enforcement.test.sql +1 -1
  17. package/tests/agent_board_enroll_resumption.test.sql +22 -4
  18. package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
  19. package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
  20. package/tests/agent_board_fk_lock_edges.test.sql +77 -16
  21. package/tests/agent_board_grant_target_role.test.sql +1118 -0
  22. package/tests/agent_board_hosted_delegates.test.sql +5 -3
  23. package/tests/agent_board_invite_no_downgrade.test.sql +15 -11
  24. package/tests/agent_board_ledger_authority.test.sql +77 -12
  25. package/tests/agent_board_membership_current_row_class.test.sql +9 -1
  26. package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
  27. package/tests/agent_board_mint_harden.test.sql +38 -27
  28. package/tests/agent_board_role_guard.test.sql +319 -0
  29. package/tests/agent_board_rolereset.test.sql +25 -29
  30. package/tests/agent_board_roomdeleg.test.sql +8 -0
  31. package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
  32. package/tests/agent_board_roomown.test.sql +3 -1
  33. package/tests/agent_board_rowid.test.sql +7 -2
  34. package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
  35. package/tests/agent_board_session_identity_backfill.test.sql +82 -0
  36. package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
  37. package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
  38. package/tests/agent_board_session_identity_class.test.sql +118 -0
  39. package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
  40. package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
  41. package/tests/agent_board_session_identity_lease.test.sql +235 -0
  42. package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
  43. package/tests/agent_board_session_identity_limits.test.sql +208 -0
  44. package/tests/agent_board_session_identity_lineage.test.sql +64 -0
  45. package/tests/agent_board_session_identity_link.test.sql +201 -0
  46. package/tests/agent_board_session_identity_mint.test.sql +233 -0
  47. package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
  48. package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
  49. package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
  50. package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
  51. package/tests/agent_board_sidowner_leases.test.sql +107 -0
  52. package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
  53. package/tests/agent_board_sidowner_read_class.test.sql +171 -0
  54. package/tests/agent_board_sidowner_reads.test.sql +136 -0
  55. package/tests/agent_board_signer_lifecycle.test.sql +26 -4
  56. package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
  57. package/tests/agent_board_write_enforcement_class.test.sql +74 -15
  58. package/tests/catalog/function_execute_privileges.json +227 -2
  59. package/tests/catalog/p_metadata_parameters.json +102 -2
  60. package/tests/isolation/expected/grant_target_role_race.out +348 -0
  61. package/tests/isolation/expected/last_owner_departure.out +562 -0
  62. package/tests/isolation/expected/session_identity_limits.out +165 -0
  63. package/tests/isolation/expected/session_identity_links.out +235 -0
  64. package/tests/isolation/expected/session_identity_reclaim.out +253 -0
  65. package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
  66. package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
  67. package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
  68. package/tests/isolation/specs/atomic_fleet_authority_races.spec +8 -3
  69. package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +0 -3
  70. package/tests/isolation/specs/atomic_fleet_board_archive.spec +0 -3
  71. package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +0 -3
  72. package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +0 -3
  73. package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +0 -3
  74. package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +0 -3
  75. package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +0 -3
  76. package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +0 -2
  77. package/tests/isolation/specs/claimpolicy_board_scope.spec +0 -2
  78. package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +0 -2
  79. package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +0 -2
  80. package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +0 -2
  81. package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +0 -2
  82. package/tests/isolation/specs/consent_approval_lock.spec +0 -1
  83. package/tests/isolation/specs/consent_emit_actual_state.spec +0 -1
  84. package/tests/isolation/specs/grant_target_role_race.spec +206 -0
  85. package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +0 -1
  86. package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +0 -1
  87. package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +0 -1
  88. package/tests/isolation/specs/last_owner_departure.spec +137 -0
  89. package/tests/isolation/specs/ledger_authority_revoke.spec +0 -3
  90. package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +0 -3
  91. package/tests/isolation/specs/outbox_wake_ordering.spec +0 -1
  92. package/tests/isolation/specs/session_identity_limits.spec +81 -0
  93. package/tests/isolation/specs/session_identity_links.spec +82 -0
  94. package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
  95. package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
  96. package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
  97. package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +107 -0
  98. package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +0 -1
  99. package/tests/isolation/specs/signer_lifecycle_revoke.spec +0 -1
  100. 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(48);
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
- select is(
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
- 'signed_grant_board_role => serialised by the board advisory lock'
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 function that EXPLICITLY row-locks both agents and board_memberships takes the board advisory lock (transitive; a new one names itself). FK-implied locks are invisible here and are covered by agent_board_fk_lock_edges.test.sql');
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(25);
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
- '62',
436
- '#325 canary: agent_board has 62 base tables — the universe the write scan runs over, pinned so an empty violating set below cannot mean an empty universe');
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 37 agent_board relations are written by an authenticated-executable function, directly or through a callee — five of them (board_invite_redemptions, board_membership_events, callback_outbox_events, delivery_recipients, send_authz_telemetry) were invisible while membership was own-body');
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 61 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');
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 ONE writer derives from raw auth.uid() — self_enroll_human CREATES the identity a session would resolve to, so it cannot use the resolver; scored on the own body because the closure form matches all 59 and would make Half A vacuous');
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 16 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, 2 the platform-admin one; a new ungated writer joins this set, and a member that loses or switches its resolver reds here');
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 37 class relations — so for those two roles every write goes through some agent_board function; WEC-3 is what says which');
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 THIRTEEN of the 37 class relations — the plan asserts this bound holds and it DOES NOT; pinned as an exact set so the hole is bounded and a widening names itself');
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 37 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');
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 19 parents CASCADES or SET NULLs into a class relation — a write path with no privilege check and no function body, so no assertion in this file gates it; one parent is auth.users, i.e. outside agent_board entirely');
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
- '88',
1027
- '#325: 88 writing referential actions (74 CASCADE + 14 SET NULL) reach the 37 class relations — bump this number only after confirming the new edge is intended; see WEC-22, which names parents independently');
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;