@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,411 @@
|
|
|
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(20);
|
|
7
|
+
|
|
8
|
+
select has_column('agent_board', 'agents', 'auth_user_id', 'agents exposes auth_user_id');
|
|
9
|
+
select has_column('agent_board', 'agents', 'owner_user_id', 'agents exposes owner_user_id');
|
|
10
|
+
select col_type_is('agent_board', 'agents', 'kind', 'agent_board.agent_kind', 'agents.kind uses the frozen enum type');
|
|
11
|
+
|
|
12
|
+
select has_table('agent_board', 'agent_credentials', 'agent_credentials table exists');
|
|
13
|
+
select has_column('agent_board', 'agent_credentials', 'refresh_token_hash', 'agent_credentials stores hashed refresh-token references');
|
|
14
|
+
select has_column('agent_board', 'agent_credentials', 'hash_version', 'agent_credentials stores hash version metadata');
|
|
15
|
+
select has_column('agent_board', 'agent_credentials', 'credential_label', 'agent_credentials stores a human-readable label');
|
|
16
|
+
select has_column('agent_board', 'agent_credentials', 'expires_at', 'agent_credentials stores expiry metadata');
|
|
17
|
+
select has_column('agent_board', 'agent_credentials', 'last_used_at', 'agent_credentials stores last-used metadata');
|
|
18
|
+
select has_column('agent_board', 'agent_credentials', 'replaced_by_credential_id', 'agent_credentials stores replacement lineage');
|
|
19
|
+
select has_column('agent_board', 'agent_credentials', 'replaces_credential_id', 'agent_credentials stores reverse rotation lineage');
|
|
20
|
+
|
|
21
|
+
select has_function('agent_board', 'current_agent_id', array[]::text[], 'current_agent_id resolver exists');
|
|
22
|
+
|
|
23
|
+
select ok(
|
|
24
|
+
(
|
|
25
|
+
select p.prosecdef and p.provolatile = 's'
|
|
26
|
+
from pg_proc p
|
|
27
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
28
|
+
where n.nspname = 'agent_board'
|
|
29
|
+
and p.proname = 'current_agent_id'
|
|
30
|
+
),
|
|
31
|
+
'current_agent_id is security definer and stable'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
select ok(
|
|
35
|
+
(
|
|
36
|
+
select count(distinct grantee) = 2
|
|
37
|
+
from information_schema.routine_privileges
|
|
38
|
+
where specific_schema = 'agent_board'
|
|
39
|
+
and routine_name = 'current_agent_id'
|
|
40
|
+
and privilege_type = 'EXECUTE'
|
|
41
|
+
and grantee in ('authenticated', 'service_role')
|
|
42
|
+
),
|
|
43
|
+
'current_agent_id is executable by authenticated and service_role'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
do $$
|
|
47
|
+
declare
|
|
48
|
+
v_owner_user uuid;
|
|
49
|
+
v_rotated_user uuid;
|
|
50
|
+
v_duplicate_user uuid;
|
|
51
|
+
v_human_agent uuid;
|
|
52
|
+
v_agent_principal uuid;
|
|
53
|
+
v_thread uuid := gen_random_uuid();
|
|
54
|
+
v_task uuid;
|
|
55
|
+
v_message uuid;
|
|
56
|
+
v_conflict uuid;
|
|
57
|
+
v_escalation uuid;
|
|
58
|
+
v_delivery_recipient uuid;
|
|
59
|
+
v_inbox_item uuid;
|
|
60
|
+
v_endpoint uuid;
|
|
61
|
+
v_subscription uuid;
|
|
62
|
+
v_credential_old uuid;
|
|
63
|
+
v_credential_new uuid;
|
|
64
|
+
begin
|
|
65
|
+
v_owner_user := public.ensure_github_user(
|
|
66
|
+
'agent-board-auth-owner',
|
|
67
|
+
'agent-board-auth-owner@example.test',
|
|
68
|
+
'agent-board-auth-owner',
|
|
69
|
+
'Agent Board Auth Owner'
|
|
70
|
+
);
|
|
71
|
+
v_rotated_user := public.ensure_github_user(
|
|
72
|
+
'agent-board-auth-rotated',
|
|
73
|
+
'agent-board-auth-rotated@example.test',
|
|
74
|
+
'agent-board-auth-rotated',
|
|
75
|
+
'Agent Board Auth Rotated'
|
|
76
|
+
);
|
|
77
|
+
v_duplicate_user := public.ensure_github_user(
|
|
78
|
+
'agent-board-auth-duplicate',
|
|
79
|
+
'agent-board-auth-duplicate@example.test',
|
|
80
|
+
'agent-board-auth-duplicate',
|
|
81
|
+
'Agent Board Auth Duplicate'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, owner_user_id, auth_user_id)
|
|
85
|
+
values (
|
|
86
|
+
gen_random_uuid(),
|
|
87
|
+
'human',
|
|
88
|
+
'test_authschema_human',
|
|
89
|
+
'Auth Human',
|
|
90
|
+
v_owner_user,
|
|
91
|
+
v_owner_user
|
|
92
|
+
)
|
|
93
|
+
returning id into v_human_agent;
|
|
94
|
+
|
|
95
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, owner_user_id)
|
|
96
|
+
values (
|
|
97
|
+
gen_random_uuid(),
|
|
98
|
+
'agent',
|
|
99
|
+
'test_authschema_agent',
|
|
100
|
+
'Auth Agent',
|
|
101
|
+
v_owner_user
|
|
102
|
+
)
|
|
103
|
+
returning id into v_agent_principal;
|
|
104
|
+
|
|
105
|
+
if (select kind::text from agent_board.agents where id = v_human_agent) is distinct from 'human' then
|
|
106
|
+
raise exception 'legacy human insert did not backfill kind';
|
|
107
|
+
end if;
|
|
108
|
+
|
|
109
|
+
if (select kind::text from agent_board.agents where id = v_agent_principal) is distinct from 'agent' then
|
|
110
|
+
raise exception 'legacy agent insert did not backfill kind';
|
|
111
|
+
end if;
|
|
112
|
+
|
|
113
|
+
update agent_board.agents
|
|
114
|
+
set agent_type = 'human'
|
|
115
|
+
where id = v_agent_principal;
|
|
116
|
+
|
|
117
|
+
if (select kind::text from agent_board.agents where id = v_agent_principal) is distinct from 'human' then
|
|
118
|
+
raise exception 'agent_type update did not keep kind in sync';
|
|
119
|
+
end if;
|
|
120
|
+
|
|
121
|
+
update agent_board.agents
|
|
122
|
+
set auth_user_id = null
|
|
123
|
+
where id = v_agent_principal;
|
|
124
|
+
|
|
125
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
126
|
+
values (v_thread, 'coordination', v_human_agent, 'Auth schema thread');
|
|
127
|
+
|
|
128
|
+
select task_id
|
|
129
|
+
into v_task
|
|
130
|
+
from agent_board.create_task(
|
|
131
|
+
v_thread,
|
|
132
|
+
v_human_agent,
|
|
133
|
+
'Auth schema task',
|
|
134
|
+
'Auth schema task details',
|
|
135
|
+
v_human_agent,
|
|
136
|
+
null,
|
|
137
|
+
'high',
|
|
138
|
+
'distributed',
|
|
139
|
+
'{}'::jsonb
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
insert into agent_board.messages(id, thread_id, author_agent_id, task_id, message_sequence, message_type, body)
|
|
143
|
+
values (gen_random_uuid(), v_thread, v_human_agent, v_task, 1, 'comment', 'Auth schema message')
|
|
144
|
+
returning id into v_message;
|
|
145
|
+
|
|
146
|
+
insert into agent_board.claims(task_id, thread_id, agent_id, resource_kind, resource_key, lease_expires_at)
|
|
147
|
+
values (v_task, v_thread, v_human_agent, 'repo_file', 'authschema.sql', now() + interval '10 minutes');
|
|
148
|
+
|
|
149
|
+
insert into agent_board.conflicts(thread_id, task_id, raised_by_agent_id, summary)
|
|
150
|
+
values (v_thread, v_task, v_human_agent, 'Auth conflict')
|
|
151
|
+
returning id into v_conflict;
|
|
152
|
+
|
|
153
|
+
insert into agent_board.escalations(thread_id, task_id, raised_by_agent_id, summary)
|
|
154
|
+
values (v_thread, v_task, v_human_agent, 'Auth escalation')
|
|
155
|
+
returning id into v_escalation;
|
|
156
|
+
|
|
157
|
+
insert into agent_board.delivery_recipients(delivery_mode, recipient_agent_id, source_kind, source_message_id)
|
|
158
|
+
values ('direct', v_human_agent, 'message', v_message)
|
|
159
|
+
returning id into v_delivery_recipient;
|
|
160
|
+
|
|
161
|
+
insert into agent_board.inbox_items(
|
|
162
|
+
delivery_recipient_id,
|
|
163
|
+
recipient_agent_id,
|
|
164
|
+
delivery_mode,
|
|
165
|
+
source_kind,
|
|
166
|
+
source_message_id,
|
|
167
|
+
subject,
|
|
168
|
+
preview,
|
|
169
|
+
priority,
|
|
170
|
+
action_state,
|
|
171
|
+
visible_in_inbox,
|
|
172
|
+
visible_in_board_feed,
|
|
173
|
+
fetch_descriptor
|
|
174
|
+
)
|
|
175
|
+
values (
|
|
176
|
+
v_delivery_recipient,
|
|
177
|
+
v_human_agent,
|
|
178
|
+
'direct',
|
|
179
|
+
'message',
|
|
180
|
+
v_message,
|
|
181
|
+
'Auth schema subject',
|
|
182
|
+
'Auth schema preview',
|
|
183
|
+
'high',
|
|
184
|
+
'new',
|
|
185
|
+
true,
|
|
186
|
+
false,
|
|
187
|
+
jsonb_build_object('surface', 'thread', 'targetId', v_thread::text, 'view', 'agent_board.v_thread_feed', 'filters', jsonb_build_object(), 'reason', 'auth schema')
|
|
188
|
+
)
|
|
189
|
+
returning id into v_inbox_item;
|
|
190
|
+
|
|
191
|
+
insert into agent_board.callback_endpoints(owner_agent_id, endpoint_url, endpoint_status, verification_requested_at, verified_at)
|
|
192
|
+
values (v_human_agent, 'https://example.test/authschema', 'verified', now(), now())
|
|
193
|
+
returning id into v_endpoint;
|
|
194
|
+
|
|
195
|
+
insert into agent_board.callback_subscriptions(endpoint_id, agent_id, subscription_scope, event_category, subscription_status)
|
|
196
|
+
values (v_endpoint, v_human_agent, 'own_inbox', 'inbox.item.delivered', 'active')
|
|
197
|
+
returning id into v_subscription;
|
|
198
|
+
|
|
199
|
+
insert into agent_board.agent_credentials(
|
|
200
|
+
agent_id,
|
|
201
|
+
refresh_token_hash,
|
|
202
|
+
hash_version,
|
|
203
|
+
credential_label,
|
|
204
|
+
expires_at
|
|
205
|
+
)
|
|
206
|
+
values (
|
|
207
|
+
v_human_agent,
|
|
208
|
+
decode(repeat('01', 32), 'hex'),
|
|
209
|
+
1,
|
|
210
|
+
'primary',
|
|
211
|
+
now() + interval '30 days'
|
|
212
|
+
)
|
|
213
|
+
returning id into v_credential_old;
|
|
214
|
+
|
|
215
|
+
insert into agent_board.agent_credentials(
|
|
216
|
+
agent_id,
|
|
217
|
+
refresh_token_hash,
|
|
218
|
+
hash_version,
|
|
219
|
+
credential_label,
|
|
220
|
+
expires_at,
|
|
221
|
+
replaces_credential_id
|
|
222
|
+
)
|
|
223
|
+
values (
|
|
224
|
+
v_human_agent,
|
|
225
|
+
decode(repeat('02', 32), 'hex'),
|
|
226
|
+
1,
|
|
227
|
+
'rotated',
|
|
228
|
+
now() + interval '60 days',
|
|
229
|
+
v_credential_old
|
|
230
|
+
)
|
|
231
|
+
returning id into v_credential_new;
|
|
232
|
+
|
|
233
|
+
update agent_board.agent_credentials
|
|
234
|
+
set replaced_by_credential_id = v_credential_new,
|
|
235
|
+
revoked_at = now(),
|
|
236
|
+
rotated_at = now()
|
|
237
|
+
where id = v_credential_old;
|
|
238
|
+
|
|
239
|
+
perform set_config('request.jwt.claim.sub', gen_random_uuid()::text, true);
|
|
240
|
+
if agent_board.current_agent_id() is not null then
|
|
241
|
+
raise exception 'unknown JWT unexpectedly resolved an agent principal';
|
|
242
|
+
end if;
|
|
243
|
+
|
|
244
|
+
perform set_config('request.jwt.claim.sub', v_owner_user::text, true);
|
|
245
|
+
if agent_board.current_agent_id() is distinct from v_human_agent then
|
|
246
|
+
raise exception 'known JWT did not resolve the expected agent principal';
|
|
247
|
+
end if;
|
|
248
|
+
|
|
249
|
+
update agent_board.agents
|
|
250
|
+
set auth_user_id = v_rotated_user
|
|
251
|
+
where id = v_human_agent;
|
|
252
|
+
|
|
253
|
+
perform set_config('request.jwt.claim.sub', v_owner_user::text, true);
|
|
254
|
+
if agent_board.current_agent_id() is not null then
|
|
255
|
+
raise exception 'rotated auth principal still resolved from the old user id';
|
|
256
|
+
end if;
|
|
257
|
+
|
|
258
|
+
perform set_config('request.jwt.claim.sub', v_rotated_user::text, true);
|
|
259
|
+
if agent_board.current_agent_id() is distinct from v_human_agent then
|
|
260
|
+
raise exception 'rotated auth principal did not resolve from the new user id';
|
|
261
|
+
end if;
|
|
262
|
+
|
|
263
|
+
if (select count(*) from agent_board.messages where author_agent_id = v_human_agent) <> 1 then
|
|
264
|
+
raise exception 'message history was orphaned by auth rotation';
|
|
265
|
+
end if;
|
|
266
|
+
|
|
267
|
+
if (select count(*) from agent_board.tasks where created_by_agent_id = v_human_agent or assigned_agent_id = v_human_agent) <> 1 then
|
|
268
|
+
raise exception 'task history was orphaned by auth rotation';
|
|
269
|
+
end if;
|
|
270
|
+
|
|
271
|
+
if (select count(*) from agent_board.claims where agent_id = v_human_agent) <> 1 then
|
|
272
|
+
raise exception 'claim history was orphaned by auth rotation';
|
|
273
|
+
end if;
|
|
274
|
+
|
|
275
|
+
if (select count(*) from agent_board.conflicts where id = v_conflict and raised_by_agent_id = v_human_agent) <> 1 then
|
|
276
|
+
raise exception 'conflict history was orphaned by auth rotation';
|
|
277
|
+
end if;
|
|
278
|
+
|
|
279
|
+
if (select count(*) from agent_board.escalations where id = v_escalation and raised_by_agent_id = v_human_agent) <> 1 then
|
|
280
|
+
raise exception 'escalation history was orphaned by auth rotation';
|
|
281
|
+
end if;
|
|
282
|
+
|
|
283
|
+
if (select count(*) from agent_board.inbox_items where id = v_inbox_item and recipient_agent_id = v_human_agent) <> 1 then
|
|
284
|
+
raise exception 'inbox history was orphaned by auth rotation';
|
|
285
|
+
end if;
|
|
286
|
+
|
|
287
|
+
if (select count(*) from agent_board.callback_endpoints where id = v_endpoint and owner_agent_id = v_human_agent) <> 1 then
|
|
288
|
+
raise exception 'callback endpoint history was orphaned by auth rotation';
|
|
289
|
+
end if;
|
|
290
|
+
|
|
291
|
+
if (select count(*) from agent_board.callback_subscriptions where id = v_subscription and agent_id = v_human_agent) <> 1 then
|
|
292
|
+
raise exception 'callback subscription history was orphaned by auth rotation';
|
|
293
|
+
end if;
|
|
294
|
+
|
|
295
|
+
begin
|
|
296
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, owner_user_id, auth_user_id)
|
|
297
|
+
values (
|
|
298
|
+
gen_random_uuid(),
|
|
299
|
+
'human',
|
|
300
|
+
'test_authschema_duplicate_auth',
|
|
301
|
+
'Duplicate Auth User',
|
|
302
|
+
v_duplicate_user,
|
|
303
|
+
v_rotated_user
|
|
304
|
+
);
|
|
305
|
+
raise exception 'duplicate active auth_user_id insert unexpectedly succeeded';
|
|
306
|
+
exception
|
|
307
|
+
when unique_violation then
|
|
308
|
+
null;
|
|
309
|
+
end;
|
|
310
|
+
|
|
311
|
+
begin
|
|
312
|
+
insert into agent_board.agent_credentials(
|
|
313
|
+
agent_id,
|
|
314
|
+
refresh_token_hash,
|
|
315
|
+
hash_version,
|
|
316
|
+
credential_label,
|
|
317
|
+
expires_at
|
|
318
|
+
)
|
|
319
|
+
values (
|
|
320
|
+
v_human_agent,
|
|
321
|
+
decode(repeat('02', 32), 'hex'),
|
|
322
|
+
1,
|
|
323
|
+
'duplicate-hash',
|
|
324
|
+
now() + interval '1 day'
|
|
325
|
+
);
|
|
326
|
+
raise exception 'duplicate refresh_token_hash unexpectedly succeeded';
|
|
327
|
+
exception
|
|
328
|
+
when unique_violation then
|
|
329
|
+
null;
|
|
330
|
+
end;
|
|
331
|
+
end
|
|
332
|
+
$$;
|
|
333
|
+
|
|
334
|
+
select ok(
|
|
335
|
+
exists (
|
|
336
|
+
select 1
|
|
337
|
+
from pg_indexes
|
|
338
|
+
where schemaname = 'agent_board'
|
|
339
|
+
and tablename = 'agents'
|
|
340
|
+
and indexname = 'idx_agent_board_agents_auth_user_id_active_unique'
|
|
341
|
+
),
|
|
342
|
+
'agents has a single-active-principal unique index on auth_user_id'
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
select ok(
|
|
346
|
+
exists (
|
|
347
|
+
select 1
|
|
348
|
+
from pg_indexes
|
|
349
|
+
where schemaname = 'agent_board'
|
|
350
|
+
and tablename = 'agent_credentials'
|
|
351
|
+
and indexname = 'idx_agent_board_agent_credentials_refresh_token_hash'
|
|
352
|
+
),
|
|
353
|
+
'agent_credentials has a unique index on refresh_token_hash'
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
select is(
|
|
357
|
+
(
|
|
358
|
+
select kind::text
|
|
359
|
+
from agent_board.agents
|
|
360
|
+
where handle = 'test_authschema_human'
|
|
361
|
+
),
|
|
362
|
+
'human',
|
|
363
|
+
'legacy agent_type writers continue to produce a valid kind value'
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
select is(
|
|
367
|
+
(
|
|
368
|
+
select count(*)::int
|
|
369
|
+
from agent_board.agents
|
|
370
|
+
where auth_user_id = (
|
|
371
|
+
select auth_user_id
|
|
372
|
+
from agent_board.agents
|
|
373
|
+
where handle = 'test_authschema_human'
|
|
374
|
+
)
|
|
375
|
+
and status = 'active'
|
|
376
|
+
),
|
|
377
|
+
1,
|
|
378
|
+
'one auth_user_id resolves to at most one active agent principal'
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
select is(
|
|
382
|
+
(
|
|
383
|
+
select count(*)::int
|
|
384
|
+
from agent_board.agent_credentials
|
|
385
|
+
where agent_id = (
|
|
386
|
+
select id
|
|
387
|
+
from agent_board.agents
|
|
388
|
+
where handle = 'test_authschema_human'
|
|
389
|
+
)
|
|
390
|
+
and replaced_by_credential_id is not null
|
|
391
|
+
and revoked_at is not null
|
|
392
|
+
),
|
|
393
|
+
1,
|
|
394
|
+
'credential rotation preserves durable replacement lineage'
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
select throws_ok(
|
|
398
|
+
$$insert into agent_board.agent_credentials(agent_id, refresh_token_hash, hash_version, credential_label)
|
|
399
|
+
values (
|
|
400
|
+
(select id from agent_board.agents where handle = 'test_authschema_human'),
|
|
401
|
+
decode(repeat('03', 32), 'hex'),
|
|
402
|
+
1,
|
|
403
|
+
'missing-expiry'
|
|
404
|
+
)$$,
|
|
405
|
+
'23502',
|
|
406
|
+
null,
|
|
407
|
+
'agent_credentials requires expiry metadata'
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
select finish();
|
|
411
|
+
rollback;
|
|
@@ -0,0 +1,148 @@
|
|
|
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(10);
|
|
7
|
+
|
|
8
|
+
select has_function('agent_board', 'current_agent_id', array[]::text[], 'current_agent_id exists');
|
|
9
|
+
select has_function('agent_board', '_signed_write_actor_id', array['jsonb', 'jsonb', 'text'], 'AUTHBOUND actor assertion helper exists');
|
|
10
|
+
select table_privs_are('agent_board', 'agent_credentials', 'service_role', array['SELECT'], 'service_role can perform server-only credential exchange lookup');
|
|
11
|
+
select ok(has_column_privilege('service_role', 'agent_board.agent_credentials', 'last_used_at', 'UPDATE'), 'service_role can update credential last_used_at');
|
|
12
|
+
select ok(has_column_privilege('service_role', 'agent_board.agent_credentials', 'updated_at', 'UPDATE'), 'service_role can update credential updated_at');
|
|
13
|
+
|
|
14
|
+
create temp table _agent_board_authbound_fixture (
|
|
15
|
+
user_id uuid,
|
|
16
|
+
agent_id uuid,
|
|
17
|
+
credential_id uuid,
|
|
18
|
+
signing_key_id uuid
|
|
19
|
+
) on commit drop;
|
|
20
|
+
|
|
21
|
+
grant all on table _agent_board_authbound_fixture to authenticated, service_role;
|
|
22
|
+
|
|
23
|
+
do $$
|
|
24
|
+
declare
|
|
25
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
26
|
+
v_user uuid;
|
|
27
|
+
v_agent uuid := gen_random_uuid();
|
|
28
|
+
v_credential uuid := gen_random_uuid();
|
|
29
|
+
v_key uuid;
|
|
30
|
+
begin
|
|
31
|
+
v_user := public.ensure_github_user(
|
|
32
|
+
'agent-board-authbound-' || v_suffix,
|
|
33
|
+
'agent-board-authbound-' || v_suffix || '@example.test',
|
|
34
|
+
'agent-board-authbound-' || v_suffix,
|
|
35
|
+
'Agent Board AUTHBOUND ' || v_suffix
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
|
|
39
|
+
values (v_agent, 'agent', 'agent', 'authbound-' || v_suffix, 'AUTHBOUND Agent', 'active', v_user, v_user);
|
|
40
|
+
|
|
41
|
+
insert into agent_board.agent_credentials(id, agent_id, refresh_token_hash, hash_version, credential_label, expires_at)
|
|
42
|
+
values (v_credential, v_agent, extensions.digest('authbound-refresh-' || v_suffix, 'sha256'), 1, 'authbound', now() + interval '1 day');
|
|
43
|
+
|
|
44
|
+
insert into agent_board.agent_signing_keys(agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
45
|
+
values (v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url', 'sha256:' || repeat('A', 43), 'active', 'proven')
|
|
46
|
+
returning id into v_key;
|
|
47
|
+
|
|
48
|
+
insert into _agent_board_authbound_fixture(user_id, agent_id, credential_id, signing_key_id)
|
|
49
|
+
values (v_user, v_agent, v_credential, v_key);
|
|
50
|
+
end
|
|
51
|
+
$$;
|
|
52
|
+
|
|
53
|
+
set local role authenticated;
|
|
54
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
55
|
+
|
|
56
|
+
do $$
|
|
57
|
+
declare
|
|
58
|
+
v_fixture record;
|
|
59
|
+
begin
|
|
60
|
+
select * into v_fixture from _agent_board_authbound_fixture;
|
|
61
|
+
perform set_config('request.jwt.claim.sub', v_fixture.user_id::text, true);
|
|
62
|
+
perform set_config('request.jwt.claim.agent_credential_id', v_fixture.credential_id::text, true);
|
|
63
|
+
|
|
64
|
+
if agent_board.current_agent_id() is distinct from v_fixture.agent_id then
|
|
65
|
+
raise exception 'credential-bound current_agent_id did not resolve active agent';
|
|
66
|
+
end if;
|
|
67
|
+
end
|
|
68
|
+
$$;
|
|
69
|
+
|
|
70
|
+
reset role;
|
|
71
|
+
|
|
72
|
+
do $$
|
|
73
|
+
declare
|
|
74
|
+
v_fixture record;
|
|
75
|
+
begin
|
|
76
|
+
select * into v_fixture from _agent_board_authbound_fixture;
|
|
77
|
+
update agent_board.agent_credentials
|
|
78
|
+
set revoked_at = now()
|
|
79
|
+
where id = v_fixture.credential_id;
|
|
80
|
+
end
|
|
81
|
+
$$;
|
|
82
|
+
|
|
83
|
+
set local role authenticated;
|
|
84
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
85
|
+
|
|
86
|
+
do $$
|
|
87
|
+
declare
|
|
88
|
+
v_fixture record;
|
|
89
|
+
begin
|
|
90
|
+
select * into v_fixture from _agent_board_authbound_fixture;
|
|
91
|
+
perform set_config('request.jwt.claim.sub', v_fixture.user_id::text, true);
|
|
92
|
+
perform set_config('request.jwt.claim.agent_credential_id', v_fixture.credential_id::text, true);
|
|
93
|
+
if agent_board.current_agent_id() is not null then
|
|
94
|
+
raise exception 'revoked credential still resolved before JWT expiry';
|
|
95
|
+
end if;
|
|
96
|
+
end
|
|
97
|
+
$$;
|
|
98
|
+
|
|
99
|
+
select pass('current_agent_id honors agent_credential_id revocation before JWT expiry');
|
|
100
|
+
|
|
101
|
+
select throws_like(
|
|
102
|
+
$$select agent_board._signed_write_actor_id(
|
|
103
|
+
'{}'::jsonb,
|
|
104
|
+
'{}'::jsonb,
|
|
105
|
+
'p_actor_agent_id'
|
|
106
|
+
)$$,
|
|
107
|
+
'%actor_mismatch%',
|
|
108
|
+
'missing gateway actor fails closed'
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
select throws_like(
|
|
112
|
+
$$select agent_board._signed_write_actor_id(
|
|
113
|
+
jsonb_build_object('p_actor_agent_id', (select agent_id::text from _agent_board_authbound_fixture)),
|
|
114
|
+
jsonb_build_object('agent_id', gen_random_uuid()::text),
|
|
115
|
+
'p_actor_agent_id'
|
|
116
|
+
)$$,
|
|
117
|
+
'%actor_mismatch%',
|
|
118
|
+
'payload actor assertion mismatch fails closed'
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
select ok(
|
|
122
|
+
exists (
|
|
123
|
+
select 1
|
|
124
|
+
from pg_proc p
|
|
125
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
126
|
+
where n.nspname = 'agent_board'
|
|
127
|
+
and p.proname = '_record_signed_write_evidence'
|
|
128
|
+
and pg_get_functiondef(p.oid) like '%authbound%'
|
|
129
|
+
and pg_get_functiondef(p.oid) like '%metadata-safe%'
|
|
130
|
+
),
|
|
131
|
+
'signed-write evidence records AUTHBOUND metadata-safe posture'
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
select ok(
|
|
135
|
+
exists (
|
|
136
|
+
select 1
|
|
137
|
+
from pg_proc p
|
|
138
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
139
|
+
where n.nspname = 'agent_board'
|
|
140
|
+
and p.proname = 'current_agent_id'
|
|
141
|
+
and pg_get_functiondef(p.oid) like '%agent_credential_id%'
|
|
142
|
+
),
|
|
143
|
+
'current_agent_id records AUTHBOUND credential-claim boundary'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
select finish();
|
|
147
|
+
|
|
148
|
+
rollback;
|