@consiliency/agent-board-schema 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent-board-schema.js +0 -0
- package/fixtures/already-applied.json +112 -0
- package/migrations/20260717000100_agent_board_board_role.sql +180 -0
- package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
- package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
- package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
- package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
- package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
- package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
- package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
- package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
- package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
- package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
- package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
- package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
- package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
- package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
- package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
- package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
- package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
- package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
- package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
- package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
- package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
- package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
- package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
- package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
- package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
- package/migrations/20260723000100_agent_board_roomown_owner_seed.sql +167 -0
- package/migrations/20260723000200_agent_board_roomown_grant_role_live.sql +72 -0
- package/package.json +9 -9
- package/schema-manifest.json +256 -4
- package/src/tooling.test.mjs +46 -1
- package/tests/agent_board_agent_lineage.test.sql +73 -0
- package/tests/agent_board_agents_status_revoked.test.sql +60 -0
- package/tests/agent_board_auth_rpcs.test.sql +17 -16
- package/tests/agent_board_board_role.test.sql +129 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
- package/tests/agent_board_delegation_enforcement.test.sql +175 -0
- package/tests/agent_board_delegation_policies.test.sql +120 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
- package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
- package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
- package/tests/agent_board_enrollment_audit_content.test.sql +139 -0
- package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
- package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
- package/tests/agent_board_fleet_registry.test.sql +2 -2
- package/tests/agent_board_identity_providers.test.sql +104 -0
- package/tests/agent_board_inbox_rpcs.test.sql +10 -10
- package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
- package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +134 -0
- package/tests/agent_board_resigning_refresh.test.sql +73 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
- package/tests/agent_board_roomown.test.sql +233 -0
- package/tests/agent_board_schema_version.test.sql +2 -2
- package/tests/agent_board_schema_version_rpc.test.sql +1 -1
- package/tests/agent_board_scoped_send_regrant.test.sql +142 -0
- package/tests/agent_board_send_authz_config.test.sql +84 -0
- package/tests/agent_board_send_authz_matrix.test.sql +162 -0
- package/tests/agent_board_send_role_gate.test.sql +181 -0
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
- package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
- package/tests/agent_board_thread_acl.test.sql +129 -0
- package/tests/agent_board_v2_invariants.test.sql +5 -5
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
-- MBFED SL-2 pgTAP (Lane B): enrollment-audit content + no-write-path-read invariant.
|
|
2
|
+
--
|
|
3
|
+
-- Proves enroll_federated_identity writes exactly ONE enrollment_federation_audit row
|
|
4
|
+
-- whose human_identity_claim equals the supplied snapshot verbatim, that the audit
|
|
5
|
+
-- table carries NO Portal session/MFA/availability/liveness column (it is a static
|
|
6
|
+
-- enrollment-time claim snapshot), and that NO write-path RPC reads that table — the
|
|
7
|
+
-- audit is never consulted as a live trust signal. Runs as postgres.
|
|
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(11);
|
|
14
|
+
|
|
15
|
+
create temp table _acfx (agent uuid, claim jsonb) on commit drop;
|
|
16
|
+
|
|
17
|
+
do $$
|
|
18
|
+
declare
|
|
19
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
20
|
+
v_agent uuid;
|
|
21
|
+
v_claim jsonb := jsonb_build_object('sub','portal|'||s,'name','Fed '||s,'groups',jsonb_build_array('eng','fed'));
|
|
22
|
+
-- A valid unique 43-char base64url key + its matching (RPC-recomputed) fingerprint.
|
|
23
|
+
v_pub text := rtrim(translate(encode(extensions.digest(convert_to('k-acfx-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
24
|
+
v_fp text;
|
|
25
|
+
begin
|
|
26
|
+
v_fp := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
27
|
+
insert into agent_board.identity_providers
|
|
28
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
29
|
+
values ('https://mock-portal-'||s||'.test', 'message-board', true, true, '{board:send}', '{board:x}', '{agent,human}');
|
|
30
|
+
|
|
31
|
+
select e.agent_id into v_agent
|
|
32
|
+
from agent_board.enroll_federated_identity(
|
|
33
|
+
'https://mock-portal-'||s||'.test', 'message-board', 'portal|'||s,
|
|
34
|
+
v_claim, 'acfx-'||s, v_pub, 'raw_ed25519_base64url', v_fp, true, null, 'human', '{board:send}', '{board:x}') e;
|
|
35
|
+
|
|
36
|
+
insert into _acfx values (v_agent, v_claim);
|
|
37
|
+
end
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
-- Exactly one audit row, with the verbatim claim snapshot. -------------------------
|
|
41
|
+
select is(
|
|
42
|
+
(select count(*)::int from agent_board.enrollment_federation_audit where agent_id = (select agent from _acfx)),
|
|
43
|
+
1, 'enroll_federated_identity writes exactly one enrollment_federation_audit row');
|
|
44
|
+
select is(
|
|
45
|
+
(select human_identity_claim from agent_board.enrollment_federation_audit where agent_id = (select agent from _acfx)),
|
|
46
|
+
(select claim from _acfx),
|
|
47
|
+
'human_identity_claim equals the supplied snapshot verbatim');
|
|
48
|
+
select is(
|
|
49
|
+
(select granted_scopes from agent_board.enrollment_federation_audit where agent_id = (select agent from _acfx)),
|
|
50
|
+
'{board:send}'::text[],
|
|
51
|
+
'granted_scopes records the in-ceiling requested scopes');
|
|
52
|
+
|
|
53
|
+
-- The audit is a STATIC snapshot: no Portal session/MFA/availability/liveness column.
|
|
54
|
+
select hasnt_column('agent_board', 'enrollment_federation_audit', 'portal_session_id',
|
|
55
|
+
'audit has no portal_session_id column');
|
|
56
|
+
select is(
|
|
57
|
+
(select count(*)::int
|
|
58
|
+
from information_schema.columns
|
|
59
|
+
where table_schema = 'agent_board'
|
|
60
|
+
and table_name = 'enrollment_federation_audit'
|
|
61
|
+
and (column_name ~* '(session|mfa|liveness|availability|reachab|online|heartbeat)')),
|
|
62
|
+
0, 'audit table carries no Portal session/MFA/liveness/availability column (static snapshot only)');
|
|
63
|
+
|
|
64
|
+
-- The audit exists and stores the enrollment-time snapshot, not a live signal. ------
|
|
65
|
+
select has_column('agent_board', 'enrollment_federation_audit', 'human_identity_claim', 'audit stores human_identity_claim');
|
|
66
|
+
select has_column('agent_board', 'enrollment_federation_audit', 'enrolled_at', 'audit stores enrolled_at (enrollment-time)');
|
|
67
|
+
|
|
68
|
+
-- No write-path RPC reads enrollment_federation_audit. Only the enroll RPC (which
|
|
69
|
+
-- WRITES it) may reference the table; nothing on the send/session path reads it. ----
|
|
70
|
+
select is(
|
|
71
|
+
(select count(*)::int from (
|
|
72
|
+
select pg_get_functiondef(p.oid) as def, p.proname
|
|
73
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
74
|
+
where n.nspname = 'agent_board'
|
|
75
|
+
-- pg_get_functiondef() RAISES on aggregate ('a') / window ('w') procs
|
|
76
|
+
-- ("X is an aggregate function"); restrict to callable functions +
|
|
77
|
+
-- procedures (the RPC surface this invariant is about).
|
|
78
|
+
and p.prokind in ('f', 'p')
|
|
79
|
+
and p.proname <> 'enroll_federated_identity'
|
|
80
|
+
) d
|
|
81
|
+
where lower(d.def) like '%enrollment_federation_audit%'),
|
|
82
|
+
0, 'no agent_board RPC other than enroll_federated_identity references enrollment_federation_audit');
|
|
83
|
+
select is(
|
|
84
|
+
(select count(*)::int from (
|
|
85
|
+
select pg_get_functiondef(p.oid) as def
|
|
86
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
87
|
+
where n.nspname = 'agent_board'
|
|
88
|
+
and p.prokind in ('f', 'p') -- pg_get_functiondef raises on aggregate/window procs
|
|
89
|
+
and p.proname in (
|
|
90
|
+
'mint_fleet_agent_session', 'post_board_message', 'send_direct_message',
|
|
91
|
+
'send_multicast_message', 'create_tasking_delivery', 'revoke_agent')
|
|
92
|
+
) d
|
|
93
|
+
where lower(d.def) like '%enrollment_federation_audit%'),
|
|
94
|
+
0, 'no send/session/revoke write-path RPC reads the enrollment audit table');
|
|
95
|
+
|
|
96
|
+
-- #6 fix: the authenticated SELECT policy is scoped to the owning principal
|
|
97
|
+
-- (agent_id = current_agent_id()), not `using (true)`. Two enrolled human
|
|
98
|
+
-- principals; principal A must NOT see principal B's PII-bearing audit row.
|
|
99
|
+
create temp table _rls (a_agent uuid, a_user uuid, b_agent uuid) on commit drop;
|
|
100
|
+
do $$
|
|
101
|
+
declare
|
|
102
|
+
s text := replace(substr(gen_random_uuid()::text,1,8),'-','');
|
|
103
|
+
v_a_agent uuid; v_a_user uuid; v_b_agent uuid;
|
|
104
|
+
v_pub_a text := rtrim(translate(encode(extensions.digest(convert_to('k-rls-a-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
105
|
+
v_pub_b text := rtrim(translate(encode(extensions.digest(convert_to('k-rls-b-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
106
|
+
v_fp_a text; v_fp_b text;
|
|
107
|
+
begin
|
|
108
|
+
v_fp_a := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub_a,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
109
|
+
v_fp_b := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub_b,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
110
|
+
insert into agent_board.identity_providers
|
|
111
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
112
|
+
values ('https://rls-portal-'||s||'.test','message-board',true,true,'{board:send}','{board:x}','{agent,human}');
|
|
113
|
+
select e.agent_id, e.auth_user_id into v_a_agent, v_a_user
|
|
114
|
+
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|a-'||s,
|
|
115
|
+
jsonb_build_object('sub','portal|a-'||s,'name','A'), 'rls-a-'||s, v_pub_a,'raw_ed25519_base64url',v_fp_a,true,null,'human','{board:send}','{board:x}') e;
|
|
116
|
+
select e.agent_id into v_b_agent
|
|
117
|
+
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|b-'||s,
|
|
118
|
+
jsonb_build_object('sub','portal|b-'||s,'name','B'), 'rls-b-'||s, v_pub_b,'raw_ed25519_base64url',v_fp_b,true,null,'human','{board:send}','{board:x}') e;
|
|
119
|
+
insert into _rls values (v_a_agent, v_a_user, v_b_agent);
|
|
120
|
+
end
|
|
121
|
+
$$;
|
|
122
|
+
grant select on _rls to authenticated;
|
|
123
|
+
|
|
124
|
+
set local role authenticated;
|
|
125
|
+
select set_config('request.jwt.claim.role','authenticated', true);
|
|
126
|
+
select set_config('request.jwt.claim.sub', (select a_user from _rls)::text, true);
|
|
127
|
+
|
|
128
|
+
select is(
|
|
129
|
+
(select count(*)::int from agent_board.enrollment_federation_audit e where e.agent_id = (select b_agent from _rls)),
|
|
130
|
+
0, 'agent A cannot read agent B''s enrollment audit row (RLS scoped to owning principal, not using(true))');
|
|
131
|
+
select is(
|
|
132
|
+
(select count(*)::int from agent_board.enrollment_federation_audit e where e.agent_id = (select a_agent from _rls)),
|
|
133
|
+
1, 'agent A can read its own enrollment audit row');
|
|
134
|
+
|
|
135
|
+
reset role;
|
|
136
|
+
|
|
137
|
+
select finish();
|
|
138
|
+
|
|
139
|
+
rollback;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- MBSESSION SL-1 pgTAP: nonfleet_sessions.identity_class admits fleet_agent_session
|
|
2
|
+
-- (plus the 4 legacy classes), rejects an unknown class, and the TTL pin caps a
|
|
3
|
+
-- fleet_agent_session to <= 900s while legacy classes keep the 3600 default.
|
|
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
|
+
-- All five valid identity classes are admitted (TTL within the fleet pin bound).
|
|
12
|
+
select lives_ok(
|
|
13
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
14
|
+
values ('fac-eph', 'fac-eph', 'ephemeral_child', now() + interval '1 hour', 900)$$,
|
|
15
|
+
'ephemeral_child identity_class is admitted');
|
|
16
|
+
select lives_ok(
|
|
17
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
18
|
+
values ('fac-hw', 'fac-hw', 'hosted_workload', now() + interval '1 hour', 900)$$,
|
|
19
|
+
'hosted_workload identity_class is admitted');
|
|
20
|
+
select lives_ok(
|
|
21
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
22
|
+
values ('fac-rs', 'fac-rs', 'remote_sandbox', now() + interval '1 hour', 900)$$,
|
|
23
|
+
'remote_sandbox identity_class is admitted');
|
|
24
|
+
select lives_ok(
|
|
25
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
26
|
+
values ('fac-inv', 'fac-inv', 'invited_external', now() + interval '1 hour', 900)$$,
|
|
27
|
+
'invited_external identity_class is admitted');
|
|
28
|
+
select lives_ok(
|
|
29
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
30
|
+
values ('fac-fas', 'fac-fas', 'fleet_agent_session', now() + interval '15 minutes', 900)$$,
|
|
31
|
+
'fleet_agent_session identity_class is admitted (new 5th class)');
|
|
32
|
+
|
|
33
|
+
-- A legacy class still keeps the 3600 default (fleet pin does not apply to it).
|
|
34
|
+
select lives_ok(
|
|
35
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at)
|
|
36
|
+
values ('fac-hw3600', 'fac-hw3600', 'hosted_workload', now() + interval '1 hour')$$,
|
|
37
|
+
'a legacy class row with the 3600 default is unaffected by the fleet TTL pin');
|
|
38
|
+
|
|
39
|
+
-- An unknown identity_class is rejected by the re-added CHECK.
|
|
40
|
+
select throws_ok(
|
|
41
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
42
|
+
values ('fac-bogus', 'fac-bogus', 'not_a_real_class', now() + interval '1 hour', 900)$$,
|
|
43
|
+
'23514', null,
|
|
44
|
+
'an unknown identity_class violates nonfleet_sessions_identity_class_check');
|
|
45
|
+
|
|
46
|
+
-- fleet_agent_session at exactly 900s is allowed; above 900s violates the TTL pin.
|
|
47
|
+
select lives_ok(
|
|
48
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
49
|
+
values ('fac-fas900', 'fac-fas900', 'fleet_agent_session', now() + interval '15 minutes', 900)$$,
|
|
50
|
+
'fleet_agent_session at the 900s boundary is admitted');
|
|
51
|
+
select throws_ok(
|
|
52
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
53
|
+
values ('fac-fas901', 'fac-fas901', 'fleet_agent_session', now() + interval '15 minutes', 901)$$,
|
|
54
|
+
'23514', null,
|
|
55
|
+
'fleet_agent_session with max_ttl_seconds > 900 violates nonfleet_sessions_fleet_ttl_pin');
|
|
56
|
+
select throws_ok(
|
|
57
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at)
|
|
58
|
+
values ('fac-fas3600', 'fac-fas3600', 'fleet_agent_session', now() + interval '15 minutes')$$,
|
|
59
|
+
'23514', null,
|
|
60
|
+
'fleet_agent_session cannot inherit the 3600 max_ttl_seconds default');
|
|
61
|
+
|
|
62
|
+
select finish();
|
|
63
|
+
|
|
64
|
+
rollback;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-2 pgTAP: the non-fleet fail-open in evaluate_signed_write_fleet_policy
|
|
2
|
+
-- is now mode-gated per sender-fleet-scope (fail_open_preserved / observe / deny)
|
|
3
|
+
-- with a tight non-fleet allowlist. Fleet-resolvable senders are unaffected.
|
|
4
|
+
--
|
|
5
|
+
-- Runs as the postgres db role in a service_role context (the function requires
|
|
6
|
+
-- service role); request.jwt.claim.role is left '' so _is_service_role() is true.
|
|
7
|
+
create extension if not exists pgtap with schema extensions;
|
|
8
|
+
set search_path = extensions, public, agent_board;
|
|
9
|
+
|
|
10
|
+
begin;
|
|
11
|
+
|
|
12
|
+
select plan(13);
|
|
13
|
+
|
|
14
|
+
create temp table _fpd_fixture (
|
|
15
|
+
ff_sender uuid,
|
|
16
|
+
ob_sender uuid,
|
|
17
|
+
dn_sender uuid,
|
|
18
|
+
dn2_sender uuid,
|
|
19
|
+
al_sender uuid,
|
|
20
|
+
fleet_sender uuid,
|
|
21
|
+
owner_agent uuid
|
|
22
|
+
) on commit drop;
|
|
23
|
+
|
|
24
|
+
do $$
|
|
25
|
+
declare
|
|
26
|
+
v_ff uuid := gen_random_uuid();
|
|
27
|
+
v_ob uuid := gen_random_uuid();
|
|
28
|
+
v_dn uuid := gen_random_uuid();
|
|
29
|
+
v_dn2 uuid := gen_random_uuid();
|
|
30
|
+
v_al uuid := gen_random_uuid();
|
|
31
|
+
v_fleet uuid := gen_random_uuid();
|
|
32
|
+
v_owner uuid := gen_random_uuid();
|
|
33
|
+
v_domain uuid;
|
|
34
|
+
v_machine uuid;
|
|
35
|
+
begin
|
|
36
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
37
|
+
values
|
|
38
|
+
(v_ff, 'agent', 'fpd-ff', 'FPD FailOpen'),
|
|
39
|
+
(v_ob, 'agent', 'fpd-ob', 'FPD Observe'),
|
|
40
|
+
(v_dn, 'agent', 'fpd-dn', 'FPD Deny'),
|
|
41
|
+
(v_dn2, 'agent', 'fpd-dn2', 'FPD Deny2'),
|
|
42
|
+
(v_al, 'agent', 'fpd-al', 'FPD Allowlisted'),
|
|
43
|
+
(v_fleet, 'agent', 'fpd-fleet', 'FPD Fleet'),
|
|
44
|
+
(v_owner, 'agent', 'fpd-owner', 'FPD Owner');
|
|
45
|
+
|
|
46
|
+
-- A fleet-resolvable sender: active trust domain + machine + agent mapping.
|
|
47
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at, metadata)
|
|
48
|
+
values ('fpd-tailnet', 'FPD Tailnet', 'tailnet', v_owner, 'active', now(), '{"invitationState":"owner_approved"}'::jsonb);
|
|
49
|
+
select id into v_domain from agent_board.fleet_trust_domains where slug = 'fpd-tailnet';
|
|
50
|
+
select machine_id into v_machine
|
|
51
|
+
from agent_board.register_fleet_machine(v_owner, v_domain, 'fpd-machine', 'sha256:fpd-machine', 'portal', '{"redacted":true}'::jsonb);
|
|
52
|
+
insert into agent_board.fleet_machine_agents(machine_id, agent_id, agent_role)
|
|
53
|
+
values (v_machine, v_fleet, 'phase_worker');
|
|
54
|
+
|
|
55
|
+
insert into _fpd_fixture values (v_ff, v_ob, v_dn, v_dn2, v_al, v_fleet, v_owner);
|
|
56
|
+
end
|
|
57
|
+
$$;
|
|
58
|
+
|
|
59
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
60
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
61
|
+
|
|
62
|
+
-- Signature ------------------------------------------------------------------
|
|
63
|
+
select has_function('agent_board', 'evaluate_signed_write_fleet_policy', array['text', 'jsonb', 'jsonb'],
|
|
64
|
+
'evaluate_signed_write_fleet_policy(text,jsonb,jsonb) exists');
|
|
65
|
+
|
|
66
|
+
-- Phase A: no flag row => fail_open_preserved (unchanged behavior) ------------
|
|
67
|
+
select is(
|
|
68
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
69
|
+
'send_direct_message', '{}'::jsonb,
|
|
70
|
+
jsonb_build_object('agent_id', (select ff_sender from _fpd_fixture)::text, 'verification_status', 'verified'))),
|
|
71
|
+
true, 'no flag row: non-fleet sender passes (fail-open preserved)');
|
|
72
|
+
|
|
73
|
+
select is(
|
|
74
|
+
(select count(*)::int from agent_board.send_authz_telemetry
|
|
75
|
+
where kind = 'would_be_deny' and actor_agent_id = (select ff_sender from _fpd_fixture)),
|
|
76
|
+
0, 'fail_open_preserved records no would_be_deny telemetry');
|
|
77
|
+
|
|
78
|
+
-- Phase B: observe => allow AND record exactly one would_be_deny row ----------
|
|
79
|
+
insert into agent_board.fleet_send_enforcement_flags(scope_key, mode) values ('nonfleet', 'observe');
|
|
80
|
+
|
|
81
|
+
select is(
|
|
82
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
83
|
+
'send_direct_message', '{}'::jsonb,
|
|
84
|
+
jsonb_build_object('agent_id', (select ob_sender from _fpd_fixture)::text,
|
|
85
|
+
'signed_write', jsonb_build_object('origin', 'host')))),
|
|
86
|
+
true, 'observe mode: non-fleet sender still passes');
|
|
87
|
+
|
|
88
|
+
select is(
|
|
89
|
+
(select count(*)::int from agent_board.send_authz_telemetry
|
|
90
|
+
where kind = 'would_be_deny' and actor_agent_id = (select ob_sender from _fpd_fixture)),
|
|
91
|
+
1, 'observe mode records exactly one would_be_deny telemetry row');
|
|
92
|
+
|
|
93
|
+
-- Phase C: deny => deny non-allowlisted, admit allowlisted (tight) -----------
|
|
94
|
+
update agent_board.fleet_send_enforcement_flags set mode = 'deny' where scope_key = 'nonfleet';
|
|
95
|
+
insert into agent_board.nonfleet_sender_allowlist(agent_id, reason)
|
|
96
|
+
values ((select al_sender from _fpd_fixture), 'sl2 test: enumerated legitimate flow');
|
|
97
|
+
|
|
98
|
+
select is(
|
|
99
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
100
|
+
'send_direct_message', '{}'::jsonb,
|
|
101
|
+
jsonb_build_object('agent_id', (select dn_sender from _fpd_fixture)::text))),
|
|
102
|
+
false, 'deny mode: non-allowlisted non-fleet sender is denied');
|
|
103
|
+
|
|
104
|
+
select is(
|
|
105
|
+
(select denial_class from agent_board.evaluate_signed_write_fleet_policy(
|
|
106
|
+
'send_direct_message', '{}'::jsonb,
|
|
107
|
+
jsonb_build_object('agent_id', (select dn_sender from _fpd_fixture)::text))),
|
|
108
|
+
'nonfleet_sender_denied', 'deny mode: denial_class is nonfleet_sender_denied');
|
|
109
|
+
|
|
110
|
+
select is(
|
|
111
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
112
|
+
'send_direct_message', '{}'::jsonb,
|
|
113
|
+
jsonb_build_object('agent_id', (select al_sender from _fpd_fixture)::text))),
|
|
114
|
+
true, 'deny mode: allowlisted non-fleet sender is admitted');
|
|
115
|
+
|
|
116
|
+
-- Tight allowlist: a different, non-enumerated non-fleet sender stays denied.
|
|
117
|
+
select is(
|
|
118
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
119
|
+
'send_direct_message', '{}'::jsonb,
|
|
120
|
+
jsonb_build_object('agent_id', (select dn2_sender from _fpd_fixture)::text))),
|
|
121
|
+
false, 'deny mode: allowlist is tight — a non-enumerated sender is still denied');
|
|
122
|
+
|
|
123
|
+
-- Boardless send_direct_message path is gated (scope resolves before any board).
|
|
124
|
+
select is(
|
|
125
|
+
(select denial_class from agent_board.evaluate_signed_write_fleet_policy(
|
|
126
|
+
'send_direct_message', jsonb_build_object('p_recipient_agent_id', gen_random_uuid()::text),
|
|
127
|
+
jsonb_build_object('agent_id', (select dn_sender from _fpd_fixture)::text))),
|
|
128
|
+
'nonfleet_sender_denied', 'deny mode gates the boardless send_direct_message path (scope before board)');
|
|
129
|
+
|
|
130
|
+
-- Fleet-resolvable sender is unaffected by the non-fleet deny flag.
|
|
131
|
+
-- An unhandled operation returns allowed=true for a fleet sender (v_machine_id
|
|
132
|
+
-- not null), proving it never enters the non-fleet branch.
|
|
133
|
+
select is(
|
|
134
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
135
|
+
'noop_operation', '{}'::jsonb,
|
|
136
|
+
jsonb_build_object('agent_id', (select fleet_sender from _fpd_fixture)::text, 'verification_status', 'verified'))),
|
|
137
|
+
true, 'fleet-resolvable sender is unaffected by the non-fleet deny flag');
|
|
138
|
+
|
|
139
|
+
-- Grants preserved -----------------------------------------------------------
|
|
140
|
+
select function_privs_are(
|
|
141
|
+
'agent_board', 'evaluate_signed_write_fleet_policy', array['text', 'jsonb', 'jsonb'],
|
|
142
|
+
'authenticated', array[]::text[], 'evaluate_signed_write_fleet_policy execute is revoked from authenticated');
|
|
143
|
+
select function_privs_are(
|
|
144
|
+
'agent_board', 'evaluate_signed_write_fleet_policy', array['text', 'jsonb', 'jsonb'],
|
|
145
|
+
'service_role', array['EXECUTE'], 'evaluate_signed_write_fleet_policy execute is granted to service_role');
|
|
146
|
+
|
|
147
|
+
select finish();
|
|
148
|
+
|
|
149
|
+
rollback;
|
|
@@ -40,8 +40,8 @@ select fk_ok('agent_board', 'fleet_machine_agents', 'signing_key_id', 'agent_boa
|
|
|
40
40
|
select fk_ok('agent_board', 'fleet_receive_policies', 'recipient_agent_id', 'agent_board', 'agents', 'id', 'receive policies reference recipient agents');
|
|
41
41
|
|
|
42
42
|
select ok(
|
|
43
|
-
exists (select 1 from agent_board.schema_version where version
|
|
44
|
-
'schema_version advanced to fleet store version 7'
|
|
43
|
+
exists (select 1 from agent_board.schema_version where version >= 7),
|
|
44
|
+
'schema_version advanced to at least fleet store version 7'
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
select ok(
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
-- MBFED SL-1 / MBPORTALGATE SL-1 pgTAP: identity_providers mirrors workload_providers
|
|
2
|
+
-- (shape + RLS + grants), the real Portal contract row is seeded INERT with the frozen
|
|
3
|
+
-- resource-audience + actor_claim, and enroll_federated_identity is service_role-only
|
|
4
|
+
-- (revoked from authenticated). Runs as postgres.
|
|
5
|
+
create extension if not exists pgtap with schema extensions;
|
|
6
|
+
set search_path = extensions, public, agent_board;
|
|
7
|
+
|
|
8
|
+
begin;
|
|
9
|
+
|
|
10
|
+
select plan(21);
|
|
11
|
+
|
|
12
|
+
-- Shape ------------------------------------------------------------------------
|
|
13
|
+
select has_table('agent_board', 'identity_providers', 'identity_providers table exists');
|
|
14
|
+
select col_is_pk('agent_board', 'identity_providers', 'id', 'id is the primary key');
|
|
15
|
+
select col_is_unique('agent_board', 'identity_providers', array['issuer', 'audience'],
|
|
16
|
+
'unique(issuer, audience) mirrors workload_providers');
|
|
17
|
+
select has_column('agent_board', 'identity_providers', 'policy_approved', 'has policy_approved');
|
|
18
|
+
select col_default_is('agent_board', 'identity_providers', 'policy_approved', 'false',
|
|
19
|
+
'policy_approved defaults false (no provider auto-approved)');
|
|
20
|
+
|
|
21
|
+
-- MBPORTALGATE: actor_claim column (forward-only, default 'act.sub') ------------
|
|
22
|
+
select has_column('agent_board', 'identity_providers', 'actor_claim',
|
|
23
|
+
'MBPORTALGATE adds the actor_claim column (forward-only)');
|
|
24
|
+
select col_default_is('agent_board', 'identity_providers', 'actor_claim', 'act.sub',
|
|
25
|
+
'actor_claim defaults to act.sub (audit-only delegated-actor claim)');
|
|
26
|
+
|
|
27
|
+
-- RLS (deny-anon + authenticated-read-true, exactly like workload_providers) ----
|
|
28
|
+
select ok(
|
|
29
|
+
(select relrowsecurity from pg_class c join pg_namespace n on n.oid=c.relnamespace
|
|
30
|
+
where n.nspname='agent_board' and c.relname='identity_providers'),
|
|
31
|
+
'identity_providers has RLS enabled');
|
|
32
|
+
select is(
|
|
33
|
+
(select count(*)::int from pg_policy pol join pg_class c on c.oid=pol.polrelid
|
|
34
|
+
join pg_namespace n on n.oid=c.relnamespace
|
|
35
|
+
where n.nspname='agent_board' and c.relname='identity_providers'
|
|
36
|
+
and pol.polname='identity_providers_deny_anon'),
|
|
37
|
+
1, 'identity_providers_deny_anon policy exists');
|
|
38
|
+
select is(
|
|
39
|
+
(select pg_get_expr(polqual, polrelid) from pg_policy pol join pg_class c on c.oid=pol.polrelid
|
|
40
|
+
join pg_namespace n on n.oid=c.relnamespace
|
|
41
|
+
where n.nspname='agent_board' and c.relname='identity_providers'
|
|
42
|
+
and pol.polname='identity_providers_authenticated_read'),
|
|
43
|
+
'true', 'authenticated read predicate is true (global registry, fleet-readable)');
|
|
44
|
+
|
|
45
|
+
-- Grants: select->authenticated, select/insert/update->service_role ------------
|
|
46
|
+
select ok(
|
|
47
|
+
has_table_privilege('authenticated', 'agent_board.identity_providers', 'SELECT'),
|
|
48
|
+
'authenticated has SELECT on identity_providers');
|
|
49
|
+
select ok(
|
|
50
|
+
not has_table_privilege('authenticated', 'agent_board.identity_providers', 'INSERT'),
|
|
51
|
+
'authenticated has NO INSERT on identity_providers (mirrors workload_providers)');
|
|
52
|
+
select ok(
|
|
53
|
+
has_table_privilege('service_role', 'agent_board.identity_providers', 'INSERT'),
|
|
54
|
+
'service_role has INSERT on identity_providers');
|
|
55
|
+
select ok(
|
|
56
|
+
has_table_privilege('service_role', 'agent_board.identity_providers', 'UPDATE'),
|
|
57
|
+
'service_role has UPDATE on identity_providers');
|
|
58
|
+
|
|
59
|
+
-- Seed policy: the shipped seed carries NO policy_approved=true row -------------
|
|
60
|
+
select is(
|
|
61
|
+
(select count(*)::int from agent_board.identity_providers where policy_approved),
|
|
62
|
+
0, 'no seeded identity provider ships policy_approved=true (approval is operational)');
|
|
63
|
+
|
|
64
|
+
-- MBPORTALGATE: the REAL Portal row is present and INERT -----------------------
|
|
65
|
+
-- Frozen resource-audience constant, asserted byte-identical (IF-0-MBPORTALGATE-1/2).
|
|
66
|
+
select is(
|
|
67
|
+
(select count(*)::int from agent_board.identity_providers
|
|
68
|
+
where issuer = 'https://portal.consiliency.io'
|
|
69
|
+
and audience = 'https://message-board.consiliency.io'
|
|
70
|
+
and supported
|
|
71
|
+
and not policy_approved
|
|
72
|
+
and subject_claim = 'sub'
|
|
73
|
+
and actor_claim = 'act.sub'),
|
|
74
|
+
1, 'real Portal row present, INERT (policy_approved=false), frozen subject/actor_claim');
|
|
75
|
+
select is(
|
|
76
|
+
(select audience from agent_board.identity_providers
|
|
77
|
+
where issuer = 'https://portal.consiliency.io'),
|
|
78
|
+
'https://message-board.consiliency.io',
|
|
79
|
+
'real Portal audience is byte-identical to the frozen resource constant (RFC 8707)');
|
|
80
|
+
select is(
|
|
81
|
+
(select policy_approved from agent_board.identity_providers
|
|
82
|
+
where issuer = 'https://portal.consiliency.io'
|
|
83
|
+
and audience = 'https://message-board.consiliency.io'),
|
|
84
|
+
false, 'real Portal row ships INERT (policy_approved=false) — approval is operational');
|
|
85
|
+
|
|
86
|
+
-- The pre-existing mock row is untouched (still present, still inert) -----------
|
|
87
|
+
select is(
|
|
88
|
+
(select count(*)::int from agent_board.identity_providers
|
|
89
|
+
where issuer = 'https://mock-portal.test'
|
|
90
|
+
and audience = 'message-board'
|
|
91
|
+
and supported
|
|
92
|
+
and not policy_approved),
|
|
93
|
+
1, 'the mock issuer row is untouched (present, supported, inert)');
|
|
94
|
+
|
|
95
|
+
-- enroll_federated_identity is service_role-only (reworked 13-arg signature) ----
|
|
96
|
+
select function_privs_are(
|
|
97
|
+
'agent_board', 'enroll_federated_identity',
|
|
98
|
+
array['text','text','text','jsonb','text','text','text','text','boolean','text','text','text[]','text[]'],
|
|
99
|
+
'authenticated', array[]::text[],
|
|
100
|
+
'reworked enroll_federated_identity execute is revoked from authenticated');
|
|
101
|
+
|
|
102
|
+
select finish();
|
|
103
|
+
|
|
104
|
+
rollback;
|
|
@@ -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(
|
|
@@ -371,7 +371,7 @@ begin
|
|
|
371
371
|
'{}'::jsonb
|
|
372
372
|
);
|
|
373
373
|
v_board_message := v_result.message_id;
|
|
374
|
-
perform is(v_result.delivered_recipient_count,
|
|
374
|
+
perform is(v_result.delivered_recipient_count, 3, 'board delivery reaches joined and subscribed members only (incl. the seeded owner-creator)');
|
|
375
375
|
perform is(
|
|
376
376
|
(select count(*)::integer from agent_board.delivery_recipients where source_message_id = v_board_message and recipient_agent_id = v_left_member),
|
|
377
377
|
0,
|
|
@@ -402,15 +402,15 @@ begin
|
|
|
402
402
|
'{}'::jsonb
|
|
403
403
|
);
|
|
404
404
|
v_task := v_result.task_id;
|
|
405
|
-
perform is(v_result.delivered_recipient_count,
|
|
405
|
+
perform is(v_result.delivered_recipient_count, 4, 'tasking counts explicit and board fan-out attempts (incl. the seeded owner-creator)');
|
|
406
406
|
perform is(
|
|
407
407
|
(select count(*)::integer from agent_board.delivery_recipients where source_task_id = v_task),
|
|
408
|
-
|
|
408
|
+
3,
|
|
409
409
|
'tasking suppresses duplicate recipient rows across explicit and board fan-out'
|
|
410
410
|
);
|
|
411
411
|
perform is(
|
|
412
412
|
(select count(*)::integer from agent_board.inbox_items where source_task_id = v_task),
|
|
413
|
-
|
|
413
|
+
3,
|
|
414
414
|
'tasking writes one inbox row per unique recipient'
|
|
415
415
|
);
|
|
416
416
|
|