@consiliency/agent-board-schema 1.3.0 → 1.5.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/bin/agent-board-schema.js +0 -0
- package/fixtures/already-applied.json +112 -0
- package/migrations/20260717000100_agent_board_board_role.sql +180 -0
- package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
- package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
- package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
- package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
- package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
- package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
- package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
- package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
- package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
- package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
- package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
- package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
- package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
- package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
- package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
- package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
- package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
- package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
- package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
- package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
- package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
- package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
- package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
- package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
- package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
- package/migrations/20260723000100_agent_board_roomown_owner_seed.sql +167 -0
- package/migrations/20260723000200_agent_board_roomown_grant_role_live.sql +72 -0
- package/package.json +9 -9
- package/schema-manifest.json +256 -4
- package/src/tooling.test.mjs +46 -1
- package/tests/agent_board_agent_lineage.test.sql +73 -0
- package/tests/agent_board_agents_status_revoked.test.sql +60 -0
- package/tests/agent_board_auth_rpcs.test.sql +17 -16
- package/tests/agent_board_board_role.test.sql +129 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
- package/tests/agent_board_delegation_enforcement.test.sql +175 -0
- package/tests/agent_board_delegation_policies.test.sql +120 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
- package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
- package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
- package/tests/agent_board_enrollment_audit_content.test.sql +139 -0
- package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
- package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
- package/tests/agent_board_fleet_registry.test.sql +2 -2
- package/tests/agent_board_identity_providers.test.sql +104 -0
- package/tests/agent_board_inbox_rpcs.test.sql +10 -10
- package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
- package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +134 -0
- package/tests/agent_board_resigning_refresh.test.sql +73 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
- package/tests/agent_board_roomown.test.sql +233 -0
- package/tests/agent_board_schema_version.test.sql +2 -2
- package/tests/agent_board_schema_version_rpc.test.sql +1 -1
- package/tests/agent_board_scoped_send_regrant.test.sql +142 -0
- package/tests/agent_board_send_authz_config.test.sql +84 -0
- package/tests/agent_board_send_authz_matrix.test.sql +162 -0
- package/tests/agent_board_send_role_gate.test.sql +181 -0
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
- package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
- package/tests/agent_board_thread_acl.test.sql +129 -0
- package/tests/agent_board_v2_invariants.test.sql +5 -5
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
-- MBAUTHZ SL-3 (Lane C) pgTAP: agents.parent_agent_id / root_agent_id and the
|
|
2
|
+
-- owner-consistency trigger.
|
|
3
|
+
create extension if not exists pgtap with schema extensions;
|
|
4
|
+
set search_path = extensions, public, agent_board;
|
|
5
|
+
|
|
6
|
+
begin;
|
|
7
|
+
|
|
8
|
+
select plan(8);
|
|
9
|
+
|
|
10
|
+
create temp table _lineage_fixture (
|
|
11
|
+
root_agent uuid,
|
|
12
|
+
root_user uuid,
|
|
13
|
+
other_user uuid
|
|
14
|
+
) on commit drop;
|
|
15
|
+
|
|
16
|
+
do $$
|
|
17
|
+
declare
|
|
18
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
19
|
+
v_root_user uuid;
|
|
20
|
+
v_other_user uuid;
|
|
21
|
+
v_root uuid := gen_random_uuid();
|
|
22
|
+
begin
|
|
23
|
+
v_root_user := public.ensure_github_user('lin-root-' || v_suffix, 'lin-root-' || v_suffix || '@example.test', 'lin-root-' || v_suffix, 'LIN Root');
|
|
24
|
+
v_other_user := public.ensure_github_user('lin-other-' || v_suffix, 'lin-other-' || v_suffix || '@example.test', 'lin-other-' || v_suffix, 'LIN Other');
|
|
25
|
+
|
|
26
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
27
|
+
values (v_root, 'agent', 'lin-root-' || v_suffix, 'LIN Root', 'active', v_root_user, v_root_user);
|
|
28
|
+
|
|
29
|
+
insert into _lineage_fixture values (v_root, v_root_user, v_other_user);
|
|
30
|
+
end
|
|
31
|
+
$$;
|
|
32
|
+
|
|
33
|
+
-- Structure -----------------------------------------------------------------
|
|
34
|
+
select has_column('agent_board', 'agents', 'parent_agent_id', 'agents has parent_agent_id');
|
|
35
|
+
select has_column('agent_board', 'agents', 'root_agent_id', 'agents has root_agent_id');
|
|
36
|
+
select col_type_is('agent_board', 'agents', 'parent_agent_id', 'uuid', 'parent_agent_id is uuid');
|
|
37
|
+
select fk_ok('agent_board', 'agents', 'parent_agent_id', 'agent_board', 'agents', 'id',
|
|
38
|
+
'parent_agent_id references agents(id)');
|
|
39
|
+
select fk_ok('agent_board', 'agents', 'root_agent_id', 'agent_board', 'agents', 'id',
|
|
40
|
+
'root_agent_id references agents(id)');
|
|
41
|
+
|
|
42
|
+
-- Owner-consistency trigger -------------------------------------------------
|
|
43
|
+
-- Child whose owner_user_id differs from its root's owner is rejected.
|
|
44
|
+
select throws_ok(
|
|
45
|
+
(select format(
|
|
46
|
+
$q$insert into agent_board.agents(id, agent_type, handle, display_name, status, owner_user_id, parent_agent_id, root_agent_id)
|
|
47
|
+
values (gen_random_uuid(), 'agent', %L, 'LIN Bad Child', 'active', %L::uuid, %L::uuid, %L::uuid)$q$,
|
|
48
|
+
'lin-badchild-' || gen_random_uuid()::text, other_user, root_agent, root_agent)
|
|
49
|
+
from _lineage_fixture),
|
|
50
|
+
'23514', null,
|
|
51
|
+
'trigger rejects a parented agent whose owner differs from its root');
|
|
52
|
+
|
|
53
|
+
-- Child whose owner matches its root's owner is allowed.
|
|
54
|
+
select lives_ok(
|
|
55
|
+
(select format(
|
|
56
|
+
$q$insert into agent_board.agents(id, agent_type, handle, display_name, status, owner_user_id, parent_agent_id, root_agent_id)
|
|
57
|
+
values (gen_random_uuid(), 'agent', %L, 'LIN Good Child', 'active', %L::uuid, %L::uuid, %L::uuid)$q$,
|
|
58
|
+
'lin-goodchild-' || gen_random_uuid()::text, root_user, root_agent, root_agent)
|
|
59
|
+
from _lineage_fixture),
|
|
60
|
+
'trigger allows a parented agent whose owner matches its root');
|
|
61
|
+
|
|
62
|
+
-- Unparented agent (NULL lineage) is unaffected regardless of owner.
|
|
63
|
+
select lives_ok(
|
|
64
|
+
(select format(
|
|
65
|
+
$q$insert into agent_board.agents(id, agent_type, handle, display_name, status, owner_user_id)
|
|
66
|
+
values (gen_random_uuid(), 'agent', %L, 'LIN Orphan', 'active', %L::uuid)$q$,
|
|
67
|
+
'lin-orphan-' || gen_random_uuid()::text, other_user)
|
|
68
|
+
from _lineage_fixture),
|
|
69
|
+
'unparented agent with NULL lineage is unaffected by the trigger');
|
|
70
|
+
|
|
71
|
+
select finish();
|
|
72
|
+
|
|
73
|
+
rollback;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
-- MBDELEG SL-1 pgTAP (IF-0-MBDELEG-1): agents.status admits 'revoked'.
|
|
2
|
+
--
|
|
3
|
+
-- Proves the CHECK-extension at 20260718000900 accepts the 4th literal 'revoked'
|
|
4
|
+
-- alongside the 3 legacy literals, rejects an unknown literal, and that a write of
|
|
5
|
+
-- status='revoked' (as the cascade performs) no longer throws check_violation.
|
|
6
|
+
--
|
|
7
|
+
-- Runs as postgres (service_role-equivalent).
|
|
8
|
+
create extension if not exists pgtap with schema extensions;
|
|
9
|
+
set search_path = extensions, public, agent_board;
|
|
10
|
+
|
|
11
|
+
begin;
|
|
12
|
+
|
|
13
|
+
select plan(6);
|
|
14
|
+
|
|
15
|
+
create temp table _srv_fx (a_upd uuid) on commit drop;
|
|
16
|
+
|
|
17
|
+
do $$
|
|
18
|
+
declare
|
|
19
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
20
|
+
v_upd uuid := gen_random_uuid();
|
|
21
|
+
begin
|
|
22
|
+
-- Seed one agent to update to 'revoked' below.
|
|
23
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
24
|
+
values (v_upd, 'agent', 'srv-upd-'||s, 'SRV-UPD', 'active');
|
|
25
|
+
insert into _srv_fx values (v_upd);
|
|
26
|
+
end
|
|
27
|
+
$$;
|
|
28
|
+
|
|
29
|
+
-- Insert-side acceptance of each literal (distinct handles).
|
|
30
|
+
select lives_ok(
|
|
31
|
+
$q$insert into agent_board.agents(agent_type, handle, display_name, status)
|
|
32
|
+
values ('agent', 'srv-active-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), 'A', 'active')$q$,
|
|
33
|
+
'agents.status accepts legacy literal ''active''');
|
|
34
|
+
select lives_ok(
|
|
35
|
+
$q$insert into agent_board.agents(agent_type, handle, display_name, status)
|
|
36
|
+
values ('agent', 'srv-inactive-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), 'I', 'inactive')$q$,
|
|
37
|
+
'agents.status accepts legacy literal ''inactive''');
|
|
38
|
+
select lives_ok(
|
|
39
|
+
$q$insert into agent_board.agents(agent_type, handle, display_name, status)
|
|
40
|
+
values ('agent', 'srv-suspended-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), 'S', 'suspended')$q$,
|
|
41
|
+
'agents.status accepts legacy literal ''suspended''');
|
|
42
|
+
select lives_ok(
|
|
43
|
+
$q$insert into agent_board.agents(agent_type, handle, display_name, status)
|
|
44
|
+
values ('agent', 'srv-revoked-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), 'R', 'revoked')$q$,
|
|
45
|
+
'agents.status accepts the new literal ''revoked''');
|
|
46
|
+
|
|
47
|
+
-- An unknown literal is rejected by agents_status_check.
|
|
48
|
+
select throws_ok(
|
|
49
|
+
$q$insert into agent_board.agents(agent_type, handle, display_name, status)
|
|
50
|
+
values ('agent', 'srv-bogus-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), 'B', 'bogus')$q$,
|
|
51
|
+
'23514', null, 'agents.status rejects an unknown literal (check_violation)');
|
|
52
|
+
|
|
53
|
+
-- A subtree write of status='revoked' (what the cascade performs) succeeds.
|
|
54
|
+
select lives_ok(
|
|
55
|
+
(select format($q$update agent_board.agents set status = 'revoked' where id = %L::uuid$q$, a_upd) from _srv_fx),
|
|
56
|
+
'updating an existing agent to status=''revoked'' no longer throws check_violation');
|
|
57
|
+
|
|
58
|
+
select finish();
|
|
59
|
+
|
|
60
|
+
rollback;
|
|
@@ -155,13 +155,15 @@ begin
|
|
|
155
155
|
|
|
156
156
|
v_previous_credential_id := v_agent_one.credential_id;
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
raise exception 'rotate_agent_token
|
|
164
|
-
|
|
158
|
+
-- MBDELEG (20260718001200): the 30-day refresh-token exchange is retired.
|
|
159
|
+
-- rotate_agent_token now raises 0A000; session refresh is re-signing
|
|
160
|
+
-- (PoP -> mint_fleet_agent_session), not a refresh-token rotation.
|
|
161
|
+
begin
|
|
162
|
+
perform * from agent_board.rotate_agent_token(v_agent_one.agent_id);
|
|
163
|
+
raise exception 'rotate_agent_token unexpectedly succeeded (should be deprecated)';
|
|
164
|
+
exception
|
|
165
|
+
when sqlstate '0A000' then null;
|
|
166
|
+
end;
|
|
165
167
|
|
|
166
168
|
select *
|
|
167
169
|
into v_revoked
|
|
@@ -263,12 +265,11 @@ $$;
|
|
|
263
265
|
|
|
264
266
|
select pass('AUTHRPC preserved the service_role callback-runtime path');
|
|
265
267
|
|
|
266
|
-
-- portal#203 (defense in depth): rotate_agent_token
|
|
267
|
-
--
|
|
268
|
-
--
|
|
269
|
-
-- system
|
|
270
|
-
--
|
|
271
|
-
-- platform-admin gate for a service/superuser caller, so the guard is exercised.
|
|
268
|
+
-- portal#203 + MBDELEG (20260718001200, defense in depth): rotate_agent_token is
|
|
269
|
+
-- now deprecated and raises 0A000 for ALL callers (session refresh is re-signing),
|
|
270
|
+
-- which subsumes the earlier system-principal fail-closed path. A pre-existing
|
|
271
|
+
-- system principal must still get no credential from rotation. Create one directly
|
|
272
|
+
-- (provision_agent rejects system) and assert rotation raises 0A000 and mints nothing.
|
|
272
273
|
reset role;
|
|
273
274
|
do $$
|
|
274
275
|
declare
|
|
@@ -288,12 +289,12 @@ begin
|
|
|
288
289
|
perform * from agent_board.rotate_agent_token(v_system_agent_id);
|
|
289
290
|
raise exception 'rotate_agent_token unexpectedly succeeded for a system principal';
|
|
290
291
|
exception
|
|
291
|
-
when sqlstate '
|
|
292
|
+
when sqlstate '0A000' then
|
|
292
293
|
v_raised := true;
|
|
293
294
|
end;
|
|
294
295
|
|
|
295
296
|
if not v_raised then
|
|
296
|
-
raise exception 'rotate_agent_token did not fail closed with sqlstate
|
|
297
|
+
raise exception 'rotate_agent_token did not fail closed with sqlstate 0A000 (deprecated) for a system principal';
|
|
297
298
|
end if;
|
|
298
299
|
|
|
299
300
|
if exists (
|
|
@@ -304,7 +305,7 @@ begin
|
|
|
304
305
|
end
|
|
305
306
|
$$;
|
|
306
307
|
|
|
307
|
-
select pass('AUTHRPC rotate_agent_token
|
|
308
|
+
select pass('AUTHRPC rotate_agent_token is deprecated (0A000) and mints no credential');
|
|
308
309
|
|
|
309
310
|
select finish();
|
|
310
311
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
-- MBAUTHZ SL-1 (Lane A) pgTAP: board_memberships.role, boards.role_gate_enforced,
|
|
2
|
+
-- and the role gate on invite_board_member/resolve_board_request.
|
|
3
|
+
create extension if not exists pgtap with schema extensions;
|
|
4
|
+
set search_path = extensions, public, agent_board;
|
|
5
|
+
|
|
6
|
+
begin;
|
|
7
|
+
|
|
8
|
+
select plan(13);
|
|
9
|
+
|
|
10
|
+
create temp table _board_role_fixture (
|
|
11
|
+
board uuid,
|
|
12
|
+
creator_agent uuid,
|
|
13
|
+
member_agent uuid,
|
|
14
|
+
admin_agent uuid,
|
|
15
|
+
target_agent uuid,
|
|
16
|
+
other_agent uuid,
|
|
17
|
+
creator_user uuid,
|
|
18
|
+
member_user uuid,
|
|
19
|
+
admin_user uuid
|
|
20
|
+
) on commit drop;
|
|
21
|
+
|
|
22
|
+
do $$
|
|
23
|
+
declare
|
|
24
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
25
|
+
v_creator_user uuid;
|
|
26
|
+
v_member_user uuid;
|
|
27
|
+
v_admin_user uuid;
|
|
28
|
+
v_creator uuid := gen_random_uuid();
|
|
29
|
+
v_member uuid := gen_random_uuid();
|
|
30
|
+
v_admin uuid := gen_random_uuid();
|
|
31
|
+
v_target uuid := gen_random_uuid();
|
|
32
|
+
v_other uuid := gen_random_uuid();
|
|
33
|
+
v_board uuid;
|
|
34
|
+
begin
|
|
35
|
+
v_creator_user := public.ensure_github_user('br-creator-' || v_suffix, 'br-creator-' || v_suffix || '@example.test', 'br-creator-' || v_suffix, 'BR Creator');
|
|
36
|
+
v_member_user := public.ensure_github_user('br-member-' || v_suffix, 'br-member-' || v_suffix || '@example.test', 'br-member-' || v_suffix, 'BR Member');
|
|
37
|
+
v_admin_user := public.ensure_github_user('br-admin-' || v_suffix, 'br-admin-' || v_suffix || '@example.test', 'br-admin-' || v_suffix, 'BR Admin');
|
|
38
|
+
|
|
39
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
40
|
+
values
|
|
41
|
+
(v_creator, 'agent', 'br-creator-' || v_suffix, 'BR Creator', 'active', v_creator_user, v_creator_user),
|
|
42
|
+
(v_member, 'agent', 'br-member-' || v_suffix, 'BR Member', 'active', v_member_user, v_member_user),
|
|
43
|
+
(v_admin, 'agent', 'br-admin-' || v_suffix, 'BR Admin', 'active', v_admin_user, v_admin_user),
|
|
44
|
+
(v_target, 'agent', 'br-target-' || v_suffix, 'BR Target', 'active', null, null),
|
|
45
|
+
(v_other, 'agent', 'br-other-' || v_suffix, 'BR Other', 'active', null, null);
|
|
46
|
+
|
|
47
|
+
select board_id into v_board
|
|
48
|
+
from agent_board.create_board(v_creator, 'br-board-' || v_suffix, 'BR Board', 'board role test', '{}'::jsonb, 'private');
|
|
49
|
+
|
|
50
|
+
-- The creator's owner membership is now seeded by create_board (ROOMOWN). The
|
|
51
|
+
-- `other` membership is inserted with the column DEFAULT (member) to exercise the
|
|
52
|
+
-- one-time backfill; member/admin get explicit roles for gate tests.
|
|
53
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id)
|
|
54
|
+
values
|
|
55
|
+
(v_board, v_other, 'joined', v_creator);
|
|
56
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
57
|
+
values
|
|
58
|
+
(v_board, v_member, 'joined', v_creator, 'member'),
|
|
59
|
+
(v_board, v_admin, 'joined', v_creator, 'admin');
|
|
60
|
+
|
|
61
|
+
insert into _board_role_fixture values
|
|
62
|
+
(v_board, v_creator, v_member, v_admin, v_target, v_other, v_creator_user, v_member_user, v_admin_user);
|
|
63
|
+
end
|
|
64
|
+
$$;
|
|
65
|
+
|
|
66
|
+
-- Structure -----------------------------------------------------------------
|
|
67
|
+
select has_column('agent_board', 'board_memberships', 'role', 'board_memberships has role column');
|
|
68
|
+
select col_type_is('agent_board', 'board_memberships', 'role', 'text', 'role is text');
|
|
69
|
+
select col_has_check('agent_board', 'board_memberships', 'role', 'role is check-constrained');
|
|
70
|
+
select has_column('agent_board', 'boards', 'role_gate_enforced', 'boards has role_gate_enforced flag');
|
|
71
|
+
select col_type_is('agent_board', 'boards', 'role_gate_enforced', 'boolean', 'role_gate_enforced is boolean');
|
|
72
|
+
|
|
73
|
+
-- Default behavior (member membership above was inserted without role) -------
|
|
74
|
+
select is(
|
|
75
|
+
(select role from agent_board.board_memberships bm join _board_role_fixture f on bm.agent_id = f.member_agent and bm.board_id = f.board),
|
|
76
|
+
'member', 'explicit member role persists');
|
|
77
|
+
|
|
78
|
+
-- One-time backfill: creator membership -> owner; other membership -> member.
|
|
79
|
+
update agent_board.board_memberships bm
|
|
80
|
+
set role = 'owner'
|
|
81
|
+
from agent_board.boards b
|
|
82
|
+
where bm.board_id = b.id
|
|
83
|
+
and bm.agent_id = b.created_by_agent_id
|
|
84
|
+
and bm.role <> 'owner';
|
|
85
|
+
|
|
86
|
+
select is(
|
|
87
|
+
(select role from agent_board.board_memberships bm join _board_role_fixture f on bm.agent_id = f.creator_agent and bm.board_id = f.board),
|
|
88
|
+
'owner', 'backfill sets creator membership role to owner');
|
|
89
|
+
select is(
|
|
90
|
+
(select role from agent_board.board_memberships bm join _board_role_fixture f on bm.agent_id = f.other_agent and bm.board_id = f.board),
|
|
91
|
+
'member', 'backfill leaves non-creator membership as member');
|
|
92
|
+
|
|
93
|
+
-- Gate: flag=false (default) preserves any-active-member behavior ------------
|
|
94
|
+
-- Non-creator member (role member) may invite when the flag is off.
|
|
95
|
+
select set_config('request.jwt.claim.sub', (select member_user from _board_role_fixture)::text, true);
|
|
96
|
+
select lives_ok(
|
|
97
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid, '{}'::jsonb)$q$, board, target_agent, creator_agent) from _board_role_fixture),
|
|
98
|
+
'flag off: active member (role member) may invite');
|
|
99
|
+
|
|
100
|
+
-- Service_role / null actor (no agent maps to this sub) bypasses.
|
|
101
|
+
select set_config('request.jwt.claim.sub', gen_random_uuid()::text, true);
|
|
102
|
+
select lives_ok(
|
|
103
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid, '{}'::jsonb)$q$, board, target_agent, creator_agent) from _board_role_fixture),
|
|
104
|
+
'flag off: service_role/null actor may invite');
|
|
105
|
+
|
|
106
|
+
-- Gate: flag=true requires owner/admin ---------------------------------------
|
|
107
|
+
update agent_board.boards set role_gate_enforced = true
|
|
108
|
+
where id = (select board from _board_role_fixture);
|
|
109
|
+
|
|
110
|
+
select set_config('request.jwt.claim.sub', (select member_user from _board_role_fixture)::text, true);
|
|
111
|
+
select throws_ok(
|
|
112
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid, '{}'::jsonb)$q$, board, target_agent, creator_agent) from _board_role_fixture),
|
|
113
|
+
'42501', null,
|
|
114
|
+
'flag on: role member actor is denied invite');
|
|
115
|
+
|
|
116
|
+
select set_config('request.jwt.claim.sub', (select admin_user from _board_role_fixture)::text, true);
|
|
117
|
+
select lives_ok(
|
|
118
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid, '{}'::jsonb)$q$, board, target_agent, creator_agent) from _board_role_fixture),
|
|
119
|
+
'flag on: role admin actor may invite');
|
|
120
|
+
|
|
121
|
+
-- Service_role / null actor still bypasses even with the flag on.
|
|
122
|
+
select set_config('request.jwt.claim.sub', gen_random_uuid()::text, true);
|
|
123
|
+
select lives_ok(
|
|
124
|
+
(select format($q$select agent_board.invite_board_member(%L::uuid, %L::uuid, %L::uuid, '{}'::jsonb)$q$, board, target_agent, creator_agent) from _board_role_fixture),
|
|
125
|
+
'flag on: service_role/null actor still bypasses');
|
|
126
|
+
|
|
127
|
+
select finish();
|
|
128
|
+
|
|
129
|
+
rollback;
|
|
@@ -22,15 +22,27 @@ select ok(has_function_privilege('service_role',
|
|
|
22
22
|
-- before any install: not ready, schema version still reported
|
|
23
23
|
select is((agent_board.get_bundle_compatibility() ->> 'ready'), 'false',
|
|
24
24
|
'not ready before materializer records an install');
|
|
25
|
-
select is((agent_board.get_bundle_compatibility() ->> 'schema_version'), '
|
|
26
|
-
'compatibility reports schema version
|
|
25
|
+
select is((agent_board.get_bundle_compatibility() ->> 'schema_version'), '8',
|
|
26
|
+
'compatibility reports schema version 8');
|
|
27
27
|
|
|
28
|
-
--
|
|
28
|
+
-- Post-MBSESSION the 8 core sends are re-granted to authenticated, so the LIVE
|
|
29
|
+
-- baseline posture is signed_write_enforced=false. Exercise BOTH branches in this
|
|
30
|
+
-- rolled-back txn. Enforced branch: revoke EXECUTE from authenticated on ALL
|
|
31
|
+
-- eight gated-write overloads (the probe checks by routine name, so any single
|
|
32
|
+
-- remaining overload grant keeps enforcement false) -> reports active=true.
|
|
33
|
+
revoke execute on function agent_board.send_direct_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
|
|
34
|
+
revoke execute on function agent_board.send_direct_message(uuid, uuid, jsonb, text, text, text, text, text, text, jsonb) from authenticated;
|
|
35
|
+
revoke execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) from authenticated;
|
|
36
|
+
revoke execute on function agent_board.send_multicast_message(uuid, uuid, jsonb, text, text, text, text, text, text, jsonb) from authenticated;
|
|
37
|
+
revoke execute on function agent_board.post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
|
|
38
|
+
revoke execute on function agent_board.post_board_message(uuid, uuid, text, text, text, text, text, text, text, jsonb) from authenticated;
|
|
39
|
+
revoke execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) from authenticated;
|
|
40
|
+
revoke execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, jsonb, text, text, text, text, text, jsonb) from authenticated;
|
|
29
41
|
select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'true',
|
|
30
|
-
'signed-write enforcement
|
|
42
|
+
'signed-write enforcement reports active when all gated write RPCs are revoked from authenticated');
|
|
31
43
|
|
|
32
|
-
--
|
|
33
|
-
--
|
|
44
|
+
-- Re-grant branch: re-granting EXECUTE on any one gated write RPC (as MBSESSION
|
|
45
|
+
-- does at 20260718000800) flips enforcement back to false.
|
|
34
46
|
grant execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) to authenticated;
|
|
35
47
|
select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'false',
|
|
36
48
|
'enforcement flips to false when any gated write RPC is re-granted to authenticated');
|
|
@@ -14,8 +14,8 @@ select function_privs_are(
|
|
|
14
14
|
'send_direct_message',
|
|
15
15
|
array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
|
|
16
16
|
'authenticated',
|
|
17
|
-
array[]
|
|
18
|
-
'authenticated
|
|
17
|
+
array['EXECUTE'],
|
|
18
|
+
'authenticated may execute direct scoped send on the MBSESSION re-granted path'
|
|
19
19
|
);
|
|
20
20
|
|
|
21
21
|
select function_privs_are(
|
|
@@ -23,8 +23,8 @@ select function_privs_are(
|
|
|
23
23
|
'send_multicast_message',
|
|
24
24
|
array['uuid','uuid','uuid[]','text','text','text','text','text','jsonb'],
|
|
25
25
|
'authenticated',
|
|
26
|
-
array[]
|
|
27
|
-
'authenticated
|
|
26
|
+
array['EXECUTE'],
|
|
27
|
+
'authenticated may execute multicast scoped send on the MBSESSION re-granted path'
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
select function_privs_are(
|
|
@@ -32,8 +32,8 @@ select function_privs_are(
|
|
|
32
32
|
'post_board_message',
|
|
33
33
|
array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
|
|
34
34
|
'authenticated',
|
|
35
|
-
array[]
|
|
36
|
-
'authenticated
|
|
35
|
+
array['EXECUTE'],
|
|
36
|
+
'authenticated may execute board scoped send on the MBSESSION re-granted path'
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
select function_privs_are(
|
|
@@ -41,8 +41,8 @@ select function_privs_are(
|
|
|
41
41
|
'create_tasking_delivery',
|
|
42
42
|
array['uuid','uuid','text','text','uuid[]','uuid','text','text','text','jsonb'],
|
|
43
43
|
'authenticated',
|
|
44
|
-
array[]
|
|
45
|
-
'authenticated
|
|
44
|
+
array['EXECUTE'],
|
|
45
|
+
'authenticated may execute tasking scoped send on the MBSESSION re-granted path'
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
select ok(
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
-- MBDELEG SL-1 pgTAP (IF-0-MBDELEG-2): bounded root-minted delegation spinoff.
|
|
2
|
+
--
|
|
3
|
+
-- Proves mint_delegated_child mints ONLY under the full bound set (mode=root_minted,
|
|
4
|
+
-- policy unrevoked, root principal active, requested ⊆ allowed_scopes, true lineage
|
|
5
|
+
-- root, max_active_children) and rejects each violation with no child/grant; that a
|
|
6
|
+
-- minted child gets a revocable ephemeral signing key; and that renew_delegated_grant
|
|
7
|
+
-- extends while the root is active but raises once the root is revoked/inactive (a
|
|
8
|
+
-- grant cannot outlive the root's authority).
|
|
9
|
+
--
|
|
10
|
+
-- Runs as postgres (service_role-equivalent).
|
|
11
|
+
create extension if not exists pgtap with schema extensions;
|
|
12
|
+
set search_path = extensions, public, agent_board;
|
|
13
|
+
|
|
14
|
+
begin;
|
|
15
|
+
|
|
16
|
+
select plan(18);
|
|
17
|
+
|
|
18
|
+
create temp table _deleg_fx (
|
|
19
|
+
owner_id uuid, r1 uuid, r2 uuid, r3 uuid,
|
|
20
|
+
child_a uuid, child_b uuid, exp_a timestamptz,
|
|
21
|
+
slug_c text, ca_pubkey text
|
|
22
|
+
) on commit drop;
|
|
23
|
+
|
|
24
|
+
do $$
|
|
25
|
+
declare
|
|
26
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
27
|
+
u_owner uuid; u_r1 uuid; u_r2 uuid; u_r3 uuid;
|
|
28
|
+
v_r1 uuid := gen_random_uuid(); v_r2 uuid := gen_random_uuid(); v_r3 uuid := gen_random_uuid();
|
|
29
|
+
v_pol1 uuid := gen_random_uuid(); v_pol2 uuid := gen_random_uuid(); v_pol3 uuid := gen_random_uuid();
|
|
30
|
+
v_ca uuid; v_cb uuid; v_exp_a timestamptz;
|
|
31
|
+
-- Real (random) 43-char base64url Ed25519 public keys, generated caller-side.
|
|
32
|
+
v_ca_pubkey text := rtrim(translate(encode(extensions.gen_random_bytes(32), 'base64'), '+/', '-_'), '=');
|
|
33
|
+
v_cb_pubkey text := rtrim(translate(encode(extensions.gen_random_bytes(32), 'base64'), '+/', '-_'), '=');
|
|
34
|
+
begin
|
|
35
|
+
u_owner := public.ensure_github_user('deleg-own-'||s, 'deleg-own-'||s||'@example.test', 'deleg-own-'||s, 'OWN');
|
|
36
|
+
u_r1 := public.ensure_github_user('deleg-r1-'||s, 'deleg-r1-'||s||'@example.test', 'deleg-r1-'||s, 'R1');
|
|
37
|
+
u_r2 := public.ensure_github_user('deleg-r2-'||s, 'deleg-r2-'||s||'@example.test', 'deleg-r2-'||s, 'R2');
|
|
38
|
+
u_r3 := public.ensure_github_user('deleg-r3-'||s, 'deleg-r3-'||s||'@example.test', 'deleg-r3-'||s, 'R3');
|
|
39
|
+
|
|
40
|
+
-- Root R1: root_minted policy, allowed_scopes=[a,b], ceiling 8h, max 2 children.
|
|
41
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
42
|
+
values (v_r1, 'agent', 'deleg-r1-'||s, 'R1', 'active', u_r1, u_owner);
|
|
43
|
+
insert into agent_board.delegation_policies(id, root_agent_id, mode, max_depth, max_active_children, allowed_scopes, child_ttl_ceiling, require_child_signing_key)
|
|
44
|
+
values (v_pol1, v_r1, 'root_minted', 1, 2, jsonb_build_array('scope:a', 'scope:b'), interval '8 hours', true);
|
|
45
|
+
|
|
46
|
+
-- Root R2: independent_auth policy (opts out of root-minted spinoff).
|
|
47
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
48
|
+
values (v_r2, 'agent', 'deleg-r2-'||s, 'R2', 'active', u_r2, u_owner);
|
|
49
|
+
insert into agent_board.delegation_policies(id, root_agent_id, mode, allowed_scopes, child_ttl_ceiling)
|
|
50
|
+
values (v_pol2, v_r2, 'independent_auth', jsonb_build_array('scope:a'), interval '8 hours');
|
|
51
|
+
|
|
52
|
+
-- Root R3: root_minted, uncapped, require_child_signing_key -> used for the
|
|
53
|
+
-- malformed-public-key rejection (reaches the key-enrollment step).
|
|
54
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
55
|
+
values (v_r3, 'agent', 'deleg-r3-'||s, 'R3', 'active', u_r3, u_owner);
|
|
56
|
+
insert into agent_board.delegation_policies(id, root_agent_id, mode, allowed_scopes, child_ttl_ceiling, require_child_signing_key)
|
|
57
|
+
values (v_pol3, v_r3, 'root_minted', jsonb_build_array('scope:a'), interval '8 hours', true);
|
|
58
|
+
|
|
59
|
+
-- Happy child-a (scope:a), enrolling the caller-supplied public key. Grant #1.
|
|
60
|
+
select child_agent_id, grant_expires_at into v_ca, v_exp_a
|
|
61
|
+
from agent_board.mint_delegated_child(v_r1, 'deleg-ca-'||s, jsonb_build_array('scope:a'), v_ca_pubkey);
|
|
62
|
+
|
|
63
|
+
-- Give child-a its own root_minted policy so the no-transitive (max_depth) guard is
|
|
64
|
+
-- exercised directly (parent_agent_id is not null), not merely the missing-policy path.
|
|
65
|
+
insert into agent_board.delegation_policies(id, root_agent_id, mode, allowed_scopes, child_ttl_ceiling)
|
|
66
|
+
values (gen_random_uuid(), v_ca, 'root_minted', jsonb_build_array('scope:a'), interval '8 hours');
|
|
67
|
+
|
|
68
|
+
-- ttl-cap child-b: request 30 days, must be capped at the 8h ceiling. Grant #2.
|
|
69
|
+
select child_agent_id into v_cb
|
|
70
|
+
from agent_board.mint_delegated_child(v_r1, 'deleg-cb-'||s, jsonb_build_array('scope:a'), v_cb_pubkey, interval '30 days');
|
|
71
|
+
|
|
72
|
+
insert into _deleg_fx values (u_owner, v_r1, v_r2, v_r3, v_ca, v_cb, v_exp_a, 'deleg-cc-'||s, v_ca_pubkey);
|
|
73
|
+
end
|
|
74
|
+
$$;
|
|
75
|
+
|
|
76
|
+
-- Function existence.
|
|
77
|
+
select has_function('agent_board', 'mint_delegated_child', array['uuid','text','jsonb','text','interval'], 'mint_delegated_child exists');
|
|
78
|
+
select has_function('agent_board', 'renew_delegated_grant', array['uuid','interval'], 'renew_delegated_grant exists');
|
|
79
|
+
|
|
80
|
+
-- Happy mint: grant row shape.
|
|
81
|
+
select is(
|
|
82
|
+
(select count(*)::int from agent_board.delegation_grants g
|
|
83
|
+
where g.child_agent_id = (select child_a from _deleg_fx)
|
|
84
|
+
and g.root_agent_id = (select r1 from _deleg_fx)
|
|
85
|
+
and g.parent_agent_id = (select r1 from _deleg_fx)
|
|
86
|
+
and g.revoked_at is null),
|
|
87
|
+
1, 'happy mint inserted exactly one live grant (root=parent=R1) for child-a');
|
|
88
|
+
|
|
89
|
+
-- Happy mint: child provisioned lineage-parented under the root, same owner, active.
|
|
90
|
+
select is(
|
|
91
|
+
(select a.parent_agent_id = (select r1 from _deleg_fx)
|
|
92
|
+
and a.root_agent_id = (select r1 from _deleg_fx)
|
|
93
|
+
and a.owner_user_id = (select owner_id from _deleg_fx)
|
|
94
|
+
and a.status = 'active'
|
|
95
|
+
from agent_board.agents a where a.id = (select child_a from _deleg_fx)),
|
|
96
|
+
true, 'minted child is parented under the root, shares owner_user_id, and is active');
|
|
97
|
+
|
|
98
|
+
-- Happy mint: revocable ephemeral signing key enrolled for the child.
|
|
99
|
+
select is(
|
|
100
|
+
(select count(*)::int from agent_board.agent_signing_keys k
|
|
101
|
+
where k.agent_id = (select child_a from _deleg_fx)
|
|
102
|
+
and k.key_id = 'delegated-ephemeral'
|
|
103
|
+
and k.status = 'active' and k.revoked_at is null),
|
|
104
|
+
1, 'minted child got exactly one active revocable ephemeral signing key');
|
|
105
|
+
|
|
106
|
+
-- #5: the enrolled key is the EXACT caller-supplied public key (no placeholder).
|
|
107
|
+
select is(
|
|
108
|
+
(select public_key from agent_board.agent_signing_keys k
|
|
109
|
+
where k.agent_id = (select child_a from _deleg_fx) and k.key_id = 'delegated-ephemeral'),
|
|
110
|
+
(select ca_pubkey from _deleg_fx),
|
|
111
|
+
'minted child enrolls the caller-supplied Ed25519 public key verbatim (no placeholder/zero key)');
|
|
112
|
+
|
|
113
|
+
-- #5: a malformed/short public key is rejected at the key-enrollment step (R3 has
|
|
114
|
+
-- capacity + require_child_signing_key, so all prior gates pass).
|
|
115
|
+
select throws_ok(
|
|
116
|
+
(select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:a'), %L)$q$,
|
|
117
|
+
r3, 'deleg-bad-'||replace(substr(gen_random_uuid()::text,1,8),'-',''), repeat('A',20)) from _deleg_fx),
|
|
118
|
+
'22023', null, 'mint rejects a malformed/short child public key (#5)');
|
|
119
|
+
|
|
120
|
+
-- Scope-exceed: scope:c is not a subset of allowed [a,b].
|
|
121
|
+
select throws_ok(
|
|
122
|
+
(select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:c'))$q$,
|
|
123
|
+
r1, 'deleg-cx-'||replace(substr(gen_random_uuid()::text,1,8),'-','')) from _deleg_fx),
|
|
124
|
+
'42501', null, 'mint rejects a requested scope outside allowed_scopes (scope-exceed)');
|
|
125
|
+
|
|
126
|
+
-- Scope-exceed minted nothing: no grant beyond the two live ones for R1.
|
|
127
|
+
select is(
|
|
128
|
+
(select count(*)::int from agent_board.delegation_grants g
|
|
129
|
+
where g.root_agent_id = (select r1 from _deleg_fx) and g.revoked_at is null),
|
|
130
|
+
2, 'the two successful mints are the only live grants (scope-exceed created none)');
|
|
131
|
+
|
|
132
|
+
-- ttl-cap: child-b grant_expires_at is capped at the 8h ceiling (not 30 days).
|
|
133
|
+
select ok(
|
|
134
|
+
(select g.grant_expires_at <= now() + interval '8 hours' + interval '1 minute'
|
|
135
|
+
from agent_board.delegation_grants g where g.child_agent_id = (select child_b from _deleg_fx)),
|
|
136
|
+
'a 30-day TTL request is capped at the 8h child_ttl_ceiling (outlive-by-ttl blocked)');
|
|
137
|
+
|
|
138
|
+
-- independent_auth: mode is not root_minted => reject.
|
|
139
|
+
select throws_ok(
|
|
140
|
+
(select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:a'))$q$,
|
|
141
|
+
r2, 'deleg-cy-'||replace(substr(gen_random_uuid()::text,1,8),'-','')) from _deleg_fx),
|
|
142
|
+
'42501', null, 'mint rejects an independent_auth root (self-provisioning, not root-minted)');
|
|
143
|
+
|
|
144
|
+
-- No-transitive (max_depth=1): a root_minted child (parent_agent_id not null) cannot mint.
|
|
145
|
+
select throws_ok(
|
|
146
|
+
(select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:a'))$q$,
|
|
147
|
+
child_a, 'deleg-gc-'||replace(substr(gen_random_uuid()::text,1,8),'-','')) from _deleg_fx),
|
|
148
|
+
'42501', null, 'mint rejects a transitive attempt from a delegated child (max_depth=1)');
|
|
149
|
+
|
|
150
|
+
-- Over-fan-out: R1 already has 2 live grants (max_active_children=2) => reject the 3rd.
|
|
151
|
+
select throws_ok(
|
|
152
|
+
(select format($q$select agent_board.mint_delegated_child(%L::uuid, %L, jsonb_build_array('scope:a'))$q$,
|
|
153
|
+
r1, slug_c) from _deleg_fx),
|
|
154
|
+
'42501', null, 'mint rejects the (N+1)th child when max_active_children=N (over-fan-out)');
|
|
155
|
+
|
|
156
|
+
-- Renew works while the root is active + unrevoked (child works for hours).
|
|
157
|
+
select lives_ok(
|
|
158
|
+
(select format($q$select agent_board.renew_delegated_grant(%L::uuid)$q$, child_a) from _deleg_fx),
|
|
159
|
+
'renew_delegated_grant extends the grant while the root is active');
|
|
160
|
+
select ok(
|
|
161
|
+
(select g.last_renewed_at is not null and g.grant_expires_at > now()
|
|
162
|
+
from agent_board.delegation_grants g where g.child_agent_id = (select child_a from _deleg_fx)),
|
|
163
|
+
'renew stamped last_renewed_at and pushed grant_expires_at into the future');
|
|
164
|
+
|
|
165
|
+
-- Revoke the root's authority: suspend R1, then renew must raise (grant cannot outlive root).
|
|
166
|
+
select lives_ok(
|
|
167
|
+
(select format($q$update agent_board.agents set status = 'suspended' where id = %L::uuid$q$, r1) from _deleg_fx),
|
|
168
|
+
'suspend the root R1');
|
|
169
|
+
select throws_ok(
|
|
170
|
+
(select format($q$select agent_board.renew_delegated_grant(%L::uuid)$q$, child_a) from _deleg_fx),
|
|
171
|
+
'42501', null, 'renew raises once the root is inactive (grant cannot outlive root authority)');
|
|
172
|
+
|
|
173
|
+
select finish();
|
|
174
|
+
|
|
175
|
+
rollback;
|