@consiliency/agent-board-schema 1.2.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 (92) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +55 -0
  4. package/bin/agent-board-schema.js +97 -0
  5. package/fixtures/already-applied.json +181 -0
  6. package/fixtures/divergent-history.json +173 -0
  7. package/fixtures/production-history.json +173 -0
  8. package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
  9. package/migrations/20260502000000_agent_board_schema.sql +180 -0
  10. package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
  11. package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
  12. package/migrations/20260504000000_agent_board_read_models.sql +206 -0
  13. package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
  14. package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
  15. package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
  16. package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
  17. package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
  18. package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
  19. package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
  20. package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
  21. package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
  22. package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
  23. package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
  24. package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
  25. package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
  26. package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
  27. package/migrations/20260513000001_agent_board_retention.sql +160 -0
  28. package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
  29. package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
  30. package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
  31. package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
  32. package/migrations/20260517000000_agent_board_addressing.sql +542 -0
  33. package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
  34. package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
  35. package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
  36. package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
  37. package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
  38. package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
  39. package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
  40. package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
  41. package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
  42. package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
  43. package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
  44. package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
  45. package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
  46. package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
  47. package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
  48. package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
  49. package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
  50. package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
  51. package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
  52. package/package.json +42 -0
  53. package/provenance.json +580 -0
  54. package/schema-manifest.json +458 -0
  55. package/src/check.js +23 -0
  56. package/src/index.js +50 -0
  57. package/src/materialize.js +73 -0
  58. package/src/plan.js +44 -0
  59. package/src/tooling.test.mjs +97 -0
  60. package/tests/agent_board_addressing.test.sql +166 -0
  61. package/tests/agent_board_auth_rls.test.sql +243 -0
  62. package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
  63. package/tests/agent_board_auth_rpcs.test.sql +311 -0
  64. package/tests/agent_board_auth_schema.test.sql +411 -0
  65. package/tests/agent_board_authbound_boundary.test.sql +148 -0
  66. package/tests/agent_board_boardopt.test.sql +170 -0
  67. package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
  68. package/tests/agent_board_callback_runtime.test.sql +425 -0
  69. package/tests/agent_board_callbackcap.test.sql +222 -0
  70. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
  71. package/tests/agent_board_dblint_rpcs.test.sql +247 -0
  72. package/tests/agent_board_fleet_policy.test.sql +370 -0
  73. package/tests/agent_board_fleet_read_models.test.sql +158 -0
  74. package/tests/agent_board_fleet_registry.test.sql +149 -0
  75. package/tests/agent_board_inbox_reads.test.sql +471 -0
  76. package/tests/agent_board_inbox_rpcs.test.sql +452 -0
  77. package/tests/agent_board_inbox_schema.test.sql +355 -0
  78. package/tests/agent_board_nonfleet_identity.test.sql +26 -0
  79. package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
  80. package/tests/agent_board_schema_version.test.sql +24 -0
  81. package/tests/agent_board_schema_version_rpc.test.sql +40 -0
  82. package/tests/agent_board_signature_rls_reads.test.sql +133 -0
  83. package/tests/agent_board_signed_write_gateway.test.sql +219 -0
  84. package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
  85. package/tests/agent_board_signing_store.test.sql +183 -0
  86. package/tests/agent_board_smoke.test.sql +12 -0
  87. package/tests/agent_board_v1_invariants.test.sql +298 -0
  88. package/tests/agent_board_v2_invariants.test.sql +753 -0
  89. package/tests/agent_board_workload_providers.test.sql +15 -0
  90. package/tests/fixtures/adopter-base.sql +19 -0
  91. package/tests/fixtures/operator-hardening.sql +27 -0
  92. package/tests/fixtures/test-prereqs.sql +84 -0
@@ -0,0 +1,225 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(7);
7
+
8
+ -- Regression test for the ambiguous-column resolution bugs fixed in
9
+ -- 20260514000000_agent_board_auth_rls_id_qualification.sql.
10
+ --
11
+ -- Two scenarios are exercised:
12
+ --
13
+ -- Scenario A (deny -> allow): agent A is a `joined` member of board X (the
14
+ -- "home" board) created by agent B. Agent B is the message author and board
15
+ -- creator. A board-mode delivery_recipients + inbox_items row exists for
16
+ -- agent A on board X. Pre-fix, the messages/boards policies' unqualified `id`
17
+ -- resolved to the inner alias's primary key, so agent A could not read either
18
+ -- the boards row or the messages row (and v_board_feed returned zero rows).
19
+ -- Post-fix, agent A sees all of these rows for board X.
20
+ --
21
+ -- Scenario B (leak -> denied): a second board Y is created by agent B with
22
+ -- a board-mode delivery_recipients + inbox_items row addressed only to agent
23
+ -- B (no membership for agent A). Pre-fix, the unqualified `board_id` in the
24
+ -- EXISTS subquery resolved to bm.board_id (always true for any membership
25
+ -- row), so agent A — by virtue of being a joined member of board X —
26
+ -- could read delivery_recipients and inbox_items rows scoped to board Y.
27
+ -- Post-fix, agent A must not see those cross-board rows.
28
+
29
+ create temp table _rls_idqual_fixture (
30
+ user_a uuid,
31
+ agent_a uuid,
32
+ agent_b uuid,
33
+ board_home uuid,
34
+ board_other uuid,
35
+ message_home uuid,
36
+ inbox_home uuid,
37
+ inbox_other uuid,
38
+ delivery_other uuid
39
+ ) on commit drop;
40
+ grant all on table _rls_idqual_fixture to authenticated, service_role;
41
+
42
+ do $$
43
+ declare
44
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
45
+ v_user_a uuid;
46
+ v_user_b uuid;
47
+ v_agent_a uuid := gen_random_uuid();
48
+ v_agent_b uuid := gen_random_uuid();
49
+ v_thread_home uuid := gen_random_uuid();
50
+ v_thread_other uuid := gen_random_uuid();
51
+ v_board_home uuid := gen_random_uuid();
52
+ v_board_other uuid := gen_random_uuid();
53
+ v_membership_a uuid := gen_random_uuid();
54
+ v_message_home uuid := gen_random_uuid();
55
+ v_message_other uuid := gen_random_uuid();
56
+ v_recipient_home uuid := gen_random_uuid();
57
+ v_recipient_other uuid := gen_random_uuid();
58
+ v_inbox_home uuid := gen_random_uuid();
59
+ v_inbox_other uuid := gen_random_uuid();
60
+ v_membership_b_home uuid := gen_random_uuid();
61
+ v_membership_b_other uuid := gen_random_uuid();
62
+ begin
63
+ v_user_a := public.ensure_github_user(
64
+ 'rls-idqual-a-' || v_suffix,
65
+ 'rls-idqual-a-' || v_suffix || '@example.test',
66
+ 'rls-idqual-a-' || v_suffix,
67
+ 'RLS IdQual Agent A ' || v_suffix
68
+ );
69
+ v_user_b := public.ensure_github_user(
70
+ 'rls-idqual-b-' || v_suffix,
71
+ 'rls-idqual-b-' || v_suffix || '@example.test',
72
+ 'rls-idqual-b-' || v_suffix,
73
+ 'RLS IdQual Agent B ' || v_suffix
74
+ );
75
+
76
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
77
+ values
78
+ (v_agent_a, 'agent', 'agent', 'rls-idqual-a-' || v_suffix, 'IdQual A', 'active', v_user_a, v_user_a),
79
+ (v_agent_b, 'agent', 'agent', 'rls-idqual-b-' || v_suffix, 'IdQual B', 'active', v_user_b, v_user_b);
80
+
81
+ insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
82
+ values
83
+ (v_thread_home, 'coordination', v_agent_b, 'IdQual home thread'),
84
+ (v_thread_other, 'coordination', v_agent_b, 'IdQual other thread');
85
+
86
+ -- "home" board: agent A is a joined member; A is a board-mode recipient.
87
+ insert into agent_board.boards(id, slug, name, created_by_agent_id)
88
+ values (v_board_home, 'rls-idqual-home-' || v_suffix, 'RLS IdQual Home', v_agent_b);
89
+
90
+ insert into agent_board.board_memberships(id, board_id, agent_id, membership_state, created_by_agent_id)
91
+ values
92
+ (v_membership_b_home, v_board_home, v_agent_b, 'joined', v_agent_b),
93
+ (v_membership_a, v_board_home, v_agent_a, 'joined', v_agent_a);
94
+
95
+ insert into agent_board.messages(id, thread_id, author_agent_id, message_sequence, message_type, body)
96
+ values (v_message_home, v_thread_home, v_agent_b, 1, 'comment', 'board post visible to joined member A');
97
+
98
+ insert into agent_board.delivery_recipients(id, delivery_mode, recipient_agent_id, source_kind, source_message_id, board_id, board_membership_id)
99
+ values (v_recipient_home, 'board', v_agent_a, 'message', v_message_home, v_board_home, v_membership_a);
100
+
101
+ insert into agent_board.inbox_items(
102
+ id, delivery_recipient_id, recipient_agent_id, board_id, delivery_mode, source_kind,
103
+ source_message_id, subject, preview, priority, action_state,
104
+ visible_in_inbox, visible_in_board_feed, fetch_descriptor
105
+ )
106
+ values (
107
+ v_inbox_home, v_recipient_home, v_agent_a, v_board_home, 'board', 'message',
108
+ v_message_home, 'IdQual home post', 'preview', 'medium', 'new',
109
+ true, true,
110
+ jsonb_build_object('surface', 'board', 'targetId', v_board_home::text, 'view', 'agent_board.v_board_feed', 'filters', jsonb_build_object(), 'reason', 'rls idqual home')
111
+ );
112
+
113
+ -- "other" board: agent A is NOT a member. Delivery + inbox row are scoped
114
+ -- to agent B as a board-mode recipient. Pre-fix, the unqualified board_id
115
+ -- in the RLS EXISTS subquery would let agent A read these rows via her
116
+ -- membership on board_home. Post-fix, she must be denied.
117
+ insert into agent_board.boards(id, slug, name, created_by_agent_id)
118
+ values (v_board_other, 'rls-idqual-other-' || v_suffix, 'RLS IdQual Other', v_agent_b);
119
+
120
+ insert into agent_board.board_memberships(id, board_id, agent_id, membership_state, created_by_agent_id)
121
+ values (v_membership_b_other, v_board_other, v_agent_b, 'joined', v_agent_b);
122
+
123
+ insert into agent_board.messages(id, thread_id, author_agent_id, message_sequence, message_type, body)
124
+ values (v_message_other, v_thread_other, v_agent_b, 1, 'comment', 'other-board post, off-limits for agent A');
125
+
126
+ insert into agent_board.delivery_recipients(id, delivery_mode, recipient_agent_id, source_kind, source_message_id, board_id, board_membership_id)
127
+ values (v_recipient_other, 'board', v_agent_b, 'message', v_message_other, v_board_other, v_membership_b_other);
128
+
129
+ insert into agent_board.inbox_items(
130
+ id, delivery_recipient_id, recipient_agent_id, board_id, delivery_mode, source_kind,
131
+ source_message_id, subject, preview, priority, action_state,
132
+ visible_in_inbox, visible_in_board_feed, fetch_descriptor
133
+ )
134
+ values (
135
+ v_inbox_other, v_recipient_other, v_agent_b, v_board_other, 'board', 'message',
136
+ v_message_other, 'IdQual other post', 'preview', 'medium', 'new',
137
+ true, true,
138
+ jsonb_build_object('surface', 'board', 'targetId', v_board_other::text, 'view', 'agent_board.v_board_feed', 'filters', jsonb_build_object(), 'reason', 'rls idqual other')
139
+ );
140
+
141
+ insert into _rls_idqual_fixture(user_a, agent_a, agent_b, board_home, board_other, message_home, inbox_home, inbox_other, delivery_other)
142
+ values (v_user_a, v_agent_a, v_agent_b, v_board_home, v_board_other, v_message_home, v_inbox_home, v_inbox_other, v_recipient_other);
143
+ end
144
+ $$;
145
+
146
+ set local role authenticated;
147
+ select set_config('request.jwt.claim.role', 'authenticated', true);
148
+
149
+ do $$
150
+ declare
151
+ v_fixture record;
152
+ v_board_count bigint;
153
+ v_message_count bigint;
154
+ v_feed_count bigint;
155
+ v_leak_delivery bigint;
156
+ v_leak_inbox bigint;
157
+ v_inbox_home bigint;
158
+ begin
159
+ select * into v_fixture from _rls_idqual_fixture;
160
+ perform set_config('request.jwt.claim.sub', v_fixture.user_a::text, true);
161
+
162
+ -- A) Deny-class regression: joined-not-creator agent A must see her own board.
163
+ select count(*) into v_board_count
164
+ from agent_board.boards b
165
+ where b.id = v_fixture.board_home;
166
+ if v_board_count <> 1 then
167
+ raise exception 'joined member A should see home board %, saw % rows', v_fixture.board_home, v_board_count;
168
+ end if;
169
+
170
+ -- B) Deny-class regression: board-delivery recipient A must see the home message.
171
+ select count(*) into v_message_count
172
+ from agent_board.messages m
173
+ where m.id = v_fixture.message_home;
174
+ if v_message_count <> 1 then
175
+ raise exception 'recipient A should see home message %, saw % rows', v_fixture.message_home, v_message_count;
176
+ end if;
177
+
178
+ -- C) Deny-class regression: v_board_feed inner-joins boards and messages.
179
+ select count(*) into v_feed_count
180
+ from agent_board.v_board_feed
181
+ where message_id = v_fixture.message_home;
182
+ if v_feed_count <> 1 then
183
+ raise exception 'v_board_feed should expose home post to A, saw % rows', v_feed_count;
184
+ end if;
185
+
186
+ -- D) Deny-class regression: A's own inbox row on home board must be visible.
187
+ select count(*) into v_inbox_home
188
+ from agent_board.inbox_items i
189
+ where i.id = v_fixture.inbox_home;
190
+ if v_inbox_home <> 1 then
191
+ raise exception 'inbox_items home row should be visible to recipient A, saw % rows', v_inbox_home;
192
+ end if;
193
+
194
+ -- E) Leak-class regression: A must NOT see delivery_recipients rows scoped
195
+ -- to the other board (recipient is agent B; A has no membership on that
196
+ -- board). Pre-fix, the unqualified board_id let A read them.
197
+ select count(*) into v_leak_delivery
198
+ from agent_board.delivery_recipients dr
199
+ where dr.id = v_fixture.delivery_other;
200
+ if v_leak_delivery <> 0 then
201
+ raise exception 'cross-board leak: A should not see delivery_recipients row %, saw % rows', v_fixture.delivery_other, v_leak_delivery;
202
+ end if;
203
+
204
+ -- F) Leak-class regression: A must NOT see inbox_items rows scoped to the
205
+ -- other board (recipient is agent B; A has no membership on that board).
206
+ select count(*) into v_leak_inbox
207
+ from agent_board.inbox_items i
208
+ where i.id = v_fixture.inbox_other;
209
+ if v_leak_inbox <> 0 then
210
+ raise exception 'cross-board leak: A should not see inbox_items row %, saw % rows', v_fixture.inbox_other, v_leak_inbox;
211
+ end if;
212
+ end
213
+ $$;
214
+
215
+ select pass('Joined board member can select boards row via qualified RLS predicate');
216
+ select pass('Board-delivery recipient can select messages row via qualified RLS predicate');
217
+ select pass('v_board_feed exposes the board-delivered post to a joined member after qualification fix');
218
+ select pass('Board-delivery recipient can select own inbox_items row on home board');
219
+ select pass('Cross-board leak closed: delivery_recipients rows scoped to another board are hidden');
220
+ select pass('Cross-board leak closed: inbox_items rows scoped to another board are hidden');
221
+ select pass('All ambiguous-column RLS resolution regressions covered');
222
+
223
+ select finish();
224
+
225
+ rollback;
@@ -0,0 +1,311 @@
1
+ create extension if not exists pgtap with schema extensions;
2
+ set search_path = extensions, public, agent_board;
3
+
4
+ begin;
5
+
6
+ select plan(7);
7
+
8
+ select has_function('agent_board', 'provision_agent', array['text', 'text', 'jsonb', 'uuid'], 'provision_agent exists');
9
+ select has_function('agent_board', 'rotate_agent_token', array['uuid'], 'rotate_agent_token exists');
10
+ select has_function('agent_board', 'revoke_agent', array['uuid', 'text'], 'revoke_agent exists');
11
+
12
+ create temp table _agent_board_authrpc_fixture (
13
+ platform_admin_user_id uuid,
14
+ non_admin_user_id uuid,
15
+ owner_user_id uuid,
16
+ agent_one_handle text,
17
+ agent_two_handle text,
18
+ agent_two_id uuid,
19
+ agent_two_auth_user_id uuid
20
+ ) on commit drop;
21
+
22
+ grant all on table _agent_board_authrpc_fixture to authenticated, service_role;
23
+
24
+ do $$
25
+ declare
26
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
27
+ v_platform_admin_user_id uuid;
28
+ v_non_admin_user_id uuid;
29
+ v_owner_user_id uuid;
30
+ begin
31
+ v_platform_admin_user_id := public.ensure_github_user(
32
+ 'agent-board-authrpc-admin-' || v_suffix,
33
+ 'agent-board-authrpc-admin-' || v_suffix || '@example.test',
34
+ 'agent-board-authrpc-admin-' || v_suffix,
35
+ 'Agent Board Auth RPC Admin ' || v_suffix
36
+ );
37
+ v_non_admin_user_id := public.ensure_github_user(
38
+ 'agent-board-authrpc-nonadmin-' || v_suffix,
39
+ 'agent-board-authrpc-nonadmin-' || v_suffix || '@example.test',
40
+ 'agent-board-authrpc-nonadmin-' || v_suffix,
41
+ 'Agent Board Auth RPC Non Admin ' || v_suffix
42
+ );
43
+ v_owner_user_id := public.ensure_github_user(
44
+ 'agent-board-authrpc-owner-' || v_suffix,
45
+ 'agent-board-authrpc-owner-' || v_suffix || '@example.test',
46
+ 'agent-board-authrpc-owner-' || v_suffix,
47
+ 'Agent Board Auth RPC Owner ' || v_suffix
48
+ );
49
+
50
+ insert into public.user_org_roles(user_id, role)
51
+ values (v_platform_admin_user_id, 'platform_admin')
52
+ on conflict do nothing;
53
+
54
+ insert into _agent_board_authrpc_fixture(
55
+ platform_admin_user_id,
56
+ non_admin_user_id,
57
+ owner_user_id,
58
+ agent_one_handle,
59
+ agent_two_handle
60
+ )
61
+ values (
62
+ v_platform_admin_user_id,
63
+ v_non_admin_user_id,
64
+ v_owner_user_id,
65
+ 'authrpc-one-' || v_suffix,
66
+ 'authrpc-two-' || v_suffix
67
+ );
68
+ end
69
+ $$;
70
+
71
+ set local role authenticated;
72
+ select set_config('request.jwt.claim.role', 'authenticated', true);
73
+
74
+ do $$
75
+ declare
76
+ v_fixture record;
77
+ v_agent_one record;
78
+ v_agent_two record;
79
+ v_thread record;
80
+ v_rotated record;
81
+ v_revoked record;
82
+ v_previous_credential_id uuid;
83
+ begin
84
+ select * into v_fixture from _agent_board_authrpc_fixture;
85
+
86
+ perform set_config('request.jwt.claim.sub', v_fixture.platform_admin_user_id::text, true);
87
+
88
+ select *
89
+ into v_agent_one
90
+ from agent_board.provision_agent(
91
+ v_fixture.agent_one_handle,
92
+ 'agent',
93
+ '["direct","callback"]'::jsonb,
94
+ v_fixture.owner_user_id
95
+ );
96
+
97
+ if v_agent_one.refresh_token is null or length(v_agent_one.refresh_token) < 32 then
98
+ raise exception 'provision_agent did not return a one-time refresh token';
99
+ end if;
100
+
101
+ select *
102
+ into v_agent_two
103
+ from agent_board.provision_agent(
104
+ v_fixture.agent_two_handle,
105
+ 'agent',
106
+ '[]'::jsonb,
107
+ v_fixture.owner_user_id
108
+ );
109
+
110
+ update _agent_board_authrpc_fixture
111
+ set agent_two_id = v_agent_two.agent_id,
112
+ agent_two_auth_user_id = v_agent_two.auth_user_id;
113
+
114
+ perform set_config('request.jwt.claim.sub', v_fixture.non_admin_user_id::text, true);
115
+ begin
116
+ perform *
117
+ from agent_board.provision_agent(
118
+ v_fixture.agent_one_handle || '-blocked',
119
+ 'agent',
120
+ '[]'::jsonb,
121
+ v_fixture.owner_user_id
122
+ );
123
+ raise exception 'non-admin provisioning unexpectedly succeeded';
124
+ exception
125
+ when sqlstate '42501' then null;
126
+ end;
127
+
128
+ perform set_config('request.jwt.claim.sub', v_agent_one.auth_user_id::text, true);
129
+
130
+ if agent_board.current_agent_id() is distinct from v_agent_one.agent_id then
131
+ raise exception 'current_agent_id did not resolve the provisioned agent principal';
132
+ end if;
133
+
134
+ select *
135
+ into v_thread
136
+ from agent_board.create_thread(v_agent_one.agent_id, 'Auth RPC thread');
137
+
138
+ begin
139
+ perform *
140
+ from agent_board.create_thread(v_agent_two.agent_id, 'Impersonation attempt');
141
+ raise exception 'impersonating create_thread unexpectedly succeeded';
142
+ exception
143
+ when sqlstate '42501' then null;
144
+ end;
145
+
146
+ begin
147
+ perform *
148
+ from agent_board.register_callback_endpoint(v_agent_two.agent_id, 'https://example.test/blocked');
149
+ raise exception 'impersonating register_callback_endpoint unexpectedly succeeded';
150
+ exception
151
+ when sqlstate '42501' then null;
152
+ end;
153
+
154
+ perform set_config('request.jwt.claim.sub', v_fixture.platform_admin_user_id::text, true);
155
+
156
+ v_previous_credential_id := v_agent_one.credential_id;
157
+
158
+ select *
159
+ into v_rotated
160
+ from agent_board.rotate_agent_token(v_agent_one.agent_id);
161
+
162
+ if v_rotated.replaces_credential_id is distinct from v_previous_credential_id then
163
+ raise exception 'rotate_agent_token did not record replaces_credential_id';
164
+ end if;
165
+
166
+ select *
167
+ into v_revoked
168
+ from agent_board.revoke_agent(v_agent_one.agent_id, 'test revoke');
169
+
170
+ if v_revoked.status <> 'suspended' or v_revoked.revoked_credential_count < 1 then
171
+ raise exception 'revoke_agent did not suspend the agent or revoke credentials';
172
+ end if;
173
+
174
+ perform set_config('request.jwt.claim.sub', v_agent_one.auth_user_id::text, true);
175
+ if agent_board.current_agent_id() is not null then
176
+ raise exception 'revoked agent principal still resolves through current_agent_id';
177
+ end if;
178
+ end
179
+ $$;
180
+
181
+ select pass('AUTHRPC provisioning, rotation, revocation, and impersonation checks passed');
182
+
183
+ -- portal#203: provision_agent must fail closed for system principals. A system
184
+ -- principal can never bind an auth session (trg_sync_agent_principal_columns
185
+ -- clears auth_user_id; agents_system_owner_shape enforces it), so the RPC must
186
+ -- reject issuance up front and leak neither an agent row nor a credential.
187
+ do $$
188
+ declare
189
+ v_fixture record;
190
+ v_system_handle text;
191
+ v_raised boolean := false;
192
+ v_msg text;
193
+ begin
194
+ select * into v_fixture from _agent_board_authrpc_fixture;
195
+
196
+ perform set_config('request.jwt.claim.sub', v_fixture.platform_admin_user_id::text, true);
197
+
198
+ v_system_handle := v_fixture.agent_one_handle || '-system';
199
+
200
+ begin
201
+ perform *
202
+ from agent_board.provision_agent(
203
+ v_system_handle,
204
+ 'system',
205
+ '[]'::jsonb,
206
+ null
207
+ );
208
+ raise exception 'system provisioning unexpectedly succeeded';
209
+ exception
210
+ when sqlstate '22023' then
211
+ v_raised := true;
212
+ v_msg := sqlerrm;
213
+ end;
214
+
215
+ if not v_raised then
216
+ raise exception 'system provisioning did not fail closed with sqlstate 22023';
217
+ end if;
218
+
219
+ -- 22023 is also raised by the slug and kind-vocabulary guards, so assert the
220
+ -- rejection came from the system-principal branch specifically (guards against
221
+ -- a future refactor passing this test for the wrong reason).
222
+ if v_msg not like '%system principals cannot be issued%' then
223
+ raise exception 'system rejection raised 22023 but with an unexpected message: %', v_msg;
224
+ end if;
225
+
226
+ -- The RPC raises before any insert, so no agent row is created for the handle.
227
+ -- agent_credentials.agent_id is FK -> agents, so the absence of an agent row is
228
+ -- sufficient to prove no credential leaked (checking agent_credentials directly
229
+ -- would require a grant the platform-admin caller role does not hold).
230
+ if exists (
231
+ select 1 from agent_board.agents where handle = v_system_handle
232
+ ) then
233
+ raise exception 'system provisioning leaked an agent row';
234
+ end if;
235
+ end
236
+ $$;
237
+
238
+ select pass('AUTHRPC system provisioning fails closed and leaks no principal or credential');
239
+
240
+ reset role;
241
+ set local role service_role;
242
+ select set_config('request.jwt.claim.role', 'service_role', true);
243
+
244
+ do $$
245
+ declare
246
+ v_fixture record;
247
+ v_endpoint record;
248
+ begin
249
+ select * into v_fixture from _agent_board_authrpc_fixture;
250
+
251
+ select *
252
+ into v_endpoint
253
+ from agent_board.register_callback_endpoint(
254
+ v_fixture.agent_two_id,
255
+ 'https://example.test/service-role'
256
+ );
257
+
258
+ if v_endpoint.owner_agent_id is distinct from v_fixture.agent_two_id then
259
+ raise exception 'service_role callback endpoint registration did not preserve the explicit owner principal';
260
+ end if;
261
+ end
262
+ $$;
263
+
264
+ select pass('AUTHRPC preserved the service_role callback-runtime path');
265
+
266
+ -- portal#203 (defense in depth): rotate_agent_token routes through the same
267
+ -- _issue_agent_credential choke point, so a pre-existing system principal must
268
+ -- also fail closed on rotation. Create one directly (provision_agent now rejects
269
+ -- system) and assert rotation raises and mints nothing. Run as the superuser so
270
+ -- the direct agents insert is permitted; rotate_agent_token still bypasses the
271
+ -- platform-admin gate for a service/superuser caller, so the guard is exercised.
272
+ reset role;
273
+ do $$
274
+ declare
275
+ v_fixture record;
276
+ v_system_handle text;
277
+ v_system_agent_id uuid;
278
+ v_raised boolean := false;
279
+ begin
280
+ select * into v_fixture from _agent_board_authrpc_fixture;
281
+ v_system_handle := v_fixture.agent_one_handle || '-sysrot';
282
+
283
+ insert into agent_board.agents(agent_type, kind, handle, display_name, status)
284
+ values ('system', 'system', v_system_handle, 'System Principal', 'active')
285
+ returning id into v_system_agent_id;
286
+
287
+ begin
288
+ perform * from agent_board.rotate_agent_token(v_system_agent_id);
289
+ raise exception 'rotate_agent_token unexpectedly succeeded for a system principal';
290
+ exception
291
+ when sqlstate '22023' then
292
+ v_raised := true;
293
+ end;
294
+
295
+ if not v_raised then
296
+ raise exception 'rotate_agent_token did not fail closed with sqlstate 22023 for a system principal';
297
+ end if;
298
+
299
+ if exists (
300
+ select 1 from agent_board.agent_credentials where agent_id = v_system_agent_id
301
+ ) then
302
+ raise exception 'rotate_agent_token leaked a credential for a system principal';
303
+ end if;
304
+ end
305
+ $$;
306
+
307
+ select pass('AUTHRPC rotate_agent_token also fails closed for system principals');
308
+
309
+ select finish();
310
+
311
+ rollback;