@consiliency/agent-board-schema 1.12.0 → 1.22.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/fixtures/already-applied.json +124 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +282 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
-- #146 operator machine reactivation — supersede-based un-revoke.
|
|
2
|
+
--
|
|
3
|
+
-- reactivate_fleet_machine (service_role only) stamps fleet_machines.reactivated_at and
|
|
4
|
+
-- clears revoked_at/lifecycle so machine-revocation READERS ignore machine events
|
|
5
|
+
-- at-or-before that supersession point. The readers are: evaluate_fleet_sender_policy and
|
|
6
|
+
-- the v_fleet_machine_status read model. revoke_fleet_subject (the WRITER) advances the
|
|
7
|
+
-- event's effective_at to now() on re-revocation so the append-only ledger stays
|
|
8
|
+
-- authoritative (a re-revocation after reactivation supersedes reactivated_at).
|
|
9
|
+
--
|
|
10
|
+
-- These assertions pin:
|
|
11
|
+
-- * revoke → policy denies; reactivate → the SAME sender passes (authority restored).
|
|
12
|
+
-- * a re-revocation via the REAL writer denies again AND advances the ledger's effective_at
|
|
13
|
+
-- past reactivated_at (supersession is prior-only; ledger stays authoritative — #146 panel
|
|
14
|
+
-- MUST-FIX 1). Because now() is transaction-frozen in pgTAP, the re-revoke fixture ages
|
|
15
|
+
-- the original revocation (event -2h) and the reactivation (marker -1h) so the real
|
|
16
|
+
-- writer's now() re-revocation lands strictly after the supersession point.
|
|
17
|
+
-- * reactivation supersedes ONLY the machine subject: a revoked trust_domain or a revoked
|
|
18
|
+
-- sender agent stays denied after machine reactivation.
|
|
19
|
+
-- * reactivation is operator-only: an authenticated caller (even the owner) is refused.
|
|
20
|
+
-- * reactivating a non-revoked machine raises (nothing to reactivate).
|
|
21
|
+
-- * v_fleet_machine_status.machine_revoked honors reactivated_at (#146 panel MUST-FIX 2).
|
|
22
|
+
-- * rule #1 CLASS: every catalog-enumerated machine-subject revocation reader honors
|
|
23
|
+
-- reactivated_at (a new non-conforming reader names itself); the WRITER exemption is
|
|
24
|
+
-- asserted positively (it bumps effective_at=now()).
|
|
25
|
+
--
|
|
26
|
+
-- Tests run as session_user=postgres ⇒ _is_service_role() is true; the authz assertion flips
|
|
27
|
+
-- to role=authenticated (jwt claims) to exercise the body guard.
|
|
28
|
+
--
|
|
29
|
+
-- Mutation pins (rule #2):
|
|
30
|
+
-- * drop the machine-clause reactivated_at guard in the policy → (8) fails.
|
|
31
|
+
-- * revert revoke_fleet_subject's UPDATE branch (remove `effective_at = now()`) → (12) fails
|
|
32
|
+
-- (the aged event stays before reactivated_at → ledger not advanced).
|
|
33
|
+
-- * remove the policy's lifecycle_state='revoked' guard → (11) STILL passes (the advanced
|
|
34
|
+
-- event clause catches the re-revocation): 2-of-2 depth.
|
|
35
|
+
-- * remove reactivate's _is_service_role() body guard → (17) fails.
|
|
36
|
+
-- * add the reactivated_at guard to the trust_domain clause → (14) fails; to the agent
|
|
37
|
+
-- clause → (16) fails.
|
|
38
|
+
-- * revert v_fleet_machine_status.machine_revoked to bare event-existence → (19) fails.
|
|
39
|
+
-- * add any machine-subject reader that omits reactivated_at → (21) fails and names it.
|
|
40
|
+
|
|
41
|
+
create extension if not exists pgtap with schema extensions;
|
|
42
|
+
set search_path = extensions, public, agent_board;
|
|
43
|
+
|
|
44
|
+
begin;
|
|
45
|
+
|
|
46
|
+
select plan(22);
|
|
47
|
+
|
|
48
|
+
-- (1)(2) Shape.
|
|
49
|
+
select has_function(
|
|
50
|
+
'agent_board', 'reactivate_fleet_machine',
|
|
51
|
+
array['uuid', 'uuid', 'text'],
|
|
52
|
+
'reactivate_fleet_machine exists'
|
|
53
|
+
);
|
|
54
|
+
select has_column(
|
|
55
|
+
'agent_board', 'fleet_machines', 'reactivated_at',
|
|
56
|
+
'fleet_machines.reactivated_at supersession marker exists'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
create temp table _react_fx (
|
|
60
|
+
recipient uuid,
|
|
61
|
+
owner uuid, -- machine owner (registers machines); also the trust-domain owner
|
|
62
|
+
a1 uuid, a2 uuid, a3 uuid, a4 uuid, a5 uuid, a6 uuid, -- sender agents
|
|
63
|
+
m1 uuid, m2 uuid, m3 uuid, m4 uuid, m5 uuid, m6 uuid, -- machines
|
|
64
|
+
domain_main uuid,
|
|
65
|
+
domain_revoked uuid,
|
|
66
|
+
owner_user_id uuid
|
|
67
|
+
) on commit drop;
|
|
68
|
+
|
|
69
|
+
grant all on table _react_fx to authenticated, service_role;
|
|
70
|
+
|
|
71
|
+
do $$
|
|
72
|
+
declare
|
|
73
|
+
v_recipient uuid := gen_random_uuid();
|
|
74
|
+
v_owner uuid := gen_random_uuid();
|
|
75
|
+
v_a1 uuid := gen_random_uuid();
|
|
76
|
+
v_a2 uuid := gen_random_uuid();
|
|
77
|
+
v_a3 uuid := gen_random_uuid();
|
|
78
|
+
v_a4 uuid := gen_random_uuid();
|
|
79
|
+
v_a5 uuid := gen_random_uuid();
|
|
80
|
+
v_a6 uuid := gen_random_uuid();
|
|
81
|
+
v_domain_main uuid;
|
|
82
|
+
v_domain_revoked uuid;
|
|
83
|
+
v_m1 uuid; v_m2 uuid; v_m3 uuid; v_m4 uuid; v_m5 uuid; v_m6 uuid;
|
|
84
|
+
begin
|
|
85
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
86
|
+
values
|
|
87
|
+
(v_recipient, 'agent', 'react-recipient', 'React Recipient'),
|
|
88
|
+
(v_owner, 'agent', 'react-owner', 'React Owner'),
|
|
89
|
+
(v_a1, 'agent', 'react-sender-1', 'React Sender 1'),
|
|
90
|
+
(v_a2, 'agent', 'react-sender-2', 'React Sender 2'),
|
|
91
|
+
(v_a3, 'agent', 'react-sender-3', 'React Sender 3'),
|
|
92
|
+
(v_a4, 'agent', 'react-sender-4', 'React Sender 4'),
|
|
93
|
+
(v_a5, 'agent', 'react-sender-5', 'React Sender 5'),
|
|
94
|
+
(v_a6, 'agent', 'react-sender-6', 'React Sender 6');
|
|
95
|
+
|
|
96
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
97
|
+
values
|
|
98
|
+
('react-domain-main', 'React Domain Main', 'tailnet', v_owner, 'active', now()),
|
|
99
|
+
('react-domain-revoked', 'React Domain Revoked', 'tailnet', v_owner, 'active', now());
|
|
100
|
+
|
|
101
|
+
select id into v_domain_main from agent_board.fleet_trust_domains where slug = 'react-domain-main';
|
|
102
|
+
select id into v_domain_revoked from agent_board.fleet_trust_domains where slug = 'react-domain-revoked';
|
|
103
|
+
|
|
104
|
+
select machine_id into v_m1 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m1', 'sha256:react-m1', 'portal', '{"redacted":true}'::jsonb);
|
|
105
|
+
select machine_id into v_m2 from agent_board.register_fleet_machine(v_owner, v_domain_revoked, 'react-m2', 'sha256:react-m2', 'portal', '{"redacted":true}'::jsonb);
|
|
106
|
+
select machine_id into v_m3 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m3', 'sha256:react-m3', 'portal', '{"redacted":true}'::jsonb);
|
|
107
|
+
select machine_id into v_m4 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m4', 'sha256:react-m4', 'portal', '{"redacted":true}'::jsonb);
|
|
108
|
+
select machine_id into v_m5 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m5', 'sha256:react-m5', 'portal', '{"redacted":true}'::jsonb);
|
|
109
|
+
select machine_id into v_m6 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m6', 'sha256:react-m6', 'portal', '{"redacted":true}'::jsonb);
|
|
110
|
+
|
|
111
|
+
insert into agent_board.fleet_machine_agents(machine_id, agent_id, agent_role)
|
|
112
|
+
values
|
|
113
|
+
(v_m1, v_a1, 'phase_worker'),
|
|
114
|
+
(v_m2, v_a2, 'phase_worker'),
|
|
115
|
+
(v_m3, v_a3, 'phase_worker'),
|
|
116
|
+
(v_m4, v_a4, 'phase_worker'),
|
|
117
|
+
(v_m5, v_a5, 'phase_worker'),
|
|
118
|
+
(v_m6, v_a6, 'phase_worker');
|
|
119
|
+
|
|
120
|
+
-- Allow-by-default receive policy for the recipient: a non-revoked bound machine passes,
|
|
121
|
+
-- so the ONLY lever these assertions exercise is the machine-revocation state.
|
|
122
|
+
perform agent_board.set_fleet_receive_policy(v_recipient, 'react allow-by-default', 'allow', false, null, null, '{"senderPolicy":"open"}'::jsonb);
|
|
123
|
+
|
|
124
|
+
insert into _react_fx(recipient, owner, a1, a2, a3, a4, a5, a6, m1, m2, m3, m4, m5, m6, domain_main, domain_revoked)
|
|
125
|
+
values (v_recipient, v_owner, v_a1, v_a2, v_a3, v_a4, v_a5, v_a6, v_m1, v_m2, v_m3, v_m4, v_m5, v_m6, v_domain_main, v_domain_revoked);
|
|
126
|
+
end
|
|
127
|
+
$$;
|
|
128
|
+
|
|
129
|
+
------------------------------------------------------------------------------------------
|
|
130
|
+
-- CORE (m1): revoke → deny; reactivate → the SAME sender passes.
|
|
131
|
+
------------------------------------------------------------------------------------------
|
|
132
|
+
do $$
|
|
133
|
+
begin
|
|
134
|
+
perform agent_board.revoke_fleet_subject('machine', (select m1 from _react_fx), (select owner from _react_fx), 'core revoke', '{"redacted":true}'::jsonb);
|
|
135
|
+
end
|
|
136
|
+
$$;
|
|
137
|
+
|
|
138
|
+
-- (3) Revoked machine is denied.
|
|
139
|
+
select is(
|
|
140
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
141
|
+
(select a1 from _react_fx), (select recipient from _react_fx), (select m1 from _react_fx), null, 'send_direct_message', null, false)),
|
|
142
|
+
'machine_revoked',
|
|
143
|
+
'core: revoked machine is denied before reactivation'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
-- (4) Operator reactivation succeeds (allow-side lives_ok + separate state reads, #113).
|
|
147
|
+
select lives_ok(
|
|
148
|
+
$$ create temp table _react_m1 as
|
|
149
|
+
select * from agent_board.reactivate_fleet_machine(
|
|
150
|
+
(select m1 from _react_fx), (select owner from _react_fx), 'operator brings m1 back') $$,
|
|
151
|
+
'core: service_role reactivation of m1 succeeds'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
-- (5)(6)(7) Row is reconciled: reactivated_at stamped, lifecycle active, revoked_at cleared.
|
|
155
|
+
select ok(
|
|
156
|
+
(select reactivated_at is not null from agent_board.fleet_machines where id = (select m1 from _react_fx)),
|
|
157
|
+
'core: reactivation stamps reactivated_at'
|
|
158
|
+
);
|
|
159
|
+
select is(
|
|
160
|
+
(select lifecycle_state from agent_board.fleet_machines where id = (select m1 from _react_fx)),
|
|
161
|
+
'active',
|
|
162
|
+
'core: reactivation sets lifecycle_state=active'
|
|
163
|
+
);
|
|
164
|
+
select ok(
|
|
165
|
+
(select revoked_at is null from agent_board.fleet_machines where id = (select m1 from _react_fx)),
|
|
166
|
+
'core: reactivation clears revoked_at'
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
-- (8) The SAME sender now passes. MUTATION PIN: dropping the reactivated_at guard on the
|
|
170
|
+
-- machine clause makes the prior machine event match again → this flips to `not ok`.
|
|
171
|
+
select is(
|
|
172
|
+
(select allowed from agent_board.evaluate_fleet_sender_policy(
|
|
173
|
+
(select a1 from _react_fx), (select recipient from _react_fx), (select m1 from _react_fx), null, 'send_direct_message', null, false)),
|
|
174
|
+
true,
|
|
175
|
+
'core: reactivation restores sender authority (same sender now passes)'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
------------------------------------------------------------------------------------------
|
|
179
|
+
-- LEDGER AUTHORITY (m6): a re-revocation via the REAL writer denies again AND advances the
|
|
180
|
+
-- append-only ledger past reactivated_at (#146 panel MUST-FIX 1). now() is transaction-frozen,
|
|
181
|
+
-- so the fixture ages the original revocation and the reactivation to create a real gap.
|
|
182
|
+
------------------------------------------------------------------------------------------
|
|
183
|
+
do $$
|
|
184
|
+
begin
|
|
185
|
+
perform agent_board.revoke_fleet_subject('machine', (select m6 from _react_fx), (select owner from _react_fx), 'm6 initial revoke', '{"redacted":true}'::jsonb);
|
|
186
|
+
perform * from agent_board.reactivate_fleet_machine((select m6 from _react_fx), (select owner from _react_fx), 'm6 reactivate');
|
|
187
|
+
-- Simulate elapsed time (frozen now()): original revocation 2h ago, reactivation 1h ago.
|
|
188
|
+
-- Only fleet_machines.reactivated_at (a mutable marker) and the existing ledger row's
|
|
189
|
+
-- timestamp are aged; the RE-REVOCATION below uses the real writer, unmodified.
|
|
190
|
+
update agent_board.fleet_revocation_events
|
|
191
|
+
set effective_at = now() - interval '2 hours'
|
|
192
|
+
where subject_kind = 'machine' and subject_id = (select m6 from _react_fx);
|
|
193
|
+
update agent_board.fleet_machines
|
|
194
|
+
set reactivated_at = now() - interval '1 hour'
|
|
195
|
+
where id = (select m6 from _react_fx);
|
|
196
|
+
end
|
|
197
|
+
$$;
|
|
198
|
+
|
|
199
|
+
-- (9) After the aged reactivation, the aged prior revocation is superseded → sender passes.
|
|
200
|
+
select is(
|
|
201
|
+
(select allowed from agent_board.evaluate_fleet_sender_policy(
|
|
202
|
+
(select a6 from _react_fx), (select recipient from _react_fx), (select m6 from _react_fx), null, 'send_direct_message', null, false)),
|
|
203
|
+
true,
|
|
204
|
+
'ledger: reactivation holds over a real gap (aged prior revocation is superseded)'
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
-- (10) Re-revoke via the REAL writer.
|
|
208
|
+
select lives_ok(
|
|
209
|
+
$$ select * from agent_board.revoke_fleet_subject('machine', (select m6 from _react_fx), (select owner from _react_fx), 'm6 re-revoke', '{"redacted":true}'::jsonb) $$,
|
|
210
|
+
'ledger: re-revoking a reactivated machine via the real writer succeeds'
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
-- (11) Denied again. MUTATION PIN: with the writer fix the ledger event now post-dates
|
|
214
|
+
-- reactivated_at, so this denial survives removing the policy's lifecycle_state guard
|
|
215
|
+
-- (the event clause catches it — 2-of-2 depth).
|
|
216
|
+
select is(
|
|
217
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
218
|
+
(select a6 from _react_fx), (select recipient from _react_fx), (select m6 from _react_fx), null, 'send_direct_message', null, false)),
|
|
219
|
+
'machine_revoked',
|
|
220
|
+
'ledger: reactivated-then-revoked machine is denied again'
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
-- (12) MUST-FIX 1 PIN: the real writer advanced the append-only ledger past the supersession
|
|
224
|
+
-- point. Reverting revoke_fleet_subject's UPDATE branch (no `effective_at = now()`) leaves the
|
|
225
|
+
-- event at -2h < reactivated_at (-1h) → this flips to `not ok`.
|
|
226
|
+
select ok(
|
|
227
|
+
(
|
|
228
|
+
(select effective_at from agent_board.fleet_revocation_events
|
|
229
|
+
where subject_kind = 'machine' and subject_id = (select m6 from _react_fx))
|
|
230
|
+
> (select reactivated_at from agent_board.fleet_machines where id = (select m6 from _react_fx))
|
|
231
|
+
),
|
|
232
|
+
'ledger: re-revocation advances the append-only event effective_at past reactivated_at (ledger stays authoritative)'
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
------------------------------------------------------------------------------------------
|
|
236
|
+
-- DOMAIN (m2): reactivating the machine does NOT un-revoke its trust domain.
|
|
237
|
+
------------------------------------------------------------------------------------------
|
|
238
|
+
do $$
|
|
239
|
+
begin
|
|
240
|
+
perform agent_board.revoke_fleet_subject('machine', (select m2 from _react_fx), (select owner from _react_fx), 'domain-case machine revoke', '{"redacted":true}'::jsonb);
|
|
241
|
+
perform agent_board.revoke_fleet_subject('trust_domain', (select domain_revoked from _react_fx), (select owner from _react_fx), 'domain revoke', '{"redacted":true}'::jsonb);
|
|
242
|
+
end
|
|
243
|
+
$$;
|
|
244
|
+
|
|
245
|
+
-- (13) Reactivate the machine only.
|
|
246
|
+
select lives_ok(
|
|
247
|
+
$$ select * from agent_board.reactivate_fleet_machine((select m2 from _react_fx), (select owner from _react_fx), 'reactivate m2 in revoked domain') $$,
|
|
248
|
+
'domain: reactivating m2 (whose domain is revoked) succeeds'
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
-- (14) Still denied: the trust_domain clause is unchanged by machine reactivation.
|
|
252
|
+
-- MUTATION PIN: adding the reactivated_at guard to the trust_domain clause flips this.
|
|
253
|
+
select is(
|
|
254
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
255
|
+
(select a2 from _react_fx), (select recipient from _react_fx), (select m2 from _react_fx), null, 'send_direct_message', null, false)),
|
|
256
|
+
'machine_revoked',
|
|
257
|
+
'domain: machine reactivation does NOT un-revoke a revoked trust domain'
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
------------------------------------------------------------------------------------------
|
|
261
|
+
-- AGENT (m3): reactivating the machine does NOT un-revoke the sender agent.
|
|
262
|
+
------------------------------------------------------------------------------------------
|
|
263
|
+
do $$
|
|
264
|
+
begin
|
|
265
|
+
perform agent_board.revoke_fleet_subject('machine', (select m3 from _react_fx), (select owner from _react_fx), 'agent-case machine revoke', '{"redacted":true}'::jsonb);
|
|
266
|
+
perform agent_board.revoke_fleet_subject('agent', (select a3 from _react_fx), (select a3 from _react_fx), 'agent revoke', '{"redacted":true}'::jsonb);
|
|
267
|
+
end
|
|
268
|
+
$$;
|
|
269
|
+
|
|
270
|
+
-- (15) Reactivate the machine only.
|
|
271
|
+
select lives_ok(
|
|
272
|
+
$$ select * from agent_board.reactivate_fleet_machine((select m3 from _react_fx), (select owner from _react_fx), 'reactivate m3 with revoked agent') $$,
|
|
273
|
+
'agent: reactivating m3 (whose sender agent is revoked) succeeds'
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
-- (16) Still denied: the agent clause is unchanged by machine reactivation.
|
|
277
|
+
-- MUTATION PIN: adding the reactivated_at guard to the agent clause flips this.
|
|
278
|
+
select is(
|
|
279
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
280
|
+
(select a3 from _react_fx), (select recipient from _react_fx), (select m3 from _react_fx), null, 'send_direct_message', null, false)),
|
|
281
|
+
'machine_revoked',
|
|
282
|
+
'agent: machine reactivation does NOT un-revoke a revoked sender agent'
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
------------------------------------------------------------------------------------------
|
|
286
|
+
-- AUTHZ (m4): operator-only. An authenticated caller — even the machine owner — is refused.
|
|
287
|
+
------------------------------------------------------------------------------------------
|
|
288
|
+
do $$
|
|
289
|
+
begin
|
|
290
|
+
perform agent_board.revoke_fleet_subject('machine', (select m4 from _react_fx), (select owner from _react_fx), 'authz-case revoke', '{"redacted":true}'::jsonb);
|
|
291
|
+
end
|
|
292
|
+
$$;
|
|
293
|
+
|
|
294
|
+
-- Map the owner agent to an auth user so the authenticated path resolves a principal.
|
|
295
|
+
do $$
|
|
296
|
+
declare
|
|
297
|
+
v_user_id uuid;
|
|
298
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
299
|
+
begin
|
|
300
|
+
v_user_id := public.ensure_github_user(
|
|
301
|
+
'react-owner-' || v_suffix,
|
|
302
|
+
'react-owner-' || v_suffix || '@example.test',
|
|
303
|
+
'react-owner-' || v_suffix,
|
|
304
|
+
'React Owner ' || v_suffix
|
|
305
|
+
);
|
|
306
|
+
update agent_board.agents set auth_user_id = v_user_id where id = (select owner from _react_fx);
|
|
307
|
+
update _react_fx set owner_user_id = v_user_id;
|
|
308
|
+
end
|
|
309
|
+
$$;
|
|
310
|
+
|
|
311
|
+
create temp table _react_authz (denied boolean) on commit drop;
|
|
312
|
+
grant all on table _react_authz to authenticated, service_role;
|
|
313
|
+
|
|
314
|
+
-- Isolate the BODY guard from the grant: temporarily grant EXECUTE to authenticated so a
|
|
315
|
+
-- call reaching the function body is refused by _is_service_role(), not the ACL. (Rolled
|
|
316
|
+
-- back with the test transaction.) This pins the "callable by authenticated" mutation to the
|
|
317
|
+
-- guard itself, independent of the service_role-only grant restated in the migration.
|
|
318
|
+
grant execute on function agent_board.reactivate_fleet_machine(uuid, uuid, text) to authenticated;
|
|
319
|
+
|
|
320
|
+
set local role authenticated;
|
|
321
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
322
|
+
select set_config('request.jwt.claim.sub', (select owner_user_id::text from _react_fx), true);
|
|
323
|
+
|
|
324
|
+
do $$
|
|
325
|
+
begin
|
|
326
|
+
begin
|
|
327
|
+
perform * from agent_board.reactivate_fleet_machine(
|
|
328
|
+
(select m4 from _react_fx), (select owner from _react_fx), 'authenticated owner attempt');
|
|
329
|
+
insert into _react_authz values (false); -- reached completion → NOT denied
|
|
330
|
+
exception
|
|
331
|
+
when sqlstate '42501' then insert into _react_authz values (true);
|
|
332
|
+
end;
|
|
333
|
+
end
|
|
334
|
+
$$;
|
|
335
|
+
|
|
336
|
+
reset role;
|
|
337
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
338
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
339
|
+
revoke execute on function agent_board.reactivate_fleet_machine(uuid, uuid, text) from authenticated;
|
|
340
|
+
|
|
341
|
+
-- (17) MUTATION PIN: removing the _is_service_role() body guard lets the authenticated call
|
|
342
|
+
-- complete → denied=false → this flips to `not ok`.
|
|
343
|
+
select ok(
|
|
344
|
+
(select denied from _react_authz),
|
|
345
|
+
'authz: authenticated caller (even the owner) cannot reactivate (42501)'
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
------------------------------------------------------------------------------------------
|
|
349
|
+
-- NON-REVOKED (m5): reactivating a machine that is not revoked raises (nothing to do).
|
|
350
|
+
------------------------------------------------------------------------------------------
|
|
351
|
+
-- (18)
|
|
352
|
+
select throws_ok(
|
|
353
|
+
$$ select * from agent_board.reactivate_fleet_machine((select m5 from _react_fx), (select owner from _react_fx), 'reactivate a live machine') $$,
|
|
354
|
+
'22023',
|
|
355
|
+
NULL,
|
|
356
|
+
'non-revoked: reactivating a machine that is not revoked raises'
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
------------------------------------------------------------------------------------------
|
|
360
|
+
-- READ MODEL (MUST-FIX 2): v_fleet_machine_status.machine_revoked honors reactivated_at.
|
|
361
|
+
------------------------------------------------------------------------------------------
|
|
362
|
+
-- (19) MUTATION PIN: reverting machine_revoked to bare event-existence flips this — m1 was
|
|
363
|
+
-- reactivated, so its lingering (superseded) event must NOT read as revoked.
|
|
364
|
+
select is(
|
|
365
|
+
(select machine_revoked from agent_board.v_fleet_machine_status where machine_id = (select m1 from _react_fx)),
|
|
366
|
+
false,
|
|
367
|
+
'view: a reactivated machine reports machine_revoked=false (agrees with the policy)'
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
-- (20) Sanity: a genuinely revoked-and-not-reactivated machine still reads revoked.
|
|
371
|
+
select is(
|
|
372
|
+
(select machine_revoked from agent_board.v_fleet_machine_status where machine_id = (select m4 from _react_fx)),
|
|
373
|
+
true,
|
|
374
|
+
'view: a revoked (never reactivated) machine still reports machine_revoked=true'
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
------------------------------------------------------------------------------------------
|
|
378
|
+
-- CLASS (rule #1): catalog-derived — every machine-subject revocation READER honors
|
|
379
|
+
-- reactivated_at; the WRITER exemption is asserted positively.
|
|
380
|
+
------------------------------------------------------------------------------------------
|
|
381
|
+
-- (21) Enumerate readers (functions + views) whose definition tests a machine-subject
|
|
382
|
+
-- revocation event (`fre.subject_kind = 'machine'`) but does NOT reference reactivated_at.
|
|
383
|
+
-- Expected: none. A new non-conforming reader appears here BY NAME (rule #1: offenders name
|
|
384
|
+
-- themselves). Aggregates are excluded (pg_get_functiondef errors on them; prokind='f').
|
|
385
|
+
select is(
|
|
386
|
+
(
|
|
387
|
+
select string_agg(offender, ', ' order by offender)
|
|
388
|
+
from (
|
|
389
|
+
select p.proname as offender
|
|
390
|
+
from pg_proc p
|
|
391
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
392
|
+
where n.nspname = 'agent_board'
|
|
393
|
+
and p.prokind = 'f'
|
|
394
|
+
and pg_get_functiondef(p.oid) ~ 'fre\.subject_kind = ''machine'''
|
|
395
|
+
and pg_get_functiondef(p.oid) not ilike '%reactivated_at%'
|
|
396
|
+
union all
|
|
397
|
+
select c.relname
|
|
398
|
+
from pg_class c
|
|
399
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
400
|
+
where n.nspname = 'agent_board'
|
|
401
|
+
and c.relkind = 'v'
|
|
402
|
+
and pg_get_viewdef(c.oid) ~ 'fre\.subject_kind = ''machine'''
|
|
403
|
+
and pg_get_viewdef(c.oid) not ilike '%reactivated_at%'
|
|
404
|
+
) offenders
|
|
405
|
+
),
|
|
406
|
+
null,
|
|
407
|
+
'class: every machine-subject revocation reader honors reactivated_at (offenders name themselves)'
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
-- (22) Positive exemption (rule #1): revoke_fleet_subject is the machine-revocation WRITER,
|
|
411
|
+
-- not a state reader — its exemption claim is that it advances the ledger's effective_at.
|
|
412
|
+
select ok(
|
|
413
|
+
(
|
|
414
|
+
select pg_get_functiondef(p.oid) ~ 'effective_at = now\(\)'
|
|
415
|
+
from pg_proc p
|
|
416
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
417
|
+
where n.nspname = 'agent_board'
|
|
418
|
+
and p.prokind = 'f'
|
|
419
|
+
and p.proname = 'revoke_fleet_subject'
|
|
420
|
+
),
|
|
421
|
+
'class exemption: revoke_fleet_subject (the writer) advances effective_at=now() on re-revocation'
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
select * from finish();
|
|
425
|
+
|
|
426
|
+
rollback;
|