@consiliency/agent-board-schema 1.3.0 → 1.4.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 (62) hide show
  1. package/bin/agent-board-schema.js +0 -0
  2. package/fixtures/already-applied.json +104 -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/package.json +9 -9
  30. package/schema-manifest.json +238 -4
  31. package/src/tooling.test.mjs +42 -1
  32. package/tests/agent_board_agent_lineage.test.sql +73 -0
  33. package/tests/agent_board_agents_status_revoked.test.sql +60 -0
  34. package/tests/agent_board_auth_rpcs.test.sql +17 -16
  35. package/tests/agent_board_board_role.test.sql +129 -0
  36. package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
  37. package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
  38. package/tests/agent_board_delegation_enforcement.test.sql +175 -0
  39. package/tests/agent_board_delegation_policies.test.sql +119 -0
  40. package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
  41. package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
  42. package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
  43. package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
  44. package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
  45. package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
  46. package/tests/agent_board_fleet_registry.test.sql +2 -2
  47. package/tests/agent_board_identity_providers.test.sql +104 -0
  48. package/tests/agent_board_inbox_rpcs.test.sql +6 -6
  49. package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
  50. package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
  51. package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
  52. package/tests/agent_board_resigning_refresh.test.sql +73 -0
  53. package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
  54. package/tests/agent_board_schema_version.test.sql +2 -2
  55. package/tests/agent_board_schema_version_rpc.test.sql +1 -1
  56. package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
  57. package/tests/agent_board_send_authz_config.test.sql +84 -0
  58. package/tests/agent_board_send_authz_matrix.test.sql +159 -0
  59. package/tests/agent_board_send_role_gate.test.sql +177 -0
  60. package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
  61. package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
  62. package/tests/agent_board_thread_acl.test.sql +129 -0
