@consiliency/agent-board-schema 1.2.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.
- package/bin/agent-board-schema.js +0 -0
- package/fixtures/already-applied.json +108 -0
- package/migrations/20260714000200_agent_board_workload_providers_rls.sql +35 -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/package.json +9 -9
- package/schema-manifest.json +247 -4
- package/src/tooling.test.mjs +42 -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 +119 -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 +134 -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 +6 -6
- 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 +130 -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_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 +138 -0
- package/tests/agent_board_send_authz_config.test.sql +84 -0
- package/tests/agent_board_send_authz_matrix.test.sql +159 -0
- package/tests/agent_board_send_role_gate.test.sql +177 -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_workload_providers_rls.test.sql +40 -0
|
@@ -130,7 +130,7 @@ select ok(
|
|
|
130
130
|
|
|
131
131
|
select ok(
|
|
132
132
|
(
|
|
133
|
-
select count(distinct routine_name) =
|
|
133
|
+
select count(distinct routine_name) = 16
|
|
134
134
|
from information_schema.routine_privileges
|
|
135
135
|
where specific_schema = 'agent_board'
|
|
136
136
|
and routine_name in (
|
|
@@ -154,11 +154,11 @@ select ok(
|
|
|
154
154
|
and grantee = 'authenticated'
|
|
155
155
|
and privilege_type = 'EXECUTE'
|
|
156
156
|
)
|
|
157
|
-
and
|
|
158
|
-
and
|
|
159
|
-
and
|
|
160
|
-
and
|
|
161
|
-
'authenticated keeps non-delivery inbox RPCs
|
|
157
|
+
and has_function_privilege('authenticated', 'agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'EXECUTE')
|
|
158
|
+
and has_function_privilege('authenticated', 'agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)', 'EXECUTE')
|
|
159
|
+
and has_function_privilege('authenticated', 'agent_board.post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)', 'EXECUTE')
|
|
160
|
+
and has_function_privilege('authenticated', 'agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)', 'EXECUTE'),
|
|
161
|
+
'authenticated keeps non-delivery inbox RPCs and holds the MBSESSION re-granted scoped-send RPCs'
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
select ok(
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
-- MBSESSION SL-1 pgTAP: whole-principal fleet_agent_session mint + lineage liveness.
|
|
2
|
+
--
|
|
3
|
+
-- Proves _agent_principal_active walks the full lineage, and mint_fleet_agent_session
|
|
4
|
+
-- mints only for an active principal with a valid/unconsumed challenge and an
|
|
5
|
+
-- active/unrevoked signing key (capping the TTL at 900), raising on every failure
|
|
6
|
+
-- clause — including the canonical REFRESH-AFTER-REVOCATION proof (a fresh re-mint
|
|
7
|
+
-- after revoking the key / suspending an ancestor RAISES; not old-token expiry).
|
|
8
|
+
--
|
|
9
|
+
-- Runs as postgres (service_role-equivalent for _is_service_role()); mint is
|
|
10
|
+
-- service_role-only and trusts the p_agent_id it is handed.
|
|
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(29);
|
|
17
|
+
|
|
18
|
+
create temp table _mint_fx (
|
|
19
|
+
a1 uuid, key_a1 uuid, ch_happy uuid, ch_ttl uuid, ch_consumed uuid, ch_expired uuid, ch_mismatch uuid,
|
|
20
|
+
a3 uuid, key_a3 uuid, ch_revkey uuid,
|
|
21
|
+
a4 uuid, key_a4 uuid, ch_expkey uuid,
|
|
22
|
+
a5 uuid, key_a5 uuid, ch_inactagent uuid,
|
|
23
|
+
r_susp uuid, c_susp uuid, key_csusp uuid, ch_inactanc uuid,
|
|
24
|
+
a6 uuid,
|
|
25
|
+
a7 uuid, key_a7 uuid, ch_refkey_a uuid, ch_refkey_b uuid,
|
|
26
|
+
r2 uuid, c2 uuid, key_c2 uuid, ch_refanc_a uuid, ch_refanc_b uuid
|
|
27
|
+
) on commit drop;
|
|
28
|
+
|
|
29
|
+
do $$
|
|
30
|
+
declare
|
|
31
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
32
|
+
u_a1 uuid; u_a3 uuid; u_a4 uuid; u_a6 uuid; u_a7 uuid; u_csusp uuid; u_r2 uuid; u_c2 uuid;
|
|
33
|
+
v_a1 uuid := gen_random_uuid(); v_a3 uuid := gen_random_uuid(); v_a4 uuid := gen_random_uuid();
|
|
34
|
+
v_a5 uuid := gen_random_uuid(); v_a6 uuid := gen_random_uuid(); v_a7 uuid := gen_random_uuid();
|
|
35
|
+
v_rsusp uuid := gen_random_uuid(); v_csusp uuid := gen_random_uuid();
|
|
36
|
+
v_r2 uuid := gen_random_uuid(); v_c2 uuid := gen_random_uuid();
|
|
37
|
+
k_a1 uuid := gen_random_uuid(); k_a3 uuid := gen_random_uuid(); k_a4 uuid := gen_random_uuid();
|
|
38
|
+
k_a5 uuid := gen_random_uuid(); k_csusp uuid := gen_random_uuid(); k_a7 uuid := gen_random_uuid();
|
|
39
|
+
k_c2 uuid := gen_random_uuid();
|
|
40
|
+
ch_happy uuid := gen_random_uuid(); ch_ttl uuid := gen_random_uuid(); ch_consumed uuid := gen_random_uuid();
|
|
41
|
+
ch_expired uuid := gen_random_uuid(); ch_mismatch uuid := gen_random_uuid(); ch_revkey uuid := gen_random_uuid();
|
|
42
|
+
ch_expkey uuid := gen_random_uuid(); ch_inactagent uuid := gen_random_uuid(); ch_inactanc uuid := gen_random_uuid();
|
|
43
|
+
ch_refkey_a uuid := gen_random_uuid(); ch_refkey_b uuid := gen_random_uuid();
|
|
44
|
+
ch_refanc_a uuid := gen_random_uuid(); ch_refanc_b uuid := gen_random_uuid();
|
|
45
|
+
begin
|
|
46
|
+
u_a1 := public.ensure_github_user('mint-a1-'||s, 'mint-a1-'||s||'@example.test', 'mint-a1-'||s, 'A1');
|
|
47
|
+
u_a3 := public.ensure_github_user('mint-a3-'||s, 'mint-a3-'||s||'@example.test', 'mint-a3-'||s, 'A3');
|
|
48
|
+
u_a4 := public.ensure_github_user('mint-a4-'||s, 'mint-a4-'||s||'@example.test', 'mint-a4-'||s, 'A4');
|
|
49
|
+
u_a6 := public.ensure_github_user('mint-a6-'||s, 'mint-a6-'||s||'@example.test', 'mint-a6-'||s, 'A6');
|
|
50
|
+
u_a7 := public.ensure_github_user('mint-a7-'||s, 'mint-a7-'||s||'@example.test', 'mint-a7-'||s, 'A7');
|
|
51
|
+
u_csusp := public.ensure_github_user('mint-cs-'||s, 'mint-cs-'||s||'@example.test', 'mint-cs-'||s, 'CS');
|
|
52
|
+
u_r2 := public.ensure_github_user('mint-r2-'||s, 'mint-r2-'||s||'@example.test', 'mint-r2-'||s, 'R2');
|
|
53
|
+
u_c2 := public.ensure_github_user('mint-c2-'||s, 'mint-c2-'||s||'@example.test', 'mint-c2-'||s, 'C2');
|
|
54
|
+
|
|
55
|
+
-- Active auth-bound singles (each a distinct auth_user_id for the active-unique index).
|
|
56
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id)
|
|
57
|
+
values
|
|
58
|
+
(v_a1, 'agent', 'mint-a1-'||s, 'A1', 'active', u_a1),
|
|
59
|
+
(v_a3, 'agent', 'mint-a3-'||s, 'A3', 'active', u_a3),
|
|
60
|
+
(v_a4, 'agent', 'mint-a4-'||s, 'A4', 'active', u_a4),
|
|
61
|
+
(v_a6, 'agent', 'mint-a6-'||s, 'A6', 'active', u_a6),
|
|
62
|
+
(v_a7, 'agent', 'mint-a7-'||s, 'A7', 'active', u_a7);
|
|
63
|
+
|
|
64
|
+
-- Suspended single (no auth binding needed; exempt from the active-unique index).
|
|
65
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
66
|
+
values (v_a5, 'agent', 'mint-a5-'||s, 'A5', 'suspended');
|
|
67
|
+
|
|
68
|
+
-- Suspended-ancestor chain: root suspended, child active.
|
|
69
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
70
|
+
values (v_rsusp, 'agent', 'mint-rs-'||s, 'RS', 'suspended');
|
|
71
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, parent_agent_id, root_agent_id)
|
|
72
|
+
values (v_csusp, 'agent', 'mint-cs-'||s, 'CS', 'active', u_csusp, v_rsusp, v_rsusp);
|
|
73
|
+
|
|
74
|
+
-- Active chain (used for full-lineage-true and refresh-after-revocation-ancestor).
|
|
75
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id)
|
|
76
|
+
values (v_r2, 'agent', 'mint-r2-'||s, 'R2', 'active', u_r2);
|
|
77
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, parent_agent_id, root_agent_id)
|
|
78
|
+
values (v_c2, 'agent', 'mint-c2-'||s, 'C2', 'active', u_c2, v_r2, v_r2);
|
|
79
|
+
|
|
80
|
+
-- Signing keys.
|
|
81
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
82
|
+
values
|
|
83
|
+
(k_a1, v_a1, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_a1::text,'-',''),43,'A'), 'active', 'proven'),
|
|
84
|
+
(k_a5, v_a5, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_a5::text,'-',''),43,'A'), 'active', 'proven'),
|
|
85
|
+
(k_csusp, v_csusp, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_csusp::text,'-',''),43,'A'), 'active', 'proven'),
|
|
86
|
+
(k_a7, v_a7, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_a7::text,'-',''),43,'A'), 'active', 'proven'),
|
|
87
|
+
(k_c2, v_c2, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_c2::text,'-',''),43,'A'), 'active', 'proven');
|
|
88
|
+
-- Revoked key.
|
|
89
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state, revoked_at)
|
|
90
|
+
values (k_a3, v_a3, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_a3::text,'-',''),43,'A'), 'revoked', 'revoked', now());
|
|
91
|
+
-- Expired key.
|
|
92
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
93
|
+
values (k_a4, v_a4, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_a4::text,'-',''),43,'A'), 'expired', 'expired');
|
|
94
|
+
|
|
95
|
+
-- Challenges. Pending + unexpired unless noted.
|
|
96
|
+
insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at, consumed_at)
|
|
97
|
+
values
|
|
98
|
+
(ch_happy, k_a1, v_a1, ch_happy::text, 'agent-signing-key-proof.v1:'||ch_happy||':n', 'pending', now() + interval '10 minutes', null),
|
|
99
|
+
(ch_ttl, k_a1, v_a1, ch_ttl::text, 'agent-signing-key-proof.v1:'||ch_ttl||':n', 'pending', now() + interval '10 minutes', null),
|
|
100
|
+
(ch_consumed, k_a1, v_a1, ch_consumed::text, 'agent-signing-key-proof.v1:'||ch_consumed||':n', 'consumed', now() + interval '10 minutes', now()),
|
|
101
|
+
(ch_expired, k_a1, v_a1, ch_expired::text, 'agent-signing-key-proof.v1:'||ch_expired||':n', 'pending', now() - interval '1 minute', null),
|
|
102
|
+
(ch_mismatch, k_a1, v_a1, ch_mismatch::text, 'agent-signing-key-proof.v1:'||ch_mismatch||':n', 'pending', now() + interval '10 minutes', null),
|
|
103
|
+
(ch_revkey, k_a3, v_a3, ch_revkey::text, 'agent-signing-key-proof.v1:'||ch_revkey||':n', 'pending', now() + interval '10 minutes', null),
|
|
104
|
+
(ch_expkey, k_a4, v_a4, ch_expkey::text, 'agent-signing-key-proof.v1:'||ch_expkey||':n', 'pending', now() + interval '10 minutes', null),
|
|
105
|
+
(ch_inactagent, k_a5, v_a5, ch_inactagent::text, 'agent-signing-key-proof.v1:'||ch_inactagent||':n', 'pending', now() + interval '10 minutes', null),
|
|
106
|
+
(ch_inactanc, k_csusp, v_csusp, ch_inactanc::text, 'agent-signing-key-proof.v1:'||ch_inactanc||':n', 'pending', now() + interval '10 minutes', null),
|
|
107
|
+
(ch_refkey_a, k_a7, v_a7, ch_refkey_a::text, 'agent-signing-key-proof.v1:'||ch_refkey_a||':n', 'pending', now() + interval '10 minutes', null),
|
|
108
|
+
(ch_refkey_b, k_a7, v_a7, ch_refkey_b::text, 'agent-signing-key-proof.v1:'||ch_refkey_b||':n', 'pending', now() + interval '10 minutes', null),
|
|
109
|
+
(ch_refanc_a, k_c2, v_c2, ch_refanc_a::text, 'agent-signing-key-proof.v1:'||ch_refanc_a||':n', 'pending', now() + interval '10 minutes', null),
|
|
110
|
+
(ch_refanc_b, k_c2, v_c2, ch_refanc_b::text, 'agent-signing-key-proof.v1:'||ch_refanc_b||':n', 'pending', now() + interval '10 minutes', null);
|
|
111
|
+
|
|
112
|
+
insert into _mint_fx values (
|
|
113
|
+
v_a1, k_a1, ch_happy, ch_ttl, ch_consumed, ch_expired, ch_mismatch,
|
|
114
|
+
v_a3, k_a3, ch_revkey,
|
|
115
|
+
v_a4, k_a4, ch_expkey,
|
|
116
|
+
v_a5, k_a5, ch_inactagent,
|
|
117
|
+
v_rsusp, v_csusp, k_csusp, ch_inactanc,
|
|
118
|
+
v_a6,
|
|
119
|
+
v_a7, k_a7, ch_refkey_a, ch_refkey_b,
|
|
120
|
+
v_r2, v_c2, k_c2, ch_refanc_a, ch_refanc_b
|
|
121
|
+
);
|
|
122
|
+
end
|
|
123
|
+
$$;
|
|
124
|
+
|
|
125
|
+
-- #4/#10/#12 fixture: delegation-grant liveness, nonce binding, key-expiry. --------
|
|
126
|
+
create temp table _grant_fx (
|
|
127
|
+
c_exp uuid, key_exp uuid, ch_exp uuid,
|
|
128
|
+
c_live uuid, key_live uuid, ch_live uuid, ch_wrong uuid,
|
|
129
|
+
a_expts uuid, key_expts uuid, ch_expts uuid
|
|
130
|
+
) on commit drop;
|
|
131
|
+
|
|
132
|
+
do $$
|
|
133
|
+
declare
|
|
134
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
135
|
+
u_root uuid; u_ce uuid; u_cl uuid; u_et uuid;
|
|
136
|
+
v_root uuid := gen_random_uuid();
|
|
137
|
+
v_pol uuid := gen_random_uuid();
|
|
138
|
+
v_ce uuid := gen_random_uuid(); v_cl uuid := gen_random_uuid(); v_et uuid := gen_random_uuid();
|
|
139
|
+
k_ce uuid := gen_random_uuid(); k_cl uuid := gen_random_uuid(); k_et uuid := gen_random_uuid();
|
|
140
|
+
ch_ce uuid := gen_random_uuid(); ch_cl uuid := gen_random_uuid();
|
|
141
|
+
ch_wrong uuid := gen_random_uuid(); ch_et uuid := gen_random_uuid();
|
|
142
|
+
begin
|
|
143
|
+
u_root := public.ensure_github_user('gr-root-'||s, 'gr-root-'||s||'@example.test', 'gr-root-'||s, 'GR');
|
|
144
|
+
u_ce := public.ensure_github_user('gr-ce-'||s, 'gr-ce-'||s||'@example.test', 'gr-ce-'||s, 'CE');
|
|
145
|
+
u_cl := public.ensure_github_user('gr-cl-'||s, 'gr-cl-'||s||'@example.test', 'gr-cl-'||s, 'CL');
|
|
146
|
+
u_et := public.ensure_github_user('gr-et-'||s, 'gr-et-'||s||'@example.test', 'gr-et-'||s, 'ET');
|
|
147
|
+
|
|
148
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id)
|
|
149
|
+
values (v_root, 'agent', 'gr-root-'||s, 'GR', 'active', u_root);
|
|
150
|
+
insert into agent_board.delegation_policies(id, root_agent_id, mode, allowed_scopes, child_ttl_ceiling)
|
|
151
|
+
values (v_pol, v_root, 'root_minted', jsonb_build_array('scope:a'), interval '8 hours');
|
|
152
|
+
|
|
153
|
+
-- Child with an EXPIRED governing grant (grant_expires_at in the past).
|
|
154
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, parent_agent_id, root_agent_id)
|
|
155
|
+
values (v_ce, 'agent', 'gr-ce-'||s, 'CE', 'active', u_ce, v_root, v_root);
|
|
156
|
+
insert into agent_board.delegation_grants(child_agent_id, root_agent_id, parent_agent_id, delegation_policy_id, granted_scopes, grant_expires_at)
|
|
157
|
+
values (v_ce, v_root, v_root, v_pol, jsonb_build_array('scope:a'), now() - interval '1 minute');
|
|
158
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
159
|
+
values (k_ce, v_ce, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_ce::text,'-',''),43,'A'), 'active', 'proven');
|
|
160
|
+
insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at)
|
|
161
|
+
values (ch_ce, k_ce, v_ce, ch_ce::text, 'x', 'pending', now() + interval '10 minutes');
|
|
162
|
+
|
|
163
|
+
-- Child with a LIVE governing grant (positive control + spare challenge for nonce test).
|
|
164
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, parent_agent_id, root_agent_id)
|
|
165
|
+
values (v_cl, 'agent', 'gr-cl-'||s, 'CL', 'active', u_cl, v_root, v_root);
|
|
166
|
+
insert into agent_board.delegation_grants(child_agent_id, root_agent_id, parent_agent_id, delegation_policy_id, granted_scopes, grant_expires_at)
|
|
167
|
+
values (v_cl, v_root, v_root, v_pol, jsonb_build_array('scope:a'), now() + interval '2 hours');
|
|
168
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
169
|
+
values (k_cl, v_cl, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_cl::text,'-',''),43,'A'), 'active', 'proven');
|
|
170
|
+
insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at)
|
|
171
|
+
values
|
|
172
|
+
(ch_cl, k_cl, v_cl, ch_cl::text, 'x', 'pending', now() + interval '10 minutes'),
|
|
173
|
+
(ch_wrong, k_cl, v_cl, ch_wrong::text, 'x', 'pending', now() + interval '10 minutes');
|
|
174
|
+
|
|
175
|
+
-- Independent agent with an ACTIVE key whose expires_at is in the past (#12).
|
|
176
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id)
|
|
177
|
+
values (v_et, 'agent', 'gr-et-'||s, 'ET', 'active', u_et);
|
|
178
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state, expires_at)
|
|
179
|
+
values (k_et, v_et, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(k_et::text,'-',''),43,'A'), 'active', 'proven', now() - interval '1 minute');
|
|
180
|
+
insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at)
|
|
181
|
+
values (ch_et, k_et, v_et, ch_et::text, 'x', 'pending', now() + interval '10 minutes');
|
|
182
|
+
|
|
183
|
+
insert into _grant_fx values (v_ce, k_ce, ch_ce, v_cl, k_cl, ch_cl, ch_wrong, v_et, k_et, ch_et);
|
|
184
|
+
end
|
|
185
|
+
$$;
|
|
186
|
+
|
|
187
|
+
-- Function existence -----------------------------------------------------------
|
|
188
|
+
select has_function('agent_board', 'mint_fleet_agent_session', array['uuid','text','uuid','uuid','integer'], 'mint_fleet_agent_session exists');
|
|
189
|
+
select has_function('agent_board', '_agent_principal_active', array['uuid'], '_agent_principal_active exists');
|
|
190
|
+
|
|
191
|
+
-- _agent_principal_active lineage semantics ------------------------------------
|
|
192
|
+
select is(agent_board._agent_principal_active((select a1 from _mint_fx)), true, 'active unparented agent is principal-active');
|
|
193
|
+
select is(agent_board._agent_principal_active((select c2 from _mint_fx)), true, 'active full lineage (child + active root) is principal-active');
|
|
194
|
+
select is(agent_board._agent_principal_active((select c_susp from _mint_fx)), false, 'a non-active (suspended) ancestor fails the whole principal');
|
|
195
|
+
select is(agent_board._agent_principal_active((select a5 from _mint_fx)), false, 'a non-active agent is not principal-active');
|
|
196
|
+
select is(agent_board._agent_principal_active(gen_random_uuid()), false, 'a non-existent agent is not principal-active');
|
|
197
|
+
|
|
198
|
+
-- Happy mint + TTL cap ---------------------------------------------------------
|
|
199
|
+
select lives_ok(
|
|
200
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_happy, key_a1, a1) from _mint_fx),
|
|
201
|
+
'mint succeeds for an active principal with a valid unconsumed challenge and active key');
|
|
202
|
+
select is(
|
|
203
|
+
(select count(*)::int from agent_board.nonfleet_sessions
|
|
204
|
+
where agent_id = (select a1 from _mint_fx) and identity_class = 'fleet_agent_session'),
|
|
205
|
+
1, 'the happy mint inserted exactly one fleet_agent_session row for the agent');
|
|
206
|
+
select lives_ok(
|
|
207
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid, 3600)$q$, ch_ttl, key_a1, a1) from _mint_fx),
|
|
208
|
+
'mint with p_max_ttl_seconds = 3600 succeeds');
|
|
209
|
+
select is(
|
|
210
|
+
(select max(max_ttl_seconds) from agent_board.nonfleet_sessions
|
|
211
|
+
where agent_id = (select a1 from _mint_fx) and identity_class = 'fleet_agent_session'),
|
|
212
|
+
900, 'mint caps max_ttl_seconds at 900 even when 3600 is requested');
|
|
213
|
+
select ok(
|
|
214
|
+
(select bool_and(expires_at <= now() + interval '900 seconds') from agent_board.nonfleet_sessions
|
|
215
|
+
where agent_id = (select a1 from _mint_fx) and identity_class = 'fleet_agent_session'),
|
|
216
|
+
'every minted fleet_agent_session expires within 900 seconds');
|
|
217
|
+
|
|
218
|
+
-- Rejection matrix (each raises; no row minted) --------------------------------
|
|
219
|
+
select throws_ok(
|
|
220
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_revkey, key_a3, a3) from _mint_fx),
|
|
221
|
+
'42501', null, 'mint rejects a revoked signing key');
|
|
222
|
+
select throws_ok(
|
|
223
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_expkey, key_a4, a4) from _mint_fx),
|
|
224
|
+
'42501', null, 'mint rejects an expired signing key');
|
|
225
|
+
select throws_ok(
|
|
226
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_inactagent, key_a5, a5) from _mint_fx),
|
|
227
|
+
'42501', null, 'mint rejects a non-active agent');
|
|
228
|
+
select throws_ok(
|
|
229
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_inactanc, key_csusp, c_susp) from _mint_fx),
|
|
230
|
+
'42501', null, 'mint rejects an agent with a non-active ancestor (_agent_principal_active false)');
|
|
231
|
+
select throws_ok(
|
|
232
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_consumed, key_a1, a1) from _mint_fx),
|
|
233
|
+
'23505', null, 'mint rejects an already-consumed challenge');
|
|
234
|
+
select throws_ok(
|
|
235
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_expired, key_a1, a1) from _mint_fx),
|
|
236
|
+
'22023', null, 'mint rejects an expired challenge');
|
|
237
|
+
select throws_ok(
|
|
238
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_mismatch, key_a1, a6) from _mint_fx),
|
|
239
|
+
'42501', null, 'mint rejects a challenge not bound to the supplied agent');
|
|
240
|
+
|
|
241
|
+
-- Refresh-after-revocation: KEY revoked after a successful mint -----------------
|
|
242
|
+
select lives_ok(
|
|
243
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_refkey_a, key_a7, a7) from _mint_fx),
|
|
244
|
+
'refresh(key): first mint succeeds while the key is active');
|
|
245
|
+
update agent_board.agent_signing_keys
|
|
246
|
+
set status = 'revoked', proof_state = 'revoked', revoked_at = now(), updated_at = now()
|
|
247
|
+
where id = (select key_a7 from _mint_fx);
|
|
248
|
+
select throws_ok(
|
|
249
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_refkey_b, key_a7, a7) from _mint_fx),
|
|
250
|
+
'42501', null, 'refresh(key): a fresh re-mint AFTER key revocation raises (not old-token expiry)');
|
|
251
|
+
|
|
252
|
+
-- Refresh-after-revocation: ANCESTOR suspended after a successful mint ----------
|
|
253
|
+
select lives_ok(
|
|
254
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_refanc_a, key_c2, c2) from _mint_fx),
|
|
255
|
+
'refresh(ancestor): first mint succeeds while the whole lineage is active');
|
|
256
|
+
update agent_board.agents set status = 'suspended', updated_at = now()
|
|
257
|
+
where id = (select r2 from _mint_fx);
|
|
258
|
+
select throws_ok(
|
|
259
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_refanc_b, key_c2, c2) from _mint_fx),
|
|
260
|
+
'42501', null, 'refresh(ancestor): a fresh re-mint AFTER suspending an ancestor raises');
|
|
261
|
+
|
|
262
|
+
-- #4 governing delegation-grant liveness -------------------------------------
|
|
263
|
+
select throws_ok(
|
|
264
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_exp, key_exp, c_exp) from _grant_fx),
|
|
265
|
+
'42501', null, 'mint rejects a root_minted child whose governing delegation grant is expired (#4)');
|
|
266
|
+
select lives_ok(
|
|
267
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_live, key_live, c_live) from _grant_fx),
|
|
268
|
+
'mint succeeds for a root_minted child whose governing grant is live (#4 positive control)');
|
|
269
|
+
|
|
270
|
+
-- #10 nonce binding: a correct challenge/key/agent but WRONG nonce is rejected ----
|
|
271
|
+
select throws_ok(
|
|
272
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %2$L::text, %3$L::uuid, %4$L::uuid)$q$, ch_wrong, 'not-the-bound-nonce', key_live, c_live) from _grant_fx),
|
|
273
|
+
'42501', null, 'mint rejects a valid challenge presented with the wrong nonce (#10)');
|
|
274
|
+
|
|
275
|
+
-- #12 mint also rejects an active key past its expires_at -------------------------
|
|
276
|
+
select throws_ok(
|
|
277
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch_expts, key_expts, a_expts) from _grant_fx),
|
|
278
|
+
'42501', null, 'mint rejects an active signing key whose expires_at is in the past (#12)');
|
|
279
|
+
|
|
280
|
+
-- Privileges -------------------------------------------------------------------
|
|
281
|
+
select function_privs_are(
|
|
282
|
+
'agent_board', 'mint_fleet_agent_session', array['uuid','text','uuid','uuid','integer'],
|
|
283
|
+
'authenticated', array[]::text[], 'mint_fleet_agent_session execute is revoked from authenticated');
|
|
284
|
+
select function_privs_are(
|
|
285
|
+
'agent_board', '_agent_principal_active', array['uuid'],
|
|
286
|
+
'authenticated', array[]::text[], '_agent_principal_active execute is revoked from authenticated');
|
|
287
|
+
|
|
288
|
+
select finish();
|
|
289
|
+
|
|
290
|
+
rollback;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
-- MBSESSION SL-1 pgTAP: reap_expired_nonfleet_sessions removes only non-revoked rows
|
|
2
|
+
-- past expires_at (subject to the grace interval), returns the affected count, and is
|
|
3
|
+
-- revoked from authenticated.
|
|
4
|
+
create extension if not exists pgtap with schema extensions;
|
|
5
|
+
set search_path = extensions, public, agent_board;
|
|
6
|
+
|
|
7
|
+
begin;
|
|
8
|
+
|
|
9
|
+
select plan(10);
|
|
10
|
+
|
|
11
|
+
select has_function(
|
|
12
|
+
'agent_board', 'reap_expired_nonfleet_sessions', array['interval'],
|
|
13
|
+
'reap_expired_nonfleet_sessions(interval) exists');
|
|
14
|
+
|
|
15
|
+
-- Seed: expired-old (>7d), expired-recent (<7d), future, and revoked-but-expired.
|
|
16
|
+
insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
17
|
+
values
|
|
18
|
+
('reap-old', 'reap-old', 'fleet_agent_session', now() - interval '30 days', 900),
|
|
19
|
+
('reap-recent', 'reap-recent', 'fleet_agent_session', now() - interval '1 hour', 900),
|
|
20
|
+
('reap-future', 'reap-future', 'fleet_agent_session', now() + interval '10 minutes', 900);
|
|
21
|
+
insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds, revoked_at)
|
|
22
|
+
values
|
|
23
|
+
('reap-revoked', 'reap-revoked', 'fleet_agent_session', now() - interval '30 days', 900, now());
|
|
24
|
+
|
|
25
|
+
-- Default 7-day grace: removes only the 30-day-old non-revoked row.
|
|
26
|
+
select is(
|
|
27
|
+
agent_board.reap_expired_nonfleet_sessions(),
|
|
28
|
+
1::bigint, 'default (7 day) reap removes exactly the one row expired > 7 days ago');
|
|
29
|
+
select ok(
|
|
30
|
+
not exists (select 1 from agent_board.nonfleet_sessions where id = 'reap-old'),
|
|
31
|
+
'the 30-day-old row was deleted');
|
|
32
|
+
select ok(
|
|
33
|
+
exists (select 1 from agent_board.nonfleet_sessions where id = 'reap-recent'),
|
|
34
|
+
'the 1-hour-old row survives the 7-day grace');
|
|
35
|
+
select ok(
|
|
36
|
+
exists (select 1 from agent_board.nonfleet_sessions where id = 'reap-revoked'),
|
|
37
|
+
'a revoked row is never reaped (partial-index predicate revoked_at is null)');
|
|
38
|
+
|
|
39
|
+
-- Zero grace: removes any non-revoked row past expires_at (the recent one) but not future/revoked.
|
|
40
|
+
select is(
|
|
41
|
+
agent_board.reap_expired_nonfleet_sessions(interval '0 seconds'),
|
|
42
|
+
1::bigint, 'zero-grace reap removes exactly the remaining past-expires_at non-revoked row');
|
|
43
|
+
select ok(
|
|
44
|
+
not exists (select 1 from agent_board.nonfleet_sessions where id = 'reap-recent'),
|
|
45
|
+
'the 1-hour-old row is deleted under zero grace');
|
|
46
|
+
select ok(
|
|
47
|
+
exists (select 1 from agent_board.nonfleet_sessions where id = 'reap-future'),
|
|
48
|
+
'a not-yet-expired row is never reaped');
|
|
49
|
+
select ok(
|
|
50
|
+
exists (select 1 from agent_board.nonfleet_sessions where id = 'reap-revoked'),
|
|
51
|
+
'the revoked row still survives zero-grace reaping');
|
|
52
|
+
|
|
53
|
+
-- Privilege: revoked from authenticated.
|
|
54
|
+
select function_privs_are(
|
|
55
|
+
'agent_board', 'reap_expired_nonfleet_sessions', array['interval'],
|
|
56
|
+
'authenticated', array[]::text[],
|
|
57
|
+
'reap_expired_nonfleet_sessions execute is revoked from authenticated');
|
|
58
|
+
|
|
59
|
+
select finish();
|
|
60
|
+
|
|
61
|
+
rollback;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
-- MBFED SL-2 pgTAP (Lane B): the load-bearing Portal-offline negative-dependency
|
|
2
|
+
-- invariant (IF-5).
|
|
3
|
+
--
|
|
4
|
+
-- Behavioral (primary): a FEDERATED-ENROLLED agent (established via
|
|
5
|
+
-- enroll_federated_identity, then given a signing key) completes the full lifecycle
|
|
6
|
+
-- PoP auth -> mint_fleet_agent_session -> send -> revoke with NO live Portal present
|
|
7
|
+
-- and /enroll unreachable, and board writes are unaffected. Nothing on this path reads
|
|
8
|
+
-- a Portal issuer/JWKS/URL — the pgTAP replay contacts no network and depends on no
|
|
9
|
+
-- Portal artifact beyond the inert allowlist seed.
|
|
10
|
+
--
|
|
11
|
+
-- Grep (paired supplement): no write/session-path RPC body (the 8 core send RPCs +
|
|
12
|
+
-- mint_fleet_agent_session) references a Portal issuer/JWKS/OIDC string or the /enroll
|
|
13
|
+
-- surface. The Portal surface is confined to supabase/functions/enroll/** + the
|
|
14
|
+
-- identity_providers seed (the file-side session/** boundary is enforced by
|
|
15
|
+
-- supabase/functions/session/session.test.ts). Runs as postgres.
|
|
16
|
+
create extension if not exists pgtap with schema extensions;
|
|
17
|
+
set search_path = extensions, public, agent_board;
|
|
18
|
+
|
|
19
|
+
begin;
|
|
20
|
+
|
|
21
|
+
select plan(10);
|
|
22
|
+
|
|
23
|
+
create temp table _pofx (
|
|
24
|
+
agent uuid, auth_user uuid, key uuid, ch uuid, board uuid, thread uuid
|
|
25
|
+
) on commit drop;
|
|
26
|
+
|
|
27
|
+
do $$
|
|
28
|
+
declare
|
|
29
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
30
|
+
v_agent uuid; v_auth uuid;
|
|
31
|
+
v_key uuid := gen_random_uuid();
|
|
32
|
+
v_ch uuid := gen_random_uuid();
|
|
33
|
+
v_board uuid; v_thread uuid := gen_random_uuid();
|
|
34
|
+
-- A valid unique 43-char base64url enrollment key + its matching (RPC-recomputed) fp.
|
|
35
|
+
v_pub text := rtrim(translate(encode(extensions.digest(convert_to('k-pofx-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
36
|
+
v_fp text;
|
|
37
|
+
begin
|
|
38
|
+
v_fp := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
39
|
+
-- Approved test provider (in-transaction only; the shipped seed stays inert).
|
|
40
|
+
insert into agent_board.identity_providers
|
|
41
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
42
|
+
values ('https://mock-portal-'||s||'.test', 'message-board', true, true, '{board:send}', '{}', '{agent}');
|
|
43
|
+
|
|
44
|
+
-- Federated enrollment (PoP-at-enroll) establishes the human owner + the agent +
|
|
45
|
+
-- its own auth.users + an active proven signing key (enrollment-only Portal touch).
|
|
46
|
+
select e.agent_id, e.auth_user_id into v_agent, v_auth
|
|
47
|
+
from agent_board.enroll_federated_identity(
|
|
48
|
+
'https://mock-portal-'||s||'.test', 'message-board', 'portal|'||s,
|
|
49
|
+
jsonb_build_object('sub','portal|'||s),
|
|
50
|
+
'pofx-'||s, v_pub, 'raw_ed25519_base64url', v_fp, true, null, 'agent', '{board:send}', '{}') e;
|
|
51
|
+
|
|
52
|
+
-- A SECOND signing key (key_version 2; enroll auto-enrolled version 1) + a pending
|
|
53
|
+
-- challenge bound to it, so the mint step below can drive PoP -> mint deterministically.
|
|
54
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
55
|
+
values (v_key, v_agent, 'primary', 2, repeat('A',43), 'raw_ed25519_base64url',
|
|
56
|
+
'sha256:'||rpad(replace(v_key::text,'-',''),43,'A'), 'active', 'proven');
|
|
57
|
+
insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at, consumed_at)
|
|
58
|
+
values (v_ch, v_key, v_agent, v_ch::text, 'agent-signing-key-proof.v1:'||v_ch||':n', 'pending', now() + interval '10 minutes', null);
|
|
59
|
+
|
|
60
|
+
-- A board + membership + thread so the enrolled agent can send.
|
|
61
|
+
select board_id into v_board
|
|
62
|
+
from agent_board.create_board(v_agent, 'pofx-board-'||s, 'POFX Board', 'portal-offline', '{}'::jsonb, 'private');
|
|
63
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
64
|
+
values (v_board, v_agent, 'joined', v_agent, 'member');
|
|
65
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
66
|
+
values (v_thread, 'coordination', v_agent, 'POFX thread');
|
|
67
|
+
|
|
68
|
+
insert into _pofx values (v_agent, v_auth, v_key, v_ch, v_board, v_thread);
|
|
69
|
+
end
|
|
70
|
+
$$;
|
|
71
|
+
|
|
72
|
+
-- The federated-enrolled agent is a fully active board principal. -----------------
|
|
73
|
+
select is(agent_board._agent_principal_active((select agent from _pofx)), true,
|
|
74
|
+
'the federated-enrolled agent is principal-active (no Portal dependency)');
|
|
75
|
+
|
|
76
|
+
-- PoP auth -> mint: the DB session mint succeeds with no Portal artifact. ----------
|
|
77
|
+
select lives_ok(
|
|
78
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, ch, key, agent) from _pofx),
|
|
79
|
+
'mint_fleet_agent_session succeeds for the enrolled agent (Portal-offline)');
|
|
80
|
+
select is(
|
|
81
|
+
(select count(*)::int from agent_board.nonfleet_sessions
|
|
82
|
+
where agent_id = (select agent from _pofx) and identity_class = 'fleet_agent_session'),
|
|
83
|
+
1, 'the mint inserted exactly one fleet_agent_session row');
|
|
84
|
+
|
|
85
|
+
-- send: an authenticated board-member send succeeds (board writes unaffected). -----
|
|
86
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
87
|
+
select set_config('request.jwt.claim.sub', (select auth_user from _pofx)::text, true);
|
|
88
|
+
select is(agent_board._is_service_role(), false, 'send runs on the authenticated path (not service_role)');
|
|
89
|
+
select lives_ok(
|
|
90
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body')$q$,
|
|
91
|
+
thread, agent, board) from _pofx),
|
|
92
|
+
'the enrolled agent completes a board-member send (Portal-offline behavioral)');
|
|
93
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
94
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
95
|
+
|
|
96
|
+
-- revoke: the lifecycle terminates cleanly; the agent is no longer active. ---------
|
|
97
|
+
select lives_ok(
|
|
98
|
+
(select format($q$select agent_board.revoke_agent(%L::uuid, 'portal-offline lifecycle test')$q$, agent) from _pofx),
|
|
99
|
+
'revoke_agent succeeds (auth -> mint -> send -> revoke completes Portal-independently)');
|
|
100
|
+
select isnt(
|
|
101
|
+
(select status from agent_board.agents where id = (select agent from _pofx)),
|
|
102
|
+
'active', 'the revoked agent is no longer active');
|
|
103
|
+
|
|
104
|
+
-- Grep (paired supplement): the DB write/session path references no Portal string. --
|
|
105
|
+
select is(
|
|
106
|
+
(select count(*)::int from (
|
|
107
|
+
select pg_get_functiondef(p.oid) as def
|
|
108
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
109
|
+
where n.nspname = 'agent_board'
|
|
110
|
+
and p.proname in (
|
|
111
|
+
'mint_fleet_agent_session', 'post_board_message', 'send_direct_message',
|
|
112
|
+
'send_multicast_message', 'create_tasking_delivery')
|
|
113
|
+
) d
|
|
114
|
+
where lower(d.def) ~ '(portal|jwks|\.well-known|openid|oidc)'),
|
|
115
|
+
0, 'no write/session-path RPC body references a Portal issuer/JWKS/OIDC string');
|
|
116
|
+
select is(
|
|
117
|
+
(select count(*)::int from (
|
|
118
|
+
select pg_get_functiondef(p.oid) as def
|
|
119
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
120
|
+
where n.nspname = 'agent_board'
|
|
121
|
+
and p.proname in (
|
|
122
|
+
'mint_fleet_agent_session', 'post_board_message', 'send_direct_message',
|
|
123
|
+
'send_multicast_message', 'create_tasking_delivery')
|
|
124
|
+
) d
|
|
125
|
+
where lower(d.def) ~ '(enroll_federated_identity|/enroll)'),
|
|
126
|
+
0, 'no write/session-path RPC body references the /enroll enrollment surface');
|
|
127
|
+
|
|
128
|
+
select finish();
|
|
129
|
+
|
|
130
|
+
rollback;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
-- MBDELEG SL-2 pgTAP (IF-0-MBDELEG-4): re-signing refresh + credential demotion.
|
|
2
|
+
--
|
|
3
|
+
-- Proves that after the migration: rotate_agent_token (the 30-day refresh exchange)
|
|
4
|
+
-- RAISES a deprecation error (no session issued); a newly issued agent_credentials
|
|
5
|
+
-- row is an enrollment/lineage record (metadata.credential_role='enrollment', not
|
|
6
|
+
-- session-exchangeable); and re-signing STILL works — a valid PoP challenge ->
|
|
7
|
+
-- mint_fleet_agent_session mints a <=900s session for the same agent.
|
|
8
|
+
--
|
|
9
|
+
-- Runs as postgres (service_role-equivalent; provision_agent + mint are service-side).
|
|
10
|
+
create extension if not exists pgtap with schema extensions;
|
|
11
|
+
set search_path = extensions, public, agent_board;
|
|
12
|
+
|
|
13
|
+
begin;
|
|
14
|
+
|
|
15
|
+
select plan(5);
|
|
16
|
+
|
|
17
|
+
create temp table _resign_fx (agent_id uuid, credential_id uuid, key_id uuid, challenge_id uuid) on commit drop;
|
|
18
|
+
|
|
19
|
+
do $$
|
|
20
|
+
declare
|
|
21
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
22
|
+
u_owner uuid;
|
|
23
|
+
v_agent uuid; v_cred uuid;
|
|
24
|
+
v_key uuid := gen_random_uuid(); v_ch uuid := gen_random_uuid();
|
|
25
|
+
begin
|
|
26
|
+
u_owner := public.ensure_github_user('resign-own-'||s, 'resign-own-'||s||'@example.test', 'resign-own-'||s, 'OWN');
|
|
27
|
+
|
|
28
|
+
-- Provision an agent through the credential choke point (_issue_agent_credential).
|
|
29
|
+
select agent_id, credential_id into v_agent, v_cred
|
|
30
|
+
from agent_board.provision_agent('resign-a-'||s, 'agent', '[]'::jsonb, u_owner);
|
|
31
|
+
|
|
32
|
+
-- Enroll an active, proven signing key + a pending challenge so the agent can re-sign.
|
|
33
|
+
insert into agent_board.agent_signing_keys(id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
34
|
+
values (v_key, v_agent, 'primary', 1, repeat('A',43), 'raw_ed25519_base64url', 'sha256:'||rpad(replace(v_key::text,'-',''),43,'A'), 'active', 'proven');
|
|
35
|
+
insert into agent_board.agent_signing_key_challenges(id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, proof_state, expires_at)
|
|
36
|
+
values (v_ch, v_key, v_agent, v_ch::text, 'agent-signing-key-proof.v1:'||v_ch||':n', 'pending', now() + interval '10 minutes');
|
|
37
|
+
|
|
38
|
+
insert into _resign_fx values (v_agent, v_cred, v_key, v_ch);
|
|
39
|
+
end
|
|
40
|
+
$$;
|
|
41
|
+
|
|
42
|
+
-- 1. rotate_agent_token (the 30-day refresh exchange) is retired -> raises 0A000.
|
|
43
|
+
select throws_ok(
|
|
44
|
+
(select format($q$select agent_board.rotate_agent_token(%L::uuid)$q$, agent_id) from _resign_fx),
|
|
45
|
+
'0A000', null, 'rotate_agent_token raises a deprecation error (no session exchanged)');
|
|
46
|
+
|
|
47
|
+
-- 2. The issued credential is an enrollment/lineage record, not a session token.
|
|
48
|
+
select is(
|
|
49
|
+
(select ac.metadata->>'credential_role' from agent_board.agent_credentials ac
|
|
50
|
+
where ac.id = (select credential_id from _resign_fx)),
|
|
51
|
+
'enrollment', 'a newly issued agent_credentials row is tagged credential_role=enrollment');
|
|
52
|
+
|
|
53
|
+
-- 3. Re-signing still refreshes: PoP challenge -> mint_fleet_agent_session mints.
|
|
54
|
+
select lives_ok(
|
|
55
|
+
(select format($q$select agent_board.mint_fleet_agent_session(%1$L::uuid, %1$L::text, %2$L::uuid, %3$L::uuid)$q$, challenge_id, key_id, agent_id) from _resign_fx),
|
|
56
|
+
're-signing still refreshes: a valid PoP -> mint_fleet_agent_session mints a session');
|
|
57
|
+
|
|
58
|
+
-- 4. Exactly one fleet_agent_session was minted for the agent.
|
|
59
|
+
select is(
|
|
60
|
+
(select count(*)::int from agent_board.nonfleet_sessions
|
|
61
|
+
where agent_id = (select agent_id from _resign_fx) and identity_class = 'fleet_agent_session'),
|
|
62
|
+
1, 're-signing minted exactly one fleet_agent_session for the agent');
|
|
63
|
+
|
|
64
|
+
-- 5. The minted session is pinned to <=900s.
|
|
65
|
+
select ok(
|
|
66
|
+
(select bool_and(max_ttl_seconds <= 900 and expires_at <= now() + interval '900 seconds')
|
|
67
|
+
from agent_board.nonfleet_sessions
|
|
68
|
+
where agent_id = (select agent_id from _resign_fx) and identity_class = 'fleet_agent_session'),
|
|
69
|
+
'the re-signed session is pinned to a <=900s TTL');
|
|
70
|
+
|
|
71
|
+
select finish();
|
|
72
|
+
|
|
73
|
+
rollback;
|