@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.
Files changed (66) hide show
  1. package/bin/agent-board-schema.js +0 -0
  2. package/fixtures/already-applied.json +112 -0
  3. package/migrations/20260717000100_agent_board_board_role.sql +180 -0
  4. package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
  5. package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
  6. package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
  7. package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
  8. package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
  9. package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
  10. package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
  11. package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
  12. package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
  13. package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
  14. package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
  15. package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
  16. package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
  17. package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
  18. package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
  19. package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
  20. package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
  21. package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
  22. package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
  23. package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
  24. package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
  25. package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
  26. package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
  27. package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
  28. package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
  29. package/migrations/20260723000100_agent_board_roomown_owner_seed.sql +167 -0
  30. package/migrations/20260723000200_agent_board_roomown_grant_role_live.sql +72 -0
  31. package/package.json +9 -9
  32. package/schema-manifest.json +256 -4
  33. package/src/tooling.test.mjs +46 -1
  34. package/tests/agent_board_agent_lineage.test.sql +73 -0
  35. package/tests/agent_board_agents_status_revoked.test.sql +60 -0
  36. package/tests/agent_board_auth_rpcs.test.sql +17 -16
  37. package/tests/agent_board_board_role.test.sql +129 -0
  38. package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
  39. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
  40. package/tests/agent_board_delegation_enforcement.test.sql +175 -0
  41. package/tests/agent_board_delegation_policies.test.sql +120 -0
  42. package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
  43. package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
  44. package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
  45. package/tests/agent_board_enrollment_audit_content.test.sql +139 -0
  46. package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
  47. package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
  48. package/tests/agent_board_fleet_registry.test.sql +2 -2
  49. package/tests/agent_board_identity_providers.test.sql +104 -0
  50. package/tests/agent_board_inbox_rpcs.test.sql +10 -10
  51. package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
  52. package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
  53. package/tests/agent_board_portal_offline_invariant.test.sql +134 -0
  54. package/tests/agent_board_resigning_refresh.test.sql +73 -0
  55. package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
  56. package/tests/agent_board_roomown.test.sql +233 -0
  57. package/tests/agent_board_schema_version.test.sql +2 -2
  58. package/tests/agent_board_schema_version_rpc.test.sql +1 -1
  59. package/tests/agent_board_scoped_send_regrant.test.sql +142 -0
  60. package/tests/agent_board_send_authz_config.test.sql +84 -0
  61. package/tests/agent_board_send_authz_matrix.test.sql +162 -0
  62. package/tests/agent_board_send_role_gate.test.sql +181 -0
  63. package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
  64. package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
  65. package/tests/agent_board_thread_acl.test.sql +129 -0
  66. 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
- 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;
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 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.
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 '22023' then
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 22023 for a system principal';
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 also fails closed for system principals');
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'), '7',
26
- 'compatibility reports schema version 7');
25
+ select is((agent_board.get_bundle_compatibility() ->> 'schema_version'), '8',
26
+ 'compatibility reports schema version 8');
27
27
 
28
- -- signed-write enforcement posture is active (canary revoked direct writes)
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 is reported active');
42
+ 'signed-write enforcement reports active when all gated write RPCs are revoked from authenticated');
31
43
 
32
- -- probe covers ALL four gated RPCs: re-granting EXECUTE on one of the OTHER
33
- -- three (not send_direct_message) must flip enforcement to false.
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[]::text[],
18
- 'authenticated cannot bypass direct signed gateway'
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[]::text[],
27
- 'authenticated cannot bypass multicast signed gateway'
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[]::text[],
36
- 'authenticated cannot bypass board signed gateway'
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[]::text[],
45
- 'authenticated cannot bypass tasking signed gateway'
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;