@@ -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;
@@ -0,0 +1,119 @@
1
+ -- MBAUTHZ SL-4 (Lane D) pgTAP: delegation_policies, board_authority_ceilings,
2
+ -- and the _grant_within_ceiling helper.
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(16);
9
+
10
+ create temp table _deleg_fixture (
11
+ board uuid,
12
+ owner_agent uuid,
13
+ member_agent uuid,
14
+ policy uuid,
15
+ owner_user uuid,
16
+ member_user uuid
17
+ ) on commit drop;
18
+
19
+ do $$
20
+ declare
21
+ v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
22
+ v_owner_user uuid;
23
+ v_member_user uuid;
24
+ v_owner uuid := gen_random_uuid();
25
+ v_member uuid := gen_random_uuid();
26
+ v_board uuid;
27
+ v_policy uuid := gen_random_uuid();
28
+ begin
29
+ v_owner_user := public.ensure_github_user('dg-owner-' || v_suffix, 'dg-owner-' || v_suffix || '@example.test', 'dg-owner-' || v_suffix, 'DG Owner');
30
+ v_member_user := public.ensure_github_user('dg-member-' || v_suffix, 'dg-member-' || v_suffix || '@example.test', 'dg-member-' || v_suffix, 'DG Member');
31
+
32
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
33
+ values
34
+ (v_owner, 'agent', 'dg-owner-' || v_suffix, 'DG Owner', 'active', v_owner_user, v_owner_user),
35
+ (v_member, 'agent', 'dg-member-' || v_suffix, 'DG Member', 'active', v_member_user, v_member_user);
36
+
37
+ select board_id into v_board
38
+ from agent_board.create_board(v_owner, 'dg-board-' || v_suffix, 'DG Board', 'delegation test', '{}'::jsonb, 'private');
39
+
40
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
41
+ values
42
+ (v_board, v_owner, 'joined', v_owner, 'owner'),
43
+ (v_board, v_member, 'joined', v_owner, 'member');
44
+
45
+ -- Policy with only root_agent_id set -> defaults apply (child_ttl_ceiling 8h).
46
+ insert into agent_board.delegation_policies(id, root_agent_id) values (v_policy, v_owner);
47
+
48
+ insert into _deleg_fixture values (v_board, v_owner, v_member, v_policy, v_owner_user, v_member_user);
49
+ end
50
+ $$;
51
+ grant select on _deleg_fixture to authenticated;
52
+
53
+ -- delegation_policies shape -------------------------------------------------
54
+ select has_table('agent_board', 'delegation_policies', 'delegation_policies table exists');
55
+ select has_column('agent_board', 'delegation_policies', 'mode', 'delegation_policies has mode');
56
+ select has_column('agent_board', 'delegation_policies', 'child_ttl_ceiling', 'delegation_policies has child_ttl_ceiling');
57
+ select is(
58
+ (select child_ttl_ceiling > interval '15 minutes' from agent_board.delegation_policies dp join _deleg_fixture f on dp.id = f.policy),
59
+ true, 'child_ttl_ceiling default is hours-scale (> 15 minutes, not the 900s session clock)');
60
+ select is(
61
+ (select child_ttl_ceiling = interval '8 hours' from agent_board.delegation_policies dp join _deleg_fixture f on dp.id = f.policy),
62
+ true, 'child_ttl_ceiling default is 8 hours');
63
+
64
+ -- board_authority_ceilings shape --------------------------------------------
65
+ select has_table('agent_board', 'board_authority_ceilings', 'board_authority_ceilings table exists');
66
+ select col_is_pk('agent_board', 'board_authority_ceilings', 'board_id', 'board_id is the primary key');
67
+ select fk_ok('agent_board', 'board_authority_ceilings', 'board_id', 'agent_board', 'boards', 'id',
68
+ 'board_id references boards(id)');
69
+
70
+ -- _grant_within_ceiling: no ceiling row (defaults to owner) -----------------
71
+ select is(
72
+ (select agent_board._grant_within_ceiling(owner_agent, board, 'admin') from _deleg_fixture),
73
+ true, 'no ceiling: owner may grant admin (within grantor role)');
74
+ select is(
75
+ (select agent_board._grant_within_ceiling(owner_agent, board, 'owner') from _deleg_fixture),
76
+ true, 'no ceiling row is treated as owner: owner may grant owner (unchanged)');
77
+
78
+ -- Exceeding grantor role is rejected regardless of ceiling ------------------
79
+ select is(
80
+ (select agent_board._grant_within_ceiling(member_agent, board, 'admin') from _deleg_fixture),
81
+ false, 'grant exceeding grantor role is rejected (member cannot grant admin)');
82
+
83
+ -- Over-ceiling grant is rejected --------------------------------------------
84
+ insert into agent_board.board_authority_ceilings(board_id, max_grantable_role)
85
+ select board, 'member' from _deleg_fixture;
86
+ select is(
87
+ (select agent_board._grant_within_ceiling(owner_agent, board, 'admin') from _deleg_fixture),
88
+ false, 'over-ceiling grant is rejected (owner cannot grant admin when ceiling is member)');
89
+
90
+ -- #13 owner-settable ceiling: set_board_authority_ceiling is owner-gated. ---------
91
+ select has_function('agent_board', 'set_board_authority_ceiling', array['uuid','text'],
92
+ 'set_board_authority_ceiling exists');
93
+
94
+ set local role authenticated;
95
+ select set_config('request.jwt.claim.role', 'authenticated', true);
96
+
97
+ -- A non-owner (member) is denied.
98
+ select set_config('request.jwt.claim.sub', (select member_user from _deleg_fixture)::text, true);
99
+ select throws_ok(
100
+ (select format($q$select agent_board.set_board_authority_ceiling(%L::uuid, 'admin')$q$, board) from _deleg_fixture),
101
+ '42501', null, 'a non-owner is denied set_board_authority_ceiling');
102
+
103
+ -- The board owner may set it (changing the pre-existing 'member' ceiling to 'admin').
104
+ select set_config('request.jwt.claim.sub', (select owner_user from _deleg_fixture)::text, true);
105
+ select lives_ok(
106
+ (select format($q$select agent_board.set_board_authority_ceiling(%L::uuid, 'admin')$q$, board) from _deleg_fixture),
107
+ 'the board owner can set the authority ceiling');
108
+
109
+ reset role;
110
+
111
+ -- Verify the stored ceiling as the table owner (the SELECT RLS policy helper
112
+ -- _current_agent_has_board_access is not executable by the authenticated role).
113
+ select is(
114
+ (select max_grantable_role from agent_board.board_authority_ceilings c where c.board_id = (select board from _deleg_fixture)),
115
+ 'admin', 'the owner-set ceiling was upserted to admin');
116
+
117
+ select finish();
118
+
119
+ rollback;
@@ -0,0 +1,217 @@
1
+ -- MBPORTALGATE SL-2 pgTAP: the reworked enroll_federated_identity principal model.
2
+ --
3
+ -- Proves: the 13-arg signature; one enroll MINTS a human owner + a SEPARATE
4
+ -- key-identified agent (own auth_user_id) + an active+proven signing key + one audit
5
+ -- row; ONE human (issuer, sub) enrolling a 2nd distinct key creates a SECOND agent
6
+ -- (first unmutated) sharing ONE owner_user_id but with a DISTINCT auth_user_id
7
+ -- (independently session-mintable); revoking one agent leaves neither the human owner
8
+ -- nor a sibling revoked; and the provider/scope/kind rejection matrix still raises.
9
+ -- The PoP-binding suite (fingerprint recompute/tamper/dedup, PoP gate, actor_sub) lives
10
+ -- in agent_board_enroll_pop_binding.test.sql. Runs as postgres (service_role-equiv).
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(28);
17
+
18
+ -- New 13-arg signature exists (the old 8-arg was dropped by 20260719001100). --------
19
+ select has_function(
20
+ 'agent_board', 'enroll_federated_identity',
21
+ array['text','text','text','jsonb','text','text','text','text','boolean','text','text','text[]','text[]'],
22
+ 'enroll_federated_identity exists with the reworked 13-arg PoP signature');
23
+ select hasnt_function(
24
+ 'agent_board', 'enroll_federated_identity',
25
+ array['text','text','text','jsonb','text','text','text[]','text[]'],
26
+ 'the prior 8-arg enroll_federated_identity was dropped');
27
+
28
+ -- Fixtures: an APPROVED provider + two real Ed25519 keys with round-trip fingerprints.
29
+ create temp table _p (
30
+ iss text, aud text, sub text, slug text, claim jsonb,
31
+ pub1 text, fp1 text, pub2 text, fp2 text
32
+ ) on commit drop;
33
+ create temp table _r (
34
+ label text, agent_id uuid, auth_user_id uuid, provider_id text,
35
+ signing_key_id uuid, granted text[], owner_user_id uuid
36
+ ) on commit drop;
37
+
38
+ do $$
39
+ declare
40
+ s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
41
+ v_agent_id uuid; v_auth uuid; v_prov text; v_key uuid; v_granted text[];
42
+ begin
43
+ insert into agent_board.identity_providers
44
+ (issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
45
+ values
46
+ ('https://fed-ok-'||s||'.test', 'message-board', true, true, '{board:send}', '{board:x}', '{agent,human}');
47
+
48
+ insert into _p values (
49
+ 'https://fed-ok-'||s||'.test', 'message-board', 'portal|'||s, 'fed-agent-'||s,
50
+ jsonb_build_object('sub', 'portal|'||s, 'act', jsonb_build_object('sub', 'act|'||s), 'name', 'Fed '||s),
51
+ 'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0',
52
+ 'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
53
+ 'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk',
54
+ 'sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA'
55
+ );
56
+
57
+ -- Enroll A (key 1).
58
+ select e.agent_id, e.auth_user_id, e.provider_id, e.signing_key_id, e.granted_scopes
59
+ into v_agent_id, v_auth, v_prov, v_key, v_granted
60
+ from _p p, agent_board.enroll_federated_identity(
61
+ p.iss, p.aud, p.sub, p.claim, p.slug, p.pub1, 'raw_ed25519_base64url', p.fp1,
62
+ true, 'act|'||s, 'agent', '{board:send}', '{board:x}') e;
63
+ insert into _r
64
+ select 'A', v_agent_id, v_auth, v_prov, v_key, v_granted, a.owner_user_id
65
+ from agent_board.agents a where a.id = v_agent_id;
66
+
67
+ -- Enroll B: SAME human (issuer, sub) + SAME slug, DIFFERENT key -> a 2nd agent.
68
+ select e.agent_id, e.auth_user_id, e.provider_id, e.signing_key_id, e.granted_scopes
69
+ into v_agent_id, v_auth, v_prov, v_key, v_granted
70
+ from _p p, agent_board.enroll_federated_identity(
71
+ p.iss, p.aud, p.sub, p.claim, p.slug, p.pub2, 'raw_ed25519_base64url', p.fp2,
72
+ true, 'act|'||s, 'agent', '{board:send}', '{board:x}') e;
73
+ insert into _r
74
+ select 'B', v_agent_id, v_auth, v_prov, v_key, v_granted, a.owner_user_id
75
+ from agent_board.agents a where a.id = v_agent_id;
76
+ end
77
+ $$;
78
+
79
+ -- One enroll mints a distinct agent + human owner + own principal --------------------
80
+ select is((select count(*)::int from _r where label='A'), 1, 'enroll A returned exactly one row');
81
+ select is(
82
+ (select a.status from agent_board.agents a join _r r on r.agent_id=a.id where r.label='A'),
83
+ 'active', 'agent A is active');
84
+ select isnt(
85
+ (select owner_user_id from _r where label='A'),
86
+ (select auth_user_id from _r where label='A'),
87
+ 'agent A owner_user_id (human) is DISTINCT from its own auth_user_id (agent principal)');
88
+ select is(
89
+ (select count(*)::int from auth.users u where u.id = (select owner_user_id from _r where label='A')),
90
+ 1, 'the human owner auth.users row exists');
91
+ select is(
92
+ (select count(*)::int from auth.users u where u.id = (select auth_user_id from _r where label='A')),
93
+ 1, 'the agent principal auth.users row exists (session-usable via its key)');
94
+
95
+ -- The enrolled key is active + proven and round-trips the fingerprint ---------------
96
+ select is(
97
+ (select k.status||'/'||k.proof_state from agent_board.agent_signing_keys k
98
+ where k.id = (select signing_key_id from _r where label='A')),
99
+ 'active/proven', 'agent A signing key is active + proven (satisfies active_requires_proof)');
100
+ select is(
101
+ (select k.fingerprint from agent_board.agent_signing_keys k
102
+ where k.id = (select signing_key_id from _r where label='A')),
103
+ 'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
104
+ 'the DB-recomputed fingerprint round-trips the independently-computed client fingerprint');
105
+ select is(
106
+ (select k.agent_id from agent_board.agent_signing_keys k
107
+ where k.id = (select signing_key_id from _r where label='A')),
108
+ (select agent_id from _r where label='A'),
109
+ 'the returned signing_key_id belongs to the returned agent');
110
+ select is(
111
+ (select count(*)::int from agent_board.enrollment_federation_audit e where e.agent_id = (select agent_id from _r where label='A')),
112
+ 1, 'enroll A wrote exactly one enrollment_federation_audit row');
113
+
114
+ -- One human -> N agents: the 2nd enroll creates a 2nd agent, first unmutated ---------
115
+ select isnt(
116
+ (select agent_id from _r where label='B'),
117
+ (select agent_id from _r where label='A'),
118
+ 'enroll B created a SECOND, distinct agent (never keyed on sub)');
119
+ select is(
120
+ (select owner_user_id from _r where label='B'),
121
+ (select owner_user_id from _r where label='A'),
122
+ 'both agents of the same human share ONE owner_user_id (idempotent human owner)');
123
+ select isnt(
124
+ (select auth_user_id from _r where label='B'),
125
+ (select auth_user_id from _r where label='A'),
126
+ 'each agent has a DISTINCT auth_user_id (each independently session-mintable)');
127
+ select is(
128
+ (select count(distinct a.id)::int from agent_board.agents a
129
+ where a.owner_user_id = (select owner_user_id from _r where label='A')),
130
+ 2, 'the human owner now has exactly two agents');
131
+ select is(
132
+ (select a.status from agent_board.agents a where a.id = (select agent_id from _r where label='A')),
133
+ 'active', 'the FIRST agent is unmutated (still active) after the 2nd enroll');
134
+ select is(
135
+ (select k.fingerprint from agent_board.agent_signing_keys k where k.agent_id = (select agent_id from _r where label='A')),
136
+ 'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
137
+ 'the first agent still holds ONLY its original key (not mutated to the 2nd key)');
138
+ select isnt(
139
+ (select a.handle from agent_board.agents a where a.id = (select agent_id from _r where label='B')),
140
+ (select a.handle from agent_board.agents a where a.id = (select agent_id from _r where label='A')),
141
+ 'the 2nd agent under the same slug got a distinct (disambiguated) handle');
142
+
143
+ -- Revoke isolation: revoking agent A leaves neither the human nor sibling B revoked --
144
+ do $$
145
+ begin
146
+ update agent_board.agents set status='revoked', updated_at=now()
147
+ where id = (select agent_id from _r where label='A');
148
+ update agent_board.agent_signing_keys set status='revoked', revoked_at=now()
149
+ where agent_id = (select agent_id from _r where label='A');
150
+ end
151
+ $$;
152
+ select is(
153
+ (select a.status from agent_board.agents a where a.id = (select agent_id from _r where label='B')),
154
+ 'active', 'revoking agent A leaves sibling agent B active (independent principals)');
155
+ select is(
156
+ (select k.status from agent_board.agent_signing_keys k where k.agent_id = (select agent_id from _r where label='B')),
157
+ 'active', 'sibling B''s signing key stays active (B remains session-mintable)');
158
+ select is(
159
+ (select count(*)::int from auth.users u where u.id = (select owner_user_id from _r where label='A')),
160
+ 1, 'revoking agent A does NOT revoke the shared human owner');
161
+
162
+ -- Provider / scope / kind rejection matrix still raises ------------------------------
163
+ select throws_ok(
164
+ (select format($q$select agent_board.enroll_federated_identity('https://nope.test',%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
165
+ aud, sub, claim::text, 'x-'||slug, pub1, fp1) from _p),
166
+ '42501', null, 'unknown (issuer,audience) is rejected');
167
+ select throws_ok(
168
+ (select format($q$select agent_board.enroll_federated_identity(%L,%L,NULL,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
169
+ iss, aud, claim::text, 'y-'||slug, pub1, fp1) from _p),
170
+ '22023', null, 'a null verified subject is rejected');
171
+ -- requested scope outside the provider allowed_scopes ceiling
172
+ select throws_ok(
173
+ (select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:admin}','{}')$q$,
174
+ iss, aud, sub, claim::text, 'z-'||slug, pub1, fp1) from _p),
175
+ '42501', null, 'a requested scope outside allowed_scopes is rejected');
176
+ -- the 'system' kind is never federatable
177
+ select throws_ok(
178
+ (select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'system','{board:send}','{}')$q$,
179
+ iss, aud, sub, claim::text, 'w-'||slug, pub1, fp1) from _p),
180
+ '42501', null, 'the system kind cannot be federated');
181
+
182
+ -- B1: the RPC RETURNS the server-assigned key_id/key_version/agent_handle so the client
183
+ -- mints /session against the AUTHORITATIVE identity (the missing binding that otherwise
184
+ -- left every enrolled agent unable to authenticate). /session resolves by exactly
185
+ -- (agents.handle, key_id, key_version), so all three must be the server's values.
186
+ create temp table _b1 (agent uuid, key_id text, key_version int, handle text) on commit drop;
187
+ do $$
188
+ declare
189
+ s text := replace(substr(gen_random_uuid()::text,1,8),'-','');
190
+ v_pub text := rtrim(translate(encode(extensions.digest(convert_to('b1-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
191
+ v_fp text := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
192
+ v_kid text; v_kver int; v_handle text; v_agent uuid;
193
+ begin
194
+ insert into agent_board.identity_providers
195
+ (issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
196
+ values ('https://fed-b1-'||s||'.test','message-board',true,true,'{board:send}','{}','{agent}');
197
+ select e.agent_id, e.key_id, e.key_version, e.agent_handle
198
+ into v_agent, v_kid, v_kver, v_handle
199
+ from agent_board.enroll_federated_identity(
200
+ 'https://fed-b1-'||s||'.test','message-board','portal|'||s,
201
+ jsonb_build_object('sub','portal|'||s), 'b1-'||s, v_pub,'raw_ed25519_base64url', v_fp,
202
+ true, null, 'agent', '{board:send}', '{}') e;
203
+ insert into _b1 values (v_agent, v_kid, v_kver, v_handle);
204
+ end $$;
205
+ select is(
206
+ (select key_id from _b1),
207
+ (select k.key_id from agent_board.agent_signing_keys k where k.agent_id = (select agent from _b1)),
208
+ 'the returned key_id equals the enrolled signing key''s key_id (session-resolvable)');
209
+ select is((select key_version from _b1), 1, 'the returned key_version is the enrolled key_version (1)');
210
+ select is(
211
+ (select handle from _b1),
212
+ (select a.handle from agent_board.agents a where a.id = (select agent from _b1)),
213
+ 'the returned agent_handle equals agents.handle (the /session resolver key)');
214
+
215
+ select finish();
216
+
217
+ rollback;