@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,84 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-0 pgTAP: shared send-authz enforcement config + telemetry.
|
|
2
|
+
--
|
|
3
|
+
-- Runs as the postgres db role. request.jwt.claim.role toggles _is_service_role():
|
|
4
|
+
-- unset/'' => service_role true (default here); 'authenticated' => false.
|
|
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(20);
|
|
11
|
+
|
|
12
|
+
-- Structure: fleet_send_enforcement_flags ------------------------------------
|
|
13
|
+
select has_table('agent_board', 'fleet_send_enforcement_flags', 'fleet_send_enforcement_flags table exists');
|
|
14
|
+
select has_column('agent_board', 'fleet_send_enforcement_flags', 'scope_key', 'flags has scope_key');
|
|
15
|
+
select col_is_pk('agent_board', 'fleet_send_enforcement_flags', 'scope_key', 'scope_key is the primary key');
|
|
16
|
+
select has_column('agent_board', 'fleet_send_enforcement_flags', 'mode', 'flags has mode');
|
|
17
|
+
select col_has_check('agent_board', 'fleet_send_enforcement_flags', 'mode', 'mode is check-constrained');
|
|
18
|
+
select has_column('agent_board', 'fleet_send_enforcement_flags', 'updated_at', 'flags has updated_at');
|
|
19
|
+
|
|
20
|
+
-- Default mode is fail_open_preserved (absent explicit mode).
|
|
21
|
+
insert into agent_board.fleet_send_enforcement_flags(scope_key) values ('sl0-default-scope');
|
|
22
|
+
select is(
|
|
23
|
+
(select mode from agent_board.fleet_send_enforcement_flags where scope_key = 'sl0-default-scope'),
|
|
24
|
+
'fail_open_preserved', 'flag mode defaults to fail_open_preserved');
|
|
25
|
+
|
|
26
|
+
-- Invalid mode is rejected by the CHECK.
|
|
27
|
+
select throws_ok(
|
|
28
|
+
$$insert into agent_board.fleet_send_enforcement_flags(scope_key, mode) values ('sl0-bad', 'block')$$,
|
|
29
|
+
'23514', null, 'flags mode CHECK rejects an unknown mode');
|
|
30
|
+
|
|
31
|
+
-- _send_fleet_scope_mode: default on no row, resolved value on a row -----------
|
|
32
|
+
select is(
|
|
33
|
+
agent_board._send_fleet_scope_mode('sl0-no-such-scope'),
|
|
34
|
+
'fail_open_preserved', '_send_fleet_scope_mode defaults to fail_open_preserved on no row');
|
|
35
|
+
insert into agent_board.fleet_send_enforcement_flags(scope_key, mode) values ('sl0-observe-scope', 'observe');
|
|
36
|
+
select is(
|
|
37
|
+
agent_board._send_fleet_scope_mode('sl0-observe-scope'),
|
|
38
|
+
'observe', '_send_fleet_scope_mode returns the stored mode when a row exists');
|
|
39
|
+
|
|
40
|
+
-- Structure: nonfleet_sender_allowlist ---------------------------------------
|
|
41
|
+
select has_table('agent_board', 'nonfleet_sender_allowlist', 'nonfleet_sender_allowlist table exists');
|
|
42
|
+
select col_is_pk('agent_board', 'nonfleet_sender_allowlist', 'agent_id', 'allowlist agent_id is the primary key');
|
|
43
|
+
|
|
44
|
+
-- Structure: send_authz_telemetry + recorder ---------------------------------
|
|
45
|
+
select has_table('agent_board', 'send_authz_telemetry', 'send_authz_telemetry table exists');
|
|
46
|
+
select col_has_check('agent_board', 'send_authz_telemetry', 'kind', 'telemetry kind is check-constrained');
|
|
47
|
+
select col_has_check('agent_board', 'send_authz_telemetry', 'origin', 'telemetry origin is check-constrained');
|
|
48
|
+
|
|
49
|
+
-- Recorder inserts a row and normalizes an out-of-vocab origin to 'unknown'.
|
|
50
|
+
do $$
|
|
51
|
+
declare v_id uuid;
|
|
52
|
+
begin
|
|
53
|
+
v_id := agent_board._record_send_authz_telemetry(
|
|
54
|
+
'service_role_bypass', 'garbage-origin', null, null, 'send_direct_message', 'nonfleet', null, '{}'::jsonb);
|
|
55
|
+
end $$;
|
|
56
|
+
select is(
|
|
57
|
+
(select origin from agent_board.send_authz_telemetry where operation = 'send_direct_message' order by recorded_at desc limit 1),
|
|
58
|
+
'unknown', 'recorder normalizes an out-of-vocab origin to unknown');
|
|
59
|
+
select is(
|
|
60
|
+
(select kind from agent_board.send_authz_telemetry where operation = 'send_direct_message' order by recorded_at desc limit 1),
|
|
61
|
+
'service_role_bypass', 'recorder persists kind');
|
|
62
|
+
|
|
63
|
+
-- Recorder is service_role-only: under an authenticated caller it raises 42501.
|
|
64
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
65
|
+
select throws_ok(
|
|
66
|
+
$$select agent_board._record_send_authz_telemetry('service_role_bypass','ef',null,null,'send_direct_message','nonfleet',null,'{}'::jsonb)$$,
|
|
67
|
+
'42501', null, 'recorder rejects a non-service-role caller');
|
|
68
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
69
|
+
|
|
70
|
+
-- Recorder execute privilege is revoked from authenticated.
|
|
71
|
+
select function_privs_are(
|
|
72
|
+
'agent_board', '_record_send_authz_telemetry',
|
|
73
|
+
array['text', 'text', 'uuid', 'uuid', 'text', 'text', 'text', 'jsonb'],
|
|
74
|
+
'authenticated', array[]::text[],
|
|
75
|
+
'recorder execute is revoked from authenticated');
|
|
76
|
+
|
|
77
|
+
-- _resolve_send_fleet_scope: a non-fleet actor maps to 'nonfleet' -------------
|
|
78
|
+
select is(
|
|
79
|
+
agent_board._resolve_send_fleet_scope(gen_random_uuid()),
|
|
80
|
+
'nonfleet', '_resolve_send_fleet_scope maps a non-fleet (no active machine) actor to nonfleet');
|
|
81
|
+
|
|
82
|
+
select finish();
|
|
83
|
+
|
|
84
|
+
rollback;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-3 pgTAP: cross-cutting authorization matrix. Proves, in one
|
|
2
|
+
-- suite, the board-role send gate (SL-1), the non-fleet default-deny + tight
|
|
3
|
+
-- allowlist + per-scope flag toggle incl. the boardless path (SL-2/SL-0), and the
|
|
4
|
+
-- grantee-bounded ceiling consumption in grant_board_role (SL-3 + MBAUTHZ helper).
|
|
5
|
+
--
|
|
6
|
+
-- Runs as the postgres db role; request.jwt.claim.role/.sub simulate callers.
|
|
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(11);
|
|
13
|
+
|
|
14
|
+
create temp table _mtx_fixture (
|
|
15
|
+
sb_board uuid,
|
|
16
|
+
sb_member uuid, sb_observer uuid,
|
|
17
|
+
sb_member_user uuid, sb_observer_user uuid,
|
|
18
|
+
thread_member uuid, thread_observer uuid,
|
|
19
|
+
nf_sender uuid, nf_allow uuid,
|
|
20
|
+
b1 uuid, g_owner uuid, g_admin uuid, g_grantee uuid,
|
|
21
|
+
b2 uuid, g2_owner uuid, g2_grantee uuid
|
|
22
|
+
) on commit drop;
|
|
23
|
+
|
|
24
|
+
do $$
|
|
25
|
+
declare
|
|
26
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
27
|
+
v_sb_member_user uuid;
|
|
28
|
+
v_sb_observer_user uuid;
|
|
29
|
+
v_sb_creator uuid := gen_random_uuid();
|
|
30
|
+
v_sb_member uuid := gen_random_uuid();
|
|
31
|
+
v_sb_observer uuid := gen_random_uuid();
|
|
32
|
+
v_sb_board uuid;
|
|
33
|
+
v_thread_member uuid := gen_random_uuid();
|
|
34
|
+
v_thread_observer uuid := gen_random_uuid();
|
|
35
|
+
v_nf_sender uuid := gen_random_uuid();
|
|
36
|
+
v_nf_allow uuid := gen_random_uuid();
|
|
37
|
+
v_b1 uuid; v_g_owner uuid := gen_random_uuid(); v_g_admin uuid := gen_random_uuid(); v_g_grantee uuid := gen_random_uuid();
|
|
38
|
+
v_b2 uuid; v_g2_owner uuid := gen_random_uuid(); v_g2_grantee uuid := gen_random_uuid();
|
|
39
|
+
begin
|
|
40
|
+
v_sb_member_user := public.ensure_github_user('mtx-member-' || v_suffix, 'mtx-member-' || v_suffix || '@example.test', 'mtx-member-' || v_suffix, 'MTX Member');
|
|
41
|
+
v_sb_observer_user := public.ensure_github_user('mtx-observer-' || v_suffix, 'mtx-observer-' || v_suffix || '@example.test', 'mtx-observer-' || v_suffix, 'MTX Observer');
|
|
42
|
+
|
|
43
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
44
|
+
values
|
|
45
|
+
(v_sb_member, 'agent', 'mtx-member-' || v_suffix, 'MTX Member', 'active', v_sb_member_user, v_sb_member_user),
|
|
46
|
+
(v_sb_observer, 'agent', 'mtx-observer-' || v_suffix, 'MTX Observer', 'active', v_sb_observer_user, v_sb_observer_user);
|
|
47
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
48
|
+
values
|
|
49
|
+
(v_sb_creator, 'agent', 'mtx-sbcreator-' || v_suffix, 'MTX Send Board Creator'),
|
|
50
|
+
(v_nf_sender, 'agent', 'mtx-nf-' || v_suffix, 'MTX NonFleet'),
|
|
51
|
+
(v_nf_allow, 'agent', 'mtx-nfa-' || v_suffix, 'MTX NonFleet Allow'),
|
|
52
|
+
(v_g_owner, 'agent', 'mtx-gowner-' || v_suffix, 'MTX Grant Owner'),
|
|
53
|
+
(v_g_admin, 'agent', 'mtx-gadmin-' || v_suffix, 'MTX Grant Admin'),
|
|
54
|
+
(v_g_grantee, 'agent', 'mtx-ggrantee-' || v_suffix, 'MTX Grant Grantee'),
|
|
55
|
+
(v_g2_owner, 'agent', 'mtx-g2owner-' || v_suffix, 'MTX Grant2 Owner'),
|
|
56
|
+
(v_g2_grantee, 'agent', 'mtx-g2grantee-' || v_suffix, 'MTX Grant2 Grantee');
|
|
57
|
+
|
|
58
|
+
-- Send board created by a dedicated creator (owner is seeded by create_board);
|
|
59
|
+
-- member/observer are genuine NON-creator members so their roles drive the gate.
|
|
60
|
+
select board_id into v_sb_board from agent_board.create_board(v_sb_creator, 'mtx-sb-' || v_suffix, 'MTX Send Board', 'matrix', '{}'::jsonb, 'private');
|
|
61
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
62
|
+
values
|
|
63
|
+
(v_sb_board, v_sb_member, 'joined', v_sb_creator, 'member'),
|
|
64
|
+
(v_sb_board, v_sb_observer, 'joined', v_sb_creator, 'observer');
|
|
65
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
66
|
+
values
|
|
67
|
+
(v_thread_member, 'coordination', v_sb_member, 'MTX member thread'),
|
|
68
|
+
(v_thread_observer, 'coordination', v_sb_observer, 'MTX observer thread');
|
|
69
|
+
|
|
70
|
+
-- Grant board B1: no ceiling row (ceiling defaults to owner).
|
|
71
|
+
select board_id into v_b1 from agent_board.create_board(v_g_owner, 'mtx-b1-' || v_suffix, 'MTX Grant B1', 'ceiling', '{}'::jsonb, 'private');
|
|
72
|
+
-- v_g_owner's owner membership is seeded by create_board (ROOMOWN).
|
|
73
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
74
|
+
values
|
|
75
|
+
(v_b1, v_g_admin, 'joined', v_g_owner, 'admin'),
|
|
76
|
+
(v_b1, v_g_grantee, 'joined', v_g_owner, 'observer');
|
|
77
|
+
|
|
78
|
+
-- Grant board B2: ceiling lowered to member.
|
|
79
|
+
select board_id into v_b2 from agent_board.create_board(v_g2_owner, 'mtx-b2-' || v_suffix, 'MTX Grant B2', 'ceiling', '{}'::jsonb, 'private');
|
|
80
|
+
-- v_g2_owner's owner membership is seeded by create_board (ROOMOWN).
|
|
81
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
82
|
+
values
|
|
83
|
+
(v_b2, v_g2_grantee, 'joined', v_g2_owner, 'observer');
|
|
84
|
+
insert into agent_board.board_authority_ceilings(board_id, max_grantable_role) values (v_b2, 'member');
|
|
85
|
+
|
|
86
|
+
insert into _mtx_fixture values (
|
|
87
|
+
v_sb_board, v_sb_member, v_sb_observer, v_sb_member_user, v_sb_observer_user,
|
|
88
|
+
v_thread_member, v_thread_observer, v_nf_sender, v_nf_allow,
|
|
89
|
+
v_b1, v_g_owner, v_g_admin, v_g_grantee, v_b2, v_g2_owner, v_g2_grantee);
|
|
90
|
+
end
|
|
91
|
+
$$;
|
|
92
|
+
|
|
93
|
+
-- A. Board-role send gate (authenticated path) -------------------------------
|
|
94
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
95
|
+
select set_config('request.jwt.claim.sub', (select sb_observer_user from _mtx_fixture)::text, true);
|
|
96
|
+
select throws_ok(
|
|
97
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 's', 'b')$q$,
|
|
98
|
+
thread_observer, sb_observer, sb_board) from _mtx_fixture),
|
|
99
|
+
'42501', 'board send requires a non-observer board role',
|
|
100
|
+
'matrix: observer authenticated actor denied on a board send');
|
|
101
|
+
select set_config('request.jwt.claim.sub', (select sb_member_user from _mtx_fixture)::text, true);
|
|
102
|
+
select lives_ok(
|
|
103
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 's', 'b')$q$,
|
|
104
|
+
thread_member, sb_member, sb_board) from _mtx_fixture),
|
|
105
|
+
'matrix: member authenticated actor allowed on a board send');
|
|
106
|
+
|
|
107
|
+
-- B/C. Non-fleet default-deny + tight allowlist + per-scope flag toggle -------
|
|
108
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
109
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
110
|
+
insert into agent_board.fleet_send_enforcement_flags(scope_key, mode) values ('nonfleet', 'deny');
|
|
111
|
+
insert into agent_board.nonfleet_sender_allowlist(agent_id, reason) values ((select nf_allow from _mtx_fixture), 'matrix enumerated flow');
|
|
112
|
+
|
|
113
|
+
select is(
|
|
114
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy('send_direct_message', '{}'::jsonb,
|
|
115
|
+
jsonb_build_object('agent_id', (select nf_sender from _mtx_fixture)::text))),
|
|
116
|
+
false, 'matrix: deny mode blocks an unenrolled non-fleet sender');
|
|
117
|
+
select is(
|
|
118
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy('send_direct_message', '{}'::jsonb,
|
|
119
|
+
jsonb_build_object('agent_id', (select nf_allow from _mtx_fixture)::text))),
|
|
120
|
+
true, 'matrix: tight allowlist admits only the enumerated non-fleet sender');
|
|
121
|
+
-- Boardless send_direct_message path is gated by the scope flag (scope before board).
|
|
122
|
+
select is(
|
|
123
|
+
(select denial_class from agent_board.evaluate_signed_write_fleet_policy('send_direct_message', '{}'::jsonb,
|
|
124
|
+
jsonb_build_object('agent_id', (select nf_sender from _mtx_fixture)::text))),
|
|
125
|
+
'nonfleet_sender_denied', 'matrix: flag gates the boardless send_direct_message path under deny');
|
|
126
|
+
-- Toggle the scope off: the same sender now passes (fail_open_preserved).
|
|
127
|
+
update agent_board.fleet_send_enforcement_flags set mode = 'fail_open_preserved' where scope_key = 'nonfleet';
|
|
128
|
+
select is(
|
|
129
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy('send_direct_message', '{}'::jsonb,
|
|
130
|
+
jsonb_build_object('agent_id', (select nf_sender from _mtx_fixture)::text))),
|
|
131
|
+
true, 'matrix: flag toggle to fail_open_preserved re-admits the same non-fleet sender');
|
|
132
|
+
|
|
133
|
+
-- D. Grantee-bounded ceiling consumption via grant_board_role -----------------
|
|
134
|
+
-- B1 has no ceiling row (ceiling defaults to owner).
|
|
135
|
+
select lives_ok(
|
|
136
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'member', %L::uuid)$q$,
|
|
137
|
+
b1, g_grantee, g_admin) from _mtx_fixture),
|
|
138
|
+
'matrix: within-ceiling grant (admin grants member) is admitted');
|
|
139
|
+
select throws_ok(
|
|
140
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'owner', %L::uuid)$q$,
|
|
141
|
+
b1, g_grantee, g_admin) from _mtx_fixture),
|
|
142
|
+
'42501', 'grant exceeds grantor role or board authority ceiling',
|
|
143
|
+
'matrix: a grant exceeding the grantor role is rejected (grantee-bounded)');
|
|
144
|
+
select lives_ok(
|
|
145
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin', %L::uuid)$q$,
|
|
146
|
+
b1, g_grantee, g_owner) from _mtx_fixture),
|
|
147
|
+
'matrix: no-ceiling-row board treats ceiling as owner (owner grants admin admitted)');
|
|
148
|
+
-- B2 ceiling is member: owner may not grant admin (over-ceiling).
|
|
149
|
+
select throws_ok(
|
|
150
|
+
(select format($q$select agent_board.grant_board_role(%L::uuid, %L::uuid, 'admin', %L::uuid)$q$,
|
|
151
|
+
b2, g2_grantee, g2_owner) from _mtx_fixture),
|
|
152
|
+
'42501', 'grant exceeds grantor role or board authority ceiling',
|
|
153
|
+
'matrix: an over-ceiling grant is rejected even for an owner grantor');
|
|
154
|
+
|
|
155
|
+
-- E. Live: grant_board_role is now granted EXECUTE to authenticated (ROOMOWN) ---
|
|
156
|
+
select function_privs_are(
|
|
157
|
+
'agent_board', 'grant_board_role', array['uuid', 'uuid', 'text', 'uuid'],
|
|
158
|
+
'authenticated', array['EXECUTE'], 'matrix: grant_board_role execute is granted to authenticated (self-service authority live)');
|
|
159
|
+
|
|
160
|
+
select finish();
|
|
161
|
+
|
|
162
|
+
rollback;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-1 pgTAP: _current_agent_can_send_to_board, the board-role send
|
|
2
|
+
-- gate on the board-target sends, boardless-send invariance, and service_role
|
|
3
|
+
-- bypass telemetry with the frozen origin discriminator.
|
|
4
|
+
--
|
|
5
|
+
-- Runs as the postgres db role; request.jwt.claim.role toggles _is_service_role()
|
|
6
|
+
-- (''/service_role => true, 'authenticated' => false), request.jwt.claim.sub picks
|
|
7
|
+
-- the acting agent.
|
|
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(16);
|
|
14
|
+
|
|
15
|
+
create temp table _srg_fixture (
|
|
16
|
+
board uuid,
|
|
17
|
+
member_agent uuid,
|
|
18
|
+
observer_agent uuid,
|
|
19
|
+
nonmember_agent uuid,
|
|
20
|
+
recipient_agent uuid,
|
|
21
|
+
member_user uuid,
|
|
22
|
+
observer_user uuid,
|
|
23
|
+
nonmember_user uuid,
|
|
24
|
+
thread_member uuid,
|
|
25
|
+
thread_observer uuid,
|
|
26
|
+
thread_dm uuid
|
|
27
|
+
) on commit drop;
|
|
28
|
+
|
|
29
|
+
do $$
|
|
30
|
+
declare
|
|
31
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
32
|
+
v_member_user uuid;
|
|
33
|
+
v_observer_user uuid;
|
|
34
|
+
v_nonmember_user uuid;
|
|
35
|
+
v_creator uuid := gen_random_uuid();
|
|
36
|
+
v_member uuid := gen_random_uuid();
|
|
37
|
+
v_observer uuid := gen_random_uuid();
|
|
38
|
+
v_nonmember uuid := gen_random_uuid();
|
|
39
|
+
v_recipient uuid := gen_random_uuid();
|
|
40
|
+
v_board uuid;
|
|
41
|
+
v_thread_member uuid := gen_random_uuid();
|
|
42
|
+
v_thread_observer uuid := gen_random_uuid();
|
|
43
|
+
v_thread_dm uuid := gen_random_uuid();
|
|
44
|
+
begin
|
|
45
|
+
v_member_user := public.ensure_github_user('srg-member-' || v_suffix, 'srg-member-' || v_suffix || '@example.test', 'srg-member-' || v_suffix, 'SRG Member');
|
|
46
|
+
v_observer_user := public.ensure_github_user('srg-observer-' || v_suffix, 'srg-observer-' || v_suffix || '@example.test', 'srg-observer-' || v_suffix, 'SRG Observer');
|
|
47
|
+
v_nonmember_user := public.ensure_github_user('srg-nonmember-' || v_suffix, 'srg-nonmember-' || v_suffix || '@example.test', 'srg-nonmember-' || v_suffix, 'SRG Nonmember');
|
|
48
|
+
|
|
49
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
50
|
+
values
|
|
51
|
+
(v_member, 'agent', 'srg-member-' || v_suffix, 'SRG Member', 'active', v_member_user, v_member_user),
|
|
52
|
+
(v_observer, 'agent', 'srg-observer-' || v_suffix, 'SRG Observer', 'active', v_observer_user, v_observer_user),
|
|
53
|
+
(v_nonmember, 'agent', 'srg-nonmember-' || v_suffix, 'SRG Nonmember', 'active', v_nonmember_user, v_nonmember_user),
|
|
54
|
+
(v_recipient, 'agent', 'srg-recipient-' || v_suffix, 'SRG Recipient', 'active', null, null),
|
|
55
|
+
(v_creator, 'agent', 'srg-creator-' || v_suffix, 'SRG Creator', 'active', null, null);
|
|
56
|
+
|
|
57
|
+
-- Dedicated creator (owner seeded by create_board); member/observer are genuine
|
|
58
|
+
-- NON-creator members so their roles drive the send gate.
|
|
59
|
+
select board_id into v_board
|
|
60
|
+
from agent_board.create_board(v_creator, 'srg-board-' || v_suffix, 'SRG Board', 'send role gate test', '{}'::jsonb, 'private');
|
|
61
|
+
|
|
62
|
+
-- member: role member; observer: role observer. Both active.
|
|
63
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
64
|
+
values
|
|
65
|
+
(v_board, v_member, 'joined', v_creator, 'member'),
|
|
66
|
+
(v_board, v_observer, 'joined', v_creator, 'observer');
|
|
67
|
+
|
|
68
|
+
-- Threads each sending actor can access (inherit mode; creator has access).
|
|
69
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
70
|
+
values
|
|
71
|
+
(v_thread_member, 'coordination', v_member, 'SRG member thread'),
|
|
72
|
+
(v_thread_observer, 'coordination', v_observer, 'SRG observer thread'),
|
|
73
|
+
(v_thread_dm, 'coordination', v_member, 'SRG dm thread');
|
|
74
|
+
|
|
75
|
+
insert into _srg_fixture values
|
|
76
|
+
(v_board, v_member, v_observer, v_nonmember, v_recipient,
|
|
77
|
+
v_member_user, v_observer_user, v_nonmember_user,
|
|
78
|
+
v_thread_member, v_thread_observer, v_thread_dm);
|
|
79
|
+
end
|
|
80
|
+
$$;
|
|
81
|
+
|
|
82
|
+
-- 1. _current_agent_can_send_to_board -----------------------------------------
|
|
83
|
+
select has_function('agent_board', '_current_agent_can_send_to_board', array['uuid'], '_current_agent_can_send_to_board exists');
|
|
84
|
+
|
|
85
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
86
|
+
select is(
|
|
87
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
88
|
+
true, 'service_role may always send to board');
|
|
89
|
+
|
|
90
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
91
|
+
select set_config('request.jwt.claim.sub', (select member_user from _srg_fixture)::text, true);
|
|
92
|
+
select is(
|
|
93
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
94
|
+
true, 'active member (role member) may send to board');
|
|
95
|
+
|
|
96
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
97
|
+
select is(
|
|
98
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
99
|
+
false, 'observer board member may not send to board');
|
|
100
|
+
|
|
101
|
+
select set_config('request.jwt.claim.sub', (select nonmember_user from _srg_fixture)::text, true);
|
|
102
|
+
select is(
|
|
103
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
104
|
+
false, 'non-member may not send to board');
|
|
105
|
+
|
|
106
|
+
-- 2. Board-target sends enforce the role gate on the authenticated path -------
|
|
107
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
108
|
+
select throws_ok(
|
|
109
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body')$q$,
|
|
110
|
+
thread_observer, observer_agent, board) from _srg_fixture),
|
|
111
|
+
'42501', 'board send requires a non-observer board role',
|
|
112
|
+
'post_board_message rejects an observer board member');
|
|
113
|
+
|
|
114
|
+
select set_config('request.jwt.claim.sub', (select member_user from _srg_fixture)::text, true);
|
|
115
|
+
select lives_ok(
|
|
116
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body')$q$,
|
|
117
|
+
thread_member, member_agent, board) from _srg_fixture),
|
|
118
|
+
'post_board_message admits a member board member');
|
|
119
|
+
|
|
120
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
121
|
+
select throws_ok(
|
|
122
|
+
(select format($q$select agent_board.create_tasking_delivery(%L::uuid, %L::uuid, 'title', null, null, %L::uuid)$q$,
|
|
123
|
+
thread_observer, observer_agent, board) from _srg_fixture),
|
|
124
|
+
'42501', 'board send requires a non-observer board role',
|
|
125
|
+
'create_tasking_delivery (board target) rejects an observer board member');
|
|
126
|
+
|
|
127
|
+
select set_config('request.jwt.claim.sub', (select member_user from _srg_fixture)::text, true);
|
|
128
|
+
select lives_ok(
|
|
129
|
+
(select format($q$select agent_board.create_tasking_delivery(%L::uuid, %L::uuid, 'title', null, null, %L::uuid)$q$,
|
|
130
|
+
thread_member, member_agent, board) from _srg_fixture),
|
|
131
|
+
'create_tasking_delivery (board target) admits a member board member');
|
|
132
|
+
|
|
133
|
+
-- 3. Boardless direct send is unaffected by the role gate ---------------------
|
|
134
|
+
-- The observer holds only an observer board role, but a board-less DM never
|
|
135
|
+
-- consults the board role gate.
|
|
136
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
137
|
+
select lives_ok(
|
|
138
|
+
(select format($q$select agent_board.send_direct_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body')$q$,
|
|
139
|
+
thread_observer, observer_agent, recipient_agent) from _srg_fixture),
|
|
140
|
+
'send_direct_message on a board-less thread is unaffected by the board role gate (observer sender admitted)');
|
|
141
|
+
|
|
142
|
+
-- 4. service_role bypass telemetry: exactly one row, origin from p_metadata ----
|
|
143
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
144
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
145
|
+
select lives_ok(
|
|
146
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body', 'comment', 'medium', null, %L::jsonb)$q$,
|
|
147
|
+
thread_member, member_agent, board,
|
|
148
|
+
jsonb_build_object('signed_write', jsonb_build_object('origin', 'ef'))) from _srg_fixture),
|
|
149
|
+
'service_role post_board_message succeeds (guarded bypass)');
|
|
150
|
+
|
|
151
|
+
select is(
|
|
152
|
+
(select count(*)::int from agent_board.send_authz_telemetry
|
|
153
|
+
where kind = 'service_role_bypass'
|
|
154
|
+
and operation = 'post_board_message'
|
|
155
|
+
and actor_agent_id = (select member_agent from _srg_fixture)),
|
|
156
|
+
1, 'exactly one service_role_bypass telemetry row is recorded for the send');
|
|
157
|
+
|
|
158
|
+
select is(
|
|
159
|
+
(select origin from agent_board.send_authz_telemetry
|
|
160
|
+
where kind = 'service_role_bypass' and operation = 'post_board_message'
|
|
161
|
+
and actor_agent_id = (select member_agent from _srg_fixture)
|
|
162
|
+
order by recorded_at desc limit 1),
|
|
163
|
+
'ef', 'bypass telemetry origin is resolved from p_metadata signed_write origin');
|
|
164
|
+
|
|
165
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
166
|
+
|
|
167
|
+
-- 5. Enforcement live (MBSESSION): the 8 core sends are re-granted to
|
|
168
|
+
-- authenticated; the board-role gate asserted above still governs who may send.
|
|
169
|
+
select function_privs_are(
|
|
170
|
+
'agent_board', 'post_board_message', array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
|
|
171
|
+
'authenticated', array['EXECUTE'], 'post_board_message execute is granted to authenticated (scoped send live)');
|
|
172
|
+
select function_privs_are(
|
|
173
|
+
'agent_board', 'create_tasking_delivery', array['uuid','uuid','text','text','uuid[]','uuid','text','text','text','jsonb'],
|
|
174
|
+
'authenticated', array['EXECUTE'], 'create_tasking_delivery execute is granted to authenticated (scoped send live)');
|
|
175
|
+
select function_privs_are(
|
|
176
|
+
'agent_board', 'send_direct_message', array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
|
|
177
|
+
'authenticated', array['EXECUTE'], 'send_direct_message execute is granted to authenticated (scoped send live)');
|
|
178
|
+
|
|
179
|
+
select finish();
|
|
180
|
+
|
|
181
|
+
rollback;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- MBHARDEN SL-1 pgTAP: issue_signing_key_challenge enforces a per-key cap of
|
|
2
|
+
-- N=16 concurrently-pending challenges; <=16 issue as before, the 17th is
|
|
3
|
+
-- rejected, and expired/consumed challenges do not count toward the cap.
|
|
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(4);
|
|
10
|
+
|
|
11
|
+
select has_function(
|
|
12
|
+
'agent_board', 'issue_signing_key_challenge', array['uuid', 'text', 'interval'],
|
|
13
|
+
'issue_signing_key_challenge exists');
|
|
14
|
+
|
|
15
|
+
-- Block 1: 16 pending challenges are allowed; the 17th is rejected by the cap.
|
|
16
|
+
do $$
|
|
17
|
+
declare
|
|
18
|
+
v_agent uuid := '00000000-0000-0000-0000-0000000000c1';
|
|
19
|
+
v_i int;
|
|
20
|
+
v_c record;
|
|
21
|
+
begin
|
|
22
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
23
|
+
values (v_agent, 'agent', 'pcap-agent', 'PCAP Agent');
|
|
24
|
+
insert into agent_board.agent_signing_keys(
|
|
25
|
+
agent_id, key_id, key_version, public_key, public_key_format, fingerprint)
|
|
26
|
+
values (
|
|
27
|
+
v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url',
|
|
28
|
+
'sha256:' || repeat('Z', 43));
|
|
29
|
+
|
|
30
|
+
set local role service_role;
|
|
31
|
+
|
|
32
|
+
for v_i in 1..16 loop
|
|
33
|
+
select * into v_c
|
|
34
|
+
from agent_board.issue_signing_key_challenge(v_agent, 'primary', interval '5 minutes');
|
|
35
|
+
if v_c.challenge_id is null then
|
|
36
|
+
raise exception 'pending challenge % was not issued below the cap', v_i;
|
|
37
|
+
end if;
|
|
38
|
+
end loop;
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
perform *
|
|
42
|
+
from agent_board.issue_signing_key_challenge(v_agent, 'primary', interval '5 minutes');
|
|
43
|
+
raise exception 'the 17th pending challenge was NOT rejected by the cap';
|
|
44
|
+
exception
|
|
45
|
+
when others then
|
|
46
|
+
if sqlerrm !~ 'challenge_pending_cap' then
|
|
47
|
+
raise;
|
|
48
|
+
end if;
|
|
49
|
+
end;
|
|
50
|
+
end
|
|
51
|
+
$$;
|
|
52
|
+
reset role;
|
|
53
|
+
|
|
54
|
+
select pass('16 pending challenges issue below the cap');
|
|
55
|
+
select pass('the 17th concurrently-pending challenge is rejected (challenge_pending_cap)');
|
|
56
|
+
|
|
57
|
+
-- Expire one pending challenge (as owner), then a new challenge succeeds: expired
|
|
58
|
+
-- rows do not count toward the cap.
|
|
59
|
+
update agent_board.agent_signing_key_challenges
|
|
60
|
+
set expires_at = now() - interval '1 minute'
|
|
61
|
+
where id = (
|
|
62
|
+
select id from agent_board.agent_signing_key_challenges
|
|
63
|
+
where agent_id = '00000000-0000-0000-0000-0000000000c1'
|
|
64
|
+
and proof_state = 'pending'
|
|
65
|
+
order by created_at
|
|
66
|
+
limit 1);
|
|
67
|
+
|
|
68
|
+
do $$
|
|
69
|
+
declare
|
|
70
|
+
v_agent uuid := '00000000-0000-0000-0000-0000000000c1';
|
|
71
|
+
v_c record;
|
|
72
|
+
begin
|
|
73
|
+
set local role service_role;
|
|
74
|
+
select * into v_c
|
|
75
|
+
from agent_board.issue_signing_key_challenge(v_agent, 'primary', interval '5 minutes');
|
|
76
|
+
if v_c.challenge_id is null then
|
|
77
|
+
raise exception 'a new challenge was rejected even though an expired row freed a slot';
|
|
78
|
+
end if;
|
|
79
|
+
end
|
|
80
|
+
$$;
|
|
81
|
+
reset role;
|
|
82
|
+
|
|
83
|
+
select pass('an expired pending challenge frees a slot under the cap');
|
|
84
|
+
|
|
85
|
+
select finish();
|
|
86
|
+
|
|
87
|
+
rollback;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- MBHARDEN SL-1 pgTAP: reap_expired_signing_key_challenges deletes only rows
|
|
2
|
+
-- whose expires_at is older than the grace argument, leaves unexpired rows, returns
|
|
3
|
+
-- the affected count, and is 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(9);
|
|
10
|
+
|
|
11
|
+
select has_function(
|
|
12
|
+
'agent_board', 'reap_expired_signing_key_challenges', array['interval'],
|
|
13
|
+
'reap_expired_signing_key_challenges(interval) exists');
|
|
14
|
+
|
|
15
|
+
-- Seed: one agent + key, then three challenges at old (>7d) / recent (<7d) / future
|
|
16
|
+
-- expires_at. All are pending (proof_state='pending', consumed_at null) — valid per
|
|
17
|
+
-- the consumed-shape check regardless of expires_at.
|
|
18
|
+
do $$
|
|
19
|
+
declare
|
|
20
|
+
v_agent uuid := gen_random_uuid();
|
|
21
|
+
v_key uuid;
|
|
22
|
+
begin
|
|
23
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
24
|
+
values (v_agent, 'agent', 'reap-chal-agent', 'REAP CHAL Agent');
|
|
25
|
+
insert into agent_board.agent_signing_keys(
|
|
26
|
+
agent_id, key_id, key_version, public_key, public_key_format, fingerprint)
|
|
27
|
+
values (
|
|
28
|
+
v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url',
|
|
29
|
+
'sha256:' || repeat('Z', 43))
|
|
30
|
+
returning id into v_key;
|
|
31
|
+
|
|
32
|
+
insert into agent_board.agent_signing_key_challenges(
|
|
33
|
+
signing_key_id, agent_id, challenge_nonce, challenge_bytes, expires_at)
|
|
34
|
+
values
|
|
35
|
+
(v_key, v_agent, 'nonce-old', 'bytes-old', now() - interval '30 days'),
|
|
36
|
+
(v_key, v_agent, 'nonce-recent', 'bytes-recent', now() - interval '1 hour'),
|
|
37
|
+
(v_key, v_agent, 'nonce-future', 'bytes-future', now() + interval '10 minutes');
|
|
38
|
+
end
|
|
39
|
+
$$;
|
|
40
|
+
|
|
41
|
+
-- Default 7-day grace: removes only the 30-day-old row.
|
|
42
|
+
select is(
|
|
43
|
+
agent_board.reap_expired_signing_key_challenges(),
|
|
44
|
+
1::bigint, 'default (7 day) reap removes exactly the one row expired > 7 days ago');
|
|
45
|
+
select ok(
|
|
46
|
+
not exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-old'),
|
|
47
|
+
'the 30-day-old challenge was deleted');
|
|
48
|
+
select ok(
|
|
49
|
+
exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-recent'),
|
|
50
|
+
'the 1-hour-old challenge survives the 7-day grace');
|
|
51
|
+
select ok(
|
|
52
|
+
exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-future'),
|
|
53
|
+
'the not-yet-expired challenge survives the 7-day grace');
|
|
54
|
+
|
|
55
|
+
-- Zero grace: removes any past-expires_at row (the recent one) but not the future one.
|
|
56
|
+
select is(
|
|
57
|
+
agent_board.reap_expired_signing_key_challenges(interval '0 seconds'),
|
|
58
|
+
1::bigint, 'zero-grace reap removes exactly the remaining past-expires_at row');
|
|
59
|
+
select ok(
|
|
60
|
+
not exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-recent'),
|
|
61
|
+
'the 1-hour-old challenge is deleted under zero grace');
|
|
62
|
+
select ok(
|
|
63
|
+
exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-future'),
|
|
64
|
+
'a not-yet-expired challenge is never reaped');
|
|
65
|
+
|
|
66
|
+
-- Privilege: execute revoked from authenticated.
|
|
67
|
+
select function_privs_are(
|
|
68
|
+
'agent_board', 'reap_expired_signing_key_challenges', array['interval'],
|
|
69
|
+
'authenticated', array[]::text[],
|
|
70
|
+
'reap_expired_signing_key_challenges execute is revoked from authenticated');
|
|
71
|
+
|
|
72
|
+
select finish();
|
|
73
|
+
|
|
74
|
+
rollback;
|