@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.
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +55 -0
- package/bin/agent-board-schema.js +97 -0
- package/fixtures/already-applied.json +181 -0
- package/fixtures/divergent-history.json +173 -0
- package/fixtures/production-history.json +173 -0
- package/migrations/20260501000001_agent_board_schema_version.sql +12 -0
- package/migrations/20260502000000_agent_board_schema.sql +180 -0
- package/migrations/20260503000000_agent_board_rpcs.sql +422 -0
- package/migrations/20260503000001_agent_board_coordination_rpcs.sql +229 -0
- package/migrations/20260504000000_agent_board_read_models.sql +206 -0
- package/migrations/20260505000000_agent_board_inbox_schema.sql +341 -0
- package/migrations/20260506000000_agent_board_inbox_delivery_rpcs.sql +1200 -0
- package/migrations/20260506000001_agent_board_inbox_callback_rpcs.sql +330 -0
- package/migrations/20260507000000_agent_board_inbox_reads.sql +334 -0
- package/migrations/20260508000000_agent_board_callback_runtime.sql +453 -0
- package/migrations/20260508000001_agent_board_inbox_service_role_reads.sql +10 -0
- package/migrations/20260510000000_agent_board_dblint_repairs.sql +413 -0
- package/migrations/20260511000000_agent_board_auth_schema.sql +146 -0
- package/migrations/20260511000001_agent_board_auth_credentials.sql +39 -0
- package/migrations/20260512000000_agent_board_auth_provisioning_rpcs.sql +622 -0
- package/migrations/20260512000001_agent_board_auth_v1_rpcs.sql +798 -0
- package/migrations/20260512000002_agent_board_auth_v2_rpcs.sql +1243 -0
- package/migrations/20260512000003_agent_board_auth_rls.sql +484 -0
- package/migrations/20260513000000_agent_board_backup_ops.sql +34 -0
- package/migrations/20260513000001_agent_board_retention.sql +160 -0
- package/migrations/20260513000002_agent_board_monitoring_views.sql +227 -0
- package/migrations/20260514000000_agent_board_auth_rls_id_qualification.sql +150 -0
- package/migrations/20260515000000_agent_board_boardopt.sql +493 -0
- package/migrations/20260516000000_agent_board_callbackcap.sql +113 -0
- package/migrations/20260517000000_agent_board_addressing.sql +542 -0
- package/migrations/20260628000000_agent_board_signing_store.sql +174 -0
- package/migrations/20260628000001_agent_board_signing_proof_rpcs.sql +215 -0
- package/migrations/20260628000002_agent_board_signature_rls_reads.sql +300 -0
- package/migrations/20260628000003_agent_board_signed_write_gateway.sql +288 -0
- package/migrations/20260628000004_agent_board_fleet_registry.sql +411 -0
- package/migrations/20260628000005_agent_board_fleet_policy.sql +509 -0
- package/migrations/20260628000006_agent_board_fleet_read_models.sql +170 -0
- package/migrations/20260629000100_agent_board_fleet_registry_repair.sql +411 -0
- package/migrations/20260629000101_agent_board_fleet_policy_refresh.sql +509 -0
- package/migrations/20260629000102_agent_board_fleet_read_models_refresh.sql +170 -0
- package/migrations/20260630000100_agent_board_harness_kind_open_vocab.sql +15 -0
- package/migrations/20260710000000_agent_board_authbound_boundary.sql +155 -0
- package/migrations/20260710000100_agent_board_schema_version_rpc.sql +14 -0
- package/migrations/20260710000400_agent_board_nonfleet_identity.sql +87 -0
- package/migrations/20260710000401_agent_board_workload_providers.sql +39 -0
- package/migrations/20260711000000_agent_board_provision_system_fail_closed.sql +242 -0
- package/migrations/20260711000100_canary_signed_fleet_enforcement.sql +234 -0
- package/migrations/20260712000000_agent_board_bundle_endpoint_compat.sql +97 -0
- package/migrations/20260713000000_agent_board_rls_reassert_board_scoping.sql +90 -0
- package/package.json +42 -0
- package/provenance.json +580 -0
- package/schema-manifest.json +458 -0
- package/src/check.js +23 -0
- package/src/index.js +50 -0
- package/src/materialize.js +73 -0
- package/src/plan.js +44 -0
- package/src/tooling.test.mjs +97 -0
- package/tests/agent_board_addressing.test.sql +166 -0
- package/tests/agent_board_auth_rls.test.sql +243 -0
- package/tests/agent_board_auth_rls_id_qualification.test.sql +225 -0
- package/tests/agent_board_auth_rpcs.test.sql +311 -0
- package/tests/agent_board_auth_schema.test.sql +411 -0
- package/tests/agent_board_authbound_boundary.test.sql +148 -0
- package/tests/agent_board_boardopt.test.sql +170 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +49 -0
- package/tests/agent_board_callback_runtime.test.sql +425 -0
- package/tests/agent_board_callbackcap.test.sql +222 -0
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +60 -0
- package/tests/agent_board_dblint_rpcs.test.sql +247 -0
- package/tests/agent_board_fleet_policy.test.sql +370 -0
- package/tests/agent_board_fleet_read_models.test.sql +158 -0
- package/tests/agent_board_fleet_registry.test.sql +149 -0
- package/tests/agent_board_inbox_reads.test.sql +471 -0
- package/tests/agent_board_inbox_rpcs.test.sql +452 -0
- package/tests/agent_board_inbox_schema.test.sql +355 -0
- package/tests/agent_board_nonfleet_identity.test.sql +26 -0
- package/tests/agent_board_rls_reassert_board_scoping.test.sql +39 -0
- package/tests/agent_board_schema_version.test.sql +24 -0
- package/tests/agent_board_schema_version_rpc.test.sql +40 -0
- package/tests/agent_board_signature_rls_reads.test.sql +133 -0
- package/tests/agent_board_signed_write_gateway.test.sql +219 -0
- package/tests/agent_board_signing_proof_rpcs.test.sql +143 -0
- package/tests/agent_board_signing_store.test.sql +183 -0
- package/tests/agent_board_smoke.test.sql +12 -0
- package/tests/agent_board_v1_invariants.test.sql +298 -0
- package/tests/agent_board_v2_invariants.test.sql +753 -0
- package/tests/agent_board_workload_providers.test.sql +15 -0
- package/tests/fixtures/adopter-base.sql +19 -0
- package/tests/fixtures/operator-hardening.sql +27 -0
- 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;
|