@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,419 @@
|
|
|
1
|
+
-- #153 whole-domain squatting arbitration — reassign a squatted fleet_trust_domain's owner.
|
|
2
|
+
--
|
|
3
|
+
-- reclaim_fleet_trust_domain (service_role only) is the operator arbitration path for a WHOLE
|
|
4
|
+
-- domain squatted by someone other than its rightful owner — the residual #150's machine-level
|
|
5
|
+
-- reclaim could not cover. It reassigns fleet_trust_domains.owner_agent_id to the arbitrated new
|
|
6
|
+
-- owner under a row lock; the reassignment is atomic at the domain row and post-commit every authz
|
|
7
|
+
-- site (revoke_fleet_subject('trust_domain'), register_fleet_machine, set_fleet_receive_policy) reads
|
|
8
|
+
-- the new owner. SERIALIZATION CAVEAT (#159): only revoke_fleet_subject('trust_domain') is locked
|
|
9
|
+
-- against an in-flight reclaim here; register_fleet_machine / set_fleet_receive_policy are NOT yet
|
|
10
|
+
-- serialized (tracked in #159). It writes NO fleet_revocation_events row (a trust_domain ledger row
|
|
11
|
+
-- over-denies the whole domain incl. the new owner — #150 divergence rationale), and APPENDS the
|
|
12
|
+
-- audit to metadata.reclaim_history.
|
|
13
|
+
--
|
|
14
|
+
-- These assertions pin:
|
|
15
|
+
-- * reclaim reassigns owner_agent_id (state + returned prior owner) — PIN: skip the reassign ⇒
|
|
16
|
+
-- (3)/(10)/(12)/(13) fail (owner stays squatter, authority does not move).
|
|
17
|
+
-- * NO trust_domain revocation event is written by reclaim (7) — PIN: add a revoke-event write ⇒
|
|
18
|
+
-- a row appears ⇒ (7) fails. Pins the over-deny divergence rationale.
|
|
19
|
+
-- * NON-GOAL scope: a machine in the reclaimed domain KEEPS owner=squatter (8) — PIN: also
|
|
20
|
+
-- reassign machine owners ⇒ (8) fails. Documents domain-vs-machine separation.
|
|
21
|
+
-- * AUTHORITY BRACKET (rule #2/#3): each authority probe is bracketed by a BEFORE probe under the
|
|
22
|
+
-- same authenticated principal proving current_agent_id() resolves — so an AFTER flip is a
|
|
23
|
+
-- genuine authority change, not a null-actor artifact. register_fleet_machine into the domain is
|
|
24
|
+
-- the reversible authority lever: squatter CAN before / CANNOT after; new owner CANNOT before /
|
|
25
|
+
-- CAN after (9-12). The kill switch itself: squatter's revoke('trust_domain') is REFUSED after
|
|
26
|
+
-- reclaim (13) — PIN: skip reassign ⇒ squatter still owns ⇒ not refused ⇒ (13) fails.
|
|
27
|
+
-- * reclaim is operator-only: an authenticated caller (the victim) is refused 42501 (14) — PIN:
|
|
28
|
+
-- remove the _is_service_role() body guard ⇒ (14) fails (body guard isolated from the ACL by a
|
|
29
|
+
-- temp grant to authenticated).
|
|
30
|
+
-- * reclaim validates the new owner + provenance agent exist and the domain exists (15,16,17).
|
|
31
|
+
-- * REVOKED-DOMAIN RESIDUAL (documented, positive pin): reclaim of a domain the squatter already
|
|
32
|
+
-- revoked SUCCEEDS + transfers owner (18,19) but the domain STAYS revoked (20) — no domain
|
|
33
|
+
-- un-revoke path today. Pins the boundary positively (rule #1) rather than leaving a silent gap.
|
|
34
|
+
-- * AUDIT APPEND (rule #2): a second reclaim APPENDS to reclaim_history (21) — PIN: overwrite ⇒
|
|
35
|
+
-- length stays 1 ⇒ (21) fails.
|
|
36
|
+
-- * CLASS (rule #1 / EC-4): reclaim_fleet_trust_domain is the ONLY function-writer of
|
|
37
|
+
-- fleet_trust_domains.owner_agent_id (22) — catalog-derived, so a future owner-writer names
|
|
38
|
+
-- itself. (No RPC INSERTs a domain; domains are seeded by direct table INSERT, which is not a
|
|
39
|
+
-- function and cannot match.)
|
|
40
|
+
--
|
|
41
|
+
-- Tests run as session_user=postgres ⇒ _is_service_role() is true; probes and the authz assertion
|
|
42
|
+
-- flip to role=authenticated (jwt claims) to exercise the principal / body guard.
|
|
43
|
+
|
|
44
|
+
create extension if not exists pgtap with schema extensions;
|
|
45
|
+
set search_path = extensions, public, agent_board;
|
|
46
|
+
|
|
47
|
+
begin;
|
|
48
|
+
|
|
49
|
+
select plan(22);
|
|
50
|
+
|
|
51
|
+
-- (1) Shape.
|
|
52
|
+
select has_function(
|
|
53
|
+
'agent_board', 'reclaim_fleet_trust_domain',
|
|
54
|
+
array['uuid', 'uuid', 'uuid', 'text'],
|
|
55
|
+
'reclaim_fleet_trust_domain exists'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
create temp table _dr_fx (
|
|
59
|
+
operator uuid, -- arbitration actor (p_reclaimed_by); no auth mapping needed
|
|
60
|
+
squatter uuid, -- squats the domains; owner of dom_core/dom_revoked/dom_authz
|
|
61
|
+
new_owner uuid, -- arbitrated rightful owner
|
|
62
|
+
victim uuid, -- used for the operator-only authz probe
|
|
63
|
+
dom_core uuid, -- main reclaim + authority + kill-switch
|
|
64
|
+
dom_revoked uuid, -- revoked by the squatter BEFORE reclaim (residual pin)
|
|
65
|
+
dom_authz uuid, -- operator-only probe target
|
|
66
|
+
m_core uuid, -- a machine in dom_core (non-goal: stays owned by squatter)
|
|
67
|
+
squatter_user uuid,
|
|
68
|
+
new_owner_user uuid,
|
|
69
|
+
victim_user uuid
|
|
70
|
+
) on commit drop;
|
|
71
|
+
grant all on table _dr_fx to authenticated, service_role;
|
|
72
|
+
|
|
73
|
+
create temp table _dr_probe (label text, allowed boolean) on commit drop;
|
|
74
|
+
grant all on table _dr_probe to authenticated, service_role;
|
|
75
|
+
|
|
76
|
+
do $$
|
|
77
|
+
declare
|
|
78
|
+
v_operator uuid := gen_random_uuid();
|
|
79
|
+
v_squatter uuid := gen_random_uuid();
|
|
80
|
+
v_new_owner uuid := gen_random_uuid();
|
|
81
|
+
v_victim uuid := gen_random_uuid();
|
|
82
|
+
v_dom_core uuid;
|
|
83
|
+
v_dom_revoked uuid;
|
|
84
|
+
v_dom_authz uuid;
|
|
85
|
+
v_m_core uuid;
|
|
86
|
+
begin
|
|
87
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
88
|
+
values
|
|
89
|
+
(v_operator, 'agent', 'dr-operator', 'DR Operator'),
|
|
90
|
+
(v_squatter, 'agent', 'dr-squatter', 'DR Squatter'),
|
|
91
|
+
(v_new_owner, 'agent', 'dr-new-owner', 'DR New Owner'),
|
|
92
|
+
(v_victim, 'agent', 'dr-victim', 'DR Victim');
|
|
93
|
+
|
|
94
|
+
-- All three domains are squatted (owned by the squatter). dom_core + dom_authz active; dom_revoked
|
|
95
|
+
-- active now (revoked below via the squatter's own kill switch to exercise the residual).
|
|
96
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
97
|
+
values
|
|
98
|
+
('dr-domain-core', 'DR Domain Core', 'tailnet', v_squatter, 'active', now()),
|
|
99
|
+
('dr-domain-revoked', 'DR Domain Revoked', 'tailnet', v_squatter, 'active', now()),
|
|
100
|
+
('dr-domain-authz', 'DR Domain Authz', 'tailnet', v_squatter, 'active', now());
|
|
101
|
+
select id into v_dom_core from agent_board.fleet_trust_domains where slug = 'dr-domain-core';
|
|
102
|
+
select id into v_dom_revoked from agent_board.fleet_trust_domains where slug = 'dr-domain-revoked';
|
|
103
|
+
select id into v_dom_authz from agent_board.fleet_trust_domains where slug = 'dr-domain-authz';
|
|
104
|
+
|
|
105
|
+
-- A machine the squatter owns inside dom_core (non-goal: domain reclaim must NOT reassign it).
|
|
106
|
+
select machine_id into v_m_core from agent_board.register_fleet_machine(
|
|
107
|
+
v_squatter, v_dom_core, 'dr-m-core', 'sha256:dr-m-core', 'portal', '{"redacted":true}'::jsonb);
|
|
108
|
+
|
|
109
|
+
insert into _dr_fx(operator, squatter, new_owner, victim, dom_core, dom_revoked, dom_authz, m_core)
|
|
110
|
+
values (v_operator, v_squatter, v_new_owner, v_victim, v_dom_core, v_dom_revoked, v_dom_authz, v_m_core);
|
|
111
|
+
end
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
-- Map the squatter, new owner and victim agents to auth users so the authenticated probes/authz
|
|
115
|
+
-- call resolve a principal via current_agent_id() → auth.uid().
|
|
116
|
+
do $$
|
|
117
|
+
declare
|
|
118
|
+
v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
119
|
+
v_squatter_user uuid;
|
|
120
|
+
v_new_owner_user uuid;
|
|
121
|
+
v_victim_user uuid;
|
|
122
|
+
begin
|
|
123
|
+
v_squatter_user := public.ensure_github_user('dr-squatter-' || v_sfx, 'dr-squatter-' || v_sfx || '@example.test', 'dr-squatter-' || v_sfx, 'DR Squatter ' || v_sfx);
|
|
124
|
+
v_new_owner_user := public.ensure_github_user('dr-newowner-' || v_sfx, 'dr-newowner-' || v_sfx || '@example.test', 'dr-newowner-' || v_sfx, 'DR New Owner ' || v_sfx);
|
|
125
|
+
v_victim_user := public.ensure_github_user('dr-victim-' || v_sfx, 'dr-victim-' || v_sfx || '@example.test', 'dr-victim-' || v_sfx, 'DR Victim ' || v_sfx);
|
|
126
|
+
|
|
127
|
+
update agent_board.agents set auth_user_id = v_squatter_user where id = (select squatter from _dr_fx);
|
|
128
|
+
update agent_board.agents set auth_user_id = v_new_owner_user where id = (select new_owner from _dr_fx);
|
|
129
|
+
update agent_board.agents set auth_user_id = v_victim_user where id = (select victim from _dr_fx);
|
|
130
|
+
|
|
131
|
+
update _dr_fx set squatter_user = v_squatter_user, new_owner_user = v_new_owner_user, victim_user = v_victim_user;
|
|
132
|
+
end
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
------------------------------------------------------------------------------------------
|
|
136
|
+
-- BEFORE authority probes (rule #2/#3 bracket): register_fleet_machine into dom_core is the
|
|
137
|
+
-- reversible authority lever (requires ftd.owner_agent_id = actor). The squatter (owner) CAN; the
|
|
138
|
+
-- new owner CANNOT — proving both principals resolve, so the AFTER flips below are genuine.
|
|
139
|
+
------------------------------------------------------------------------------------------
|
|
140
|
+
set local role authenticated;
|
|
141
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
142
|
+
select set_config('request.jwt.claim.sub', (select squatter_user::text from _dr_fx), true);
|
|
143
|
+
do $$
|
|
144
|
+
begin
|
|
145
|
+
begin
|
|
146
|
+
perform agent_board.register_fleet_machine((select squatter from _dr_fx), (select dom_core from _dr_fx),
|
|
147
|
+
'dr-m-sqbefore', 'sha256:dr-m-sqbefore', 'portal', '{"redacted":true}'::jsonb);
|
|
148
|
+
insert into _dr_probe values ('squatter_before', true);
|
|
149
|
+
exception when sqlstate '42501' then insert into _dr_probe values ('squatter_before', false);
|
|
150
|
+
end;
|
|
151
|
+
end $$;
|
|
152
|
+
select set_config('request.jwt.claim.sub', (select new_owner_user::text from _dr_fx), true);
|
|
153
|
+
do $$
|
|
154
|
+
begin
|
|
155
|
+
begin
|
|
156
|
+
perform agent_board.register_fleet_machine((select new_owner from _dr_fx), (select dom_core from _dr_fx),
|
|
157
|
+
'dr-m-nobefore', 'sha256:dr-m-nobefore', 'portal', '{"redacted":true}'::jsonb);
|
|
158
|
+
insert into _dr_probe values ('new_owner_before', true);
|
|
159
|
+
exception when sqlstate '42501' then insert into _dr_probe values ('new_owner_before', false);
|
|
160
|
+
end;
|
|
161
|
+
end $$;
|
|
162
|
+
reset role;
|
|
163
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
164
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
165
|
+
|
|
166
|
+
------------------------------------------------------------------------------------------
|
|
167
|
+
-- CORE: operator reclaims dom_core from the squatter to the new owner.
|
|
168
|
+
------------------------------------------------------------------------------------------
|
|
169
|
+
-- (2) Reclaim succeeds (allow-side lives_ok + separate state reads, #113).
|
|
170
|
+
select lives_ok(
|
|
171
|
+
$$ create temp table _dr_core as
|
|
172
|
+
select * from agent_board.reclaim_fleet_trust_domain(
|
|
173
|
+
(select dom_core from _dr_fx), (select new_owner from _dr_fx),
|
|
174
|
+
(select operator from _dr_fx), 'operator arbitrated: domain belongs to new owner') $$,
|
|
175
|
+
'core: service_role reclaim of dom_core succeeds'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
-- (3) Owner is reassigned. MUTATION PIN: skipping the owner reassignment leaves owner=squatter.
|
|
179
|
+
select is(
|
|
180
|
+
(select owner_agent_id from agent_board.fleet_trust_domains where id = (select dom_core from _dr_fx)),
|
|
181
|
+
(select new_owner from _dr_fx),
|
|
182
|
+
'core: reclaim reassigns owner_agent_id to the new owner'
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
-- (4) The returned prior owner is the squatter.
|
|
186
|
+
select is(
|
|
187
|
+
(select prior_owner_agent_id from _dr_core),
|
|
188
|
+
(select squatter from _dr_fx),
|
|
189
|
+
'core: reclaim returns the prior owner (the squatter)'
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
-- (5) Audit APPENDED to metadata.reclaim_history: last record has operator + prior owner.
|
|
193
|
+
select ok(
|
|
194
|
+
(select (metadata -> 'reclaim_history' -> -1 ->> 'reclaimed_by') = (select operator::text from _dr_fx)
|
|
195
|
+
and (metadata -> 'reclaim_history' -> -1 ->> 'prior_owner') = (select squatter::text from _dr_fx)
|
|
196
|
+
from agent_board.fleet_trust_domains where id = (select dom_core from _dr_fx)),
|
|
197
|
+
'core: reclaim audit (reclaimed_by + prior owner) appended to metadata.reclaim_history'
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
-- (6) The audit records the prior lifecycle (active here) — makes the revoked residual visible.
|
|
201
|
+
select is(
|
|
202
|
+
(select metadata -> 'reclaim_history' -> -1 ->> 'prior_lifecycle_state'
|
|
203
|
+
from agent_board.fleet_trust_domains where id = (select dom_core from _dr_fx)),
|
|
204
|
+
'active',
|
|
205
|
+
'core: reclaim audit records prior_lifecycle_state'
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
-- (7) NO trust_domain revocation event is written by reclaim (over-deny divergence rationale).
|
|
209
|
+
-- MUTATION PIN: adding a fleet_revocation_events write to reclaim makes a row appear → this flips.
|
|
210
|
+
select is(
|
|
211
|
+
(select count(*)::integer from agent_board.fleet_revocation_events
|
|
212
|
+
where subject_kind = 'trust_domain' and subject_id = (select dom_core from _dr_fx)),
|
|
213
|
+
0,
|
|
214
|
+
'core: reclaim writes NO trust_domain revocation event (no over-deny)'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
-- (8) NON-GOAL: a machine in dom_core KEEPS owner=squatter (domain reclaim ≠ machine reclaim).
|
|
218
|
+
-- MUTATION PIN: also reassigning machine owners in reclaim flips this to new_owner.
|
|
219
|
+
select is(
|
|
220
|
+
(select owner_agent_id from agent_board.fleet_machines where id = (select m_core from _dr_fx)),
|
|
221
|
+
(select squatter from _dr_fx),
|
|
222
|
+
'non-goal: a machine in the reclaimed domain keeps owner=squatter'
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
------------------------------------------------------------------------------------------
|
|
226
|
+
-- AFTER authority probes: register authority on dom_core post-reclaim, then the kill switch.
|
|
227
|
+
------------------------------------------------------------------------------------------
|
|
228
|
+
set local role authenticated;
|
|
229
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
230
|
+
select set_config('request.jwt.claim.sub', (select squatter_user::text from _dr_fx), true);
|
|
231
|
+
do $$
|
|
232
|
+
begin
|
|
233
|
+
begin
|
|
234
|
+
perform agent_board.register_fleet_machine((select squatter from _dr_fx), (select dom_core from _dr_fx),
|
|
235
|
+
'dr-m-sqafter', 'sha256:dr-m-sqafter', 'portal', '{"redacted":true}'::jsonb);
|
|
236
|
+
insert into _dr_probe values ('squatter_after', true);
|
|
237
|
+
exception when sqlstate '42501' then insert into _dr_probe values ('squatter_after', false);
|
|
238
|
+
end;
|
|
239
|
+
end $$;
|
|
240
|
+
select set_config('request.jwt.claim.sub', (select new_owner_user::text from _dr_fx), true);
|
|
241
|
+
do $$
|
|
242
|
+
begin
|
|
243
|
+
begin
|
|
244
|
+
perform agent_board.register_fleet_machine((select new_owner from _dr_fx), (select dom_core from _dr_fx),
|
|
245
|
+
'dr-m-noafter', 'sha256:dr-m-noafter', 'portal', '{"redacted":true}'::jsonb);
|
|
246
|
+
insert into _dr_probe values ('new_owner_after', true);
|
|
247
|
+
exception when sqlstate '42501' then insert into _dr_probe values ('new_owner_after', false);
|
|
248
|
+
end;
|
|
249
|
+
end $$;
|
|
250
|
+
-- Kill switch: the squatter (no longer owner) tries revoke_fleet_subject('trust_domain', dom_core).
|
|
251
|
+
select set_config('request.jwt.claim.sub', (select squatter_user::text from _dr_fx), true);
|
|
252
|
+
do $$
|
|
253
|
+
begin
|
|
254
|
+
begin
|
|
255
|
+
perform agent_board.revoke_fleet_subject('trust_domain', (select dom_core from _dr_fx),
|
|
256
|
+
(select squatter from _dr_fx), 'squatter tries the kill switch after losing the domain',
|
|
257
|
+
'{"redacted":true}'::jsonb);
|
|
258
|
+
insert into _dr_probe values ('squatter_killswitch_denied', false); -- reached completion → NOT denied
|
|
259
|
+
exception when sqlstate '42501' then insert into _dr_probe values ('squatter_killswitch_denied', true);
|
|
260
|
+
end;
|
|
261
|
+
end $$;
|
|
262
|
+
reset role;
|
|
263
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
264
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
265
|
+
|
|
266
|
+
-- (9)(10) Authority lever (register): squatter COULD before (bracket) → CANNOT after (PIN skip reassign).
|
|
267
|
+
select is((select allowed from _dr_probe where label = 'squatter_before'), true,
|
|
268
|
+
'bracket: the squatter (owner) could register into dom_core before reclaim');
|
|
269
|
+
select is((select allowed from _dr_probe where label = 'squatter_after'), false,
|
|
270
|
+
'squatter: loses domain authority (cannot register into dom_core) after reclaim');
|
|
271
|
+
-- (11)(12) New owner: CANNOT before (bracket) → CAN after (PIN skip reassign).
|
|
272
|
+
select is((select allowed from _dr_probe where label = 'new_owner_before'), false,
|
|
273
|
+
'bracket: the new owner could NOT register into dom_core before reclaim');
|
|
274
|
+
select is((select allowed from _dr_probe where label = 'new_owner_after'), true,
|
|
275
|
+
'new owner: gains domain authority (can register into dom_core) after reclaim');
|
|
276
|
+
-- (13) KILL SWITCH removed: the squatter's revoke('trust_domain') is refused after reclaim.
|
|
277
|
+
-- MUTATION PIN: skipping the owner reassign leaves the squatter owning dom_core → revoke is NOT
|
|
278
|
+
-- refused → denied=false → this flips.
|
|
279
|
+
select is((select allowed from _dr_probe where label = 'squatter_killswitch_denied'), true,
|
|
280
|
+
'kill-switch: the squatter''s revoke(trust_domain) is refused (42501) after reclaim');
|
|
281
|
+
|
|
282
|
+
------------------------------------------------------------------------------------------
|
|
283
|
+
-- AUTHZ (dom_authz): operator-only. An authenticated caller (the victim) is refused. The BODY guard
|
|
284
|
+
-- is isolated from the service_role-only grant by a temp grant to authenticated.
|
|
285
|
+
------------------------------------------------------------------------------------------
|
|
286
|
+
create temp table _dr_authz (denied boolean) on commit drop;
|
|
287
|
+
grant all on table _dr_authz to authenticated, service_role;
|
|
288
|
+
|
|
289
|
+
grant execute on function agent_board.reclaim_fleet_trust_domain(uuid, uuid, uuid, text) to authenticated;
|
|
290
|
+
|
|
291
|
+
set local role authenticated;
|
|
292
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
293
|
+
select set_config('request.jwt.claim.sub', (select victim_user::text from _dr_fx), true);
|
|
294
|
+
do $$
|
|
295
|
+
begin
|
|
296
|
+
begin
|
|
297
|
+
perform * from agent_board.reclaim_fleet_trust_domain(
|
|
298
|
+
(select dom_authz from _dr_fx), (select victim from _dr_fx),
|
|
299
|
+
(select victim from _dr_fx), 'victim self-reclaim attempt');
|
|
300
|
+
insert into _dr_authz values (false); -- reached completion → NOT denied
|
|
301
|
+
exception when sqlstate '42501' then insert into _dr_authz values (true);
|
|
302
|
+
end;
|
|
303
|
+
end
|
|
304
|
+
$$;
|
|
305
|
+
reset role;
|
|
306
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
307
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
308
|
+
revoke execute on function agent_board.reclaim_fleet_trust_domain(uuid, uuid, uuid, text) from authenticated;
|
|
309
|
+
|
|
310
|
+
-- (14) MUTATION PIN: removing the _is_service_role() body guard lets the authenticated call
|
|
311
|
+
-- complete → denied=false → this flips.
|
|
312
|
+
select ok(
|
|
313
|
+
(select denied from _dr_authz),
|
|
314
|
+
'authz: authenticated caller (the victim) cannot reclaim (42501) — operator-only'
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
------------------------------------------------------------------------------------------
|
|
318
|
+
-- VALIDATION.
|
|
319
|
+
------------------------------------------------------------------------------------------
|
|
320
|
+
-- (15) unknown new owner
|
|
321
|
+
select throws_ok(
|
|
322
|
+
format($$ select * from agent_board.reclaim_fleet_trust_domain(%L::uuid, %L::uuid, %L::uuid, 'unknown new owner') $$,
|
|
323
|
+
(select dom_authz from _dr_fx), gen_random_uuid(), (select operator from _dr_fx)),
|
|
324
|
+
'22023', NULL, 'validate: reclaim to an unknown new owner raises'
|
|
325
|
+
);
|
|
326
|
+
-- (16) unknown provenance
|
|
327
|
+
select throws_ok(
|
|
328
|
+
format($$ select * from agent_board.reclaim_fleet_trust_domain(%L::uuid, %L::uuid, %L::uuid, 'unknown provenance') $$,
|
|
329
|
+
(select dom_authz from _dr_fx), (select new_owner from _dr_fx), gen_random_uuid()),
|
|
330
|
+
'22023', NULL, 'validate: reclaim with an unknown reclaimed_by agent raises'
|
|
331
|
+
);
|
|
332
|
+
-- (17) unknown domain
|
|
333
|
+
select throws_ok(
|
|
334
|
+
format($$ select * from agent_board.reclaim_fleet_trust_domain(%L::uuid, %L::uuid, %L::uuid, 'unknown domain') $$,
|
|
335
|
+
gen_random_uuid(), (select new_owner from _dr_fx), (select operator from _dr_fx)),
|
|
336
|
+
'22023', NULL, 'validate: reclaim of a non-existent domain raises'
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
------------------------------------------------------------------------------------------
|
|
340
|
+
-- REVOKED-DOMAIN RESIDUAL (documented, positive pin): the squatter revokes dom_revoked first; the
|
|
341
|
+
-- operator then reclaims it. Ownership transfers, but the domain STAYS revoked (no un-revoke path).
|
|
342
|
+
------------------------------------------------------------------------------------------
|
|
343
|
+
do $$
|
|
344
|
+
begin
|
|
345
|
+
perform agent_board.revoke_fleet_subject('trust_domain', (select dom_revoked from _dr_fx),
|
|
346
|
+
(select squatter from _dr_fx), 'squatter revoke before arbitration', '{"redacted":true}'::jsonb);
|
|
347
|
+
end
|
|
348
|
+
$$;
|
|
349
|
+
-- (18)
|
|
350
|
+
select lives_ok(
|
|
351
|
+
$$ select * from agent_board.reclaim_fleet_trust_domain(
|
|
352
|
+
(select dom_revoked from _dr_fx), (select new_owner from _dr_fx),
|
|
353
|
+
(select operator from _dr_fx), 'reclaim a revoked-then-squatted domain') $$,
|
|
354
|
+
'revoked-residual: reclaiming a revoked domain succeeds'
|
|
355
|
+
);
|
|
356
|
+
-- (19)
|
|
357
|
+
select is(
|
|
358
|
+
(select owner_agent_id from agent_board.fleet_trust_domains where id = (select dom_revoked from _dr_fx)),
|
|
359
|
+
(select new_owner from _dr_fx),
|
|
360
|
+
'revoked-residual: reclaim of a revoked domain still transfers ownership'
|
|
361
|
+
);
|
|
362
|
+
-- (20) BOUNDARY AS BEHAVIOR: the owner-transfer SUCCEEDED (owner=new_owner) AND the domain is STILL
|
|
363
|
+
-- revoked — one compound assertion so the intentional residual is pinned as behavior, not inferred
|
|
364
|
+
-- across two checks. PIN (mutation H): making reclaim revive the domain (lifecycle='active' on the
|
|
365
|
+
-- revoked branch) flips this; skipping the reassign (owner stays squatter) also flips it.
|
|
366
|
+
select ok(
|
|
367
|
+
(select owner_agent_id = (select new_owner from _dr_fx) and lifecycle_state = 'revoked'
|
|
368
|
+
from agent_board.fleet_trust_domains where id = (select dom_revoked from _dr_fx)),
|
|
369
|
+
'revoked-residual: after a successful owner-transfer the pre-revoked domain STAYS revoked (intentional boundary)'
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
------------------------------------------------------------------------------------------
|
|
373
|
+
-- AUDIT APPEND (rule #2): a SECOND reclaim of dom_core APPENDS to reclaim_history.
|
|
374
|
+
------------------------------------------------------------------------------------------
|
|
375
|
+
do $$
|
|
376
|
+
begin
|
|
377
|
+
perform * from agent_board.reclaim_fleet_trust_domain(
|
|
378
|
+
(select dom_core from _dr_fx), (select victim from _dr_fx),
|
|
379
|
+
(select operator from _dr_fx), 'second arbitration on the same domain');
|
|
380
|
+
end
|
|
381
|
+
$$;
|
|
382
|
+
-- (21) MUTATION PIN: reverting the audit to overwrite (single-element array / flat keys) leaves
|
|
383
|
+
-- reclaim_history at length 1 → this flips.
|
|
384
|
+
select is(
|
|
385
|
+
(select jsonb_array_length(metadata -> 'reclaim_history') from agent_board.fleet_trust_domains where id = (select dom_core from _dr_fx)),
|
|
386
|
+
2,
|
|
387
|
+
'audit: a repeated reclaim appends to reclaim_history (prior record preserved)'
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
------------------------------------------------------------------------------------------
|
|
391
|
+
-- CLASS (rule #1 / EC-4): catalog-derived — reclaim_fleet_trust_domain is the ONLY writer of
|
|
392
|
+
-- fleet_trust_domains.owner_agent_id. A future owner-writer appears here BY NAME. WRITER = a
|
|
393
|
+
-- function whose definition INSERTs owner_agent_id into fleet_trust_domains or UPDATEs
|
|
394
|
+
-- fleet_trust_domains assigning owner_agent_id. Reads (WHERE owner_agent_id = ...) and the lifecycle
|
|
395
|
+
-- -only revoke UPDATE (no owner_agent_id =) do not match. Aggregates excluded (prokind='f').
|
|
396
|
+
------------------------------------------------------------------------------------------
|
|
397
|
+
-- (22)
|
|
398
|
+
select is(
|
|
399
|
+
(
|
|
400
|
+
select string_agg(proname, ', ' order by proname)
|
|
401
|
+
from (
|
|
402
|
+
select p.proname
|
|
403
|
+
from pg_proc p
|
|
404
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
405
|
+
where n.nspname = 'agent_board'
|
|
406
|
+
and p.prokind = 'f'
|
|
407
|
+
and (
|
|
408
|
+
pg_get_functiondef(p.oid) ~ 'insert into agent_board\.fleet_trust_domains\s*\([^)]*owner_agent_id'
|
|
409
|
+
or pg_get_functiondef(p.oid) ~ 'update agent_board\.fleet_trust_domains[^;]*owner_agent_id\s*='
|
|
410
|
+
)
|
|
411
|
+
) writers
|
|
412
|
+
),
|
|
413
|
+
'reclaim_fleet_trust_domain',
|
|
414
|
+
'class: reclaim_fleet_trust_domain is the only writer of fleet_trust_domains.owner_agent_id (a new writer names itself)'
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
select * from finish();
|
|
418
|
+
|
|
419
|
+
rollback;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
-- #141 register_fleet_machine fleet-ownership hijack.
|
|
2
|
+
--
|
|
3
|
+
-- machine_fingerprint is globally unique, so a fingerprint conflict can target ANY owner's
|
|
4
|
+
-- machine. The fix gates the `on conflict do update` with `where owner_agent_id = <resolved
|
|
5
|
+
-- actor>` and raises 42501 when the conflicting row belongs to a different owner. These
|
|
6
|
+
-- assertions pin: (a) a cross-owner re-register RAISES and leaves the victim row untouched,
|
|
7
|
+
-- (b) a same-owner re-register still updates descriptive fields, and (c) a same-owner
|
|
8
|
+
-- re-register does NOT resurrect a deliberately revoked machine.
|
|
9
|
+
--
|
|
10
|
+
-- Tests run as session_user=postgres => _is_service_role() is true => _resolved_actor_agent_id
|
|
11
|
+
-- returns the supplied p_owner_agent_id verbatim, so owner A and owner B are addressed
|
|
12
|
+
-- directly. (The authenticated-actor path -- p_owner must match current_agent_id() -- is
|
|
13
|
+
-- already covered by agent_board_fleet_policy.test.sql's impersonation block.)
|
|
14
|
+
|
|
15
|
+
create extension if not exists pgtap with schema extensions;
|
|
16
|
+
set search_path = extensions, public, agent_board;
|
|
17
|
+
|
|
18
|
+
begin;
|
|
19
|
+
|
|
20
|
+
select plan(14);
|
|
21
|
+
|
|
22
|
+
select has_function(
|
|
23
|
+
'agent_board', 'register_fleet_machine',
|
|
24
|
+
array['uuid', 'uuid', 'text', 'text', 'text', 'jsonb'],
|
|
25
|
+
'register_fleet_machine exists'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
create temp table _hijack_fx (
|
|
29
|
+
owner_a uuid,
|
|
30
|
+
owner_b uuid,
|
|
31
|
+
domain_a uuid,
|
|
32
|
+
domain_b uuid,
|
|
33
|
+
machine uuid
|
|
34
|
+
) on commit drop;
|
|
35
|
+
|
|
36
|
+
grant all on table _hijack_fx to authenticated, service_role;
|
|
37
|
+
|
|
38
|
+
do $$
|
|
39
|
+
declare
|
|
40
|
+
v_owner_a uuid := gen_random_uuid();
|
|
41
|
+
v_owner_b uuid := gen_random_uuid();
|
|
42
|
+
v_domain_a uuid;
|
|
43
|
+
v_domain_b uuid;
|
|
44
|
+
v_machine uuid;
|
|
45
|
+
begin
|
|
46
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
47
|
+
values
|
|
48
|
+
(v_owner_a, 'agent', 'fleet-hijack-owner-a', 'Fleet Hijack Owner A'),
|
|
49
|
+
(v_owner_b, 'agent', 'fleet-hijack-owner-b', 'Fleet Hijack Owner B');
|
|
50
|
+
|
|
51
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
52
|
+
values
|
|
53
|
+
('fleet-hijack-domain-a', 'Hijack Domain A', 'tailnet', v_owner_a, 'active', now()),
|
|
54
|
+
('fleet-hijack-domain-b', 'Hijack Domain B', 'external', v_owner_b, 'active', now());
|
|
55
|
+
|
|
56
|
+
select id into v_domain_a from agent_board.fleet_trust_domains where slug = 'fleet-hijack-domain-a';
|
|
57
|
+
select id into v_domain_b from agent_board.fleet_trust_domains where slug = 'fleet-hijack-domain-b';
|
|
58
|
+
|
|
59
|
+
-- Owner A registers the machine that owner B will try to steal.
|
|
60
|
+
select machine_id
|
|
61
|
+
into v_machine
|
|
62
|
+
from agent_board.register_fleet_machine(
|
|
63
|
+
v_owner_a, v_domain_a, 'owner-a-claw', 'sha256:fleet-hijack-fixture', 'portal', '{"redacted":true}'::jsonb
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
insert into _hijack_fx(owner_a, owner_b, domain_a, domain_b, machine)
|
|
67
|
+
values (v_owner_a, v_owner_b, v_domain_a, v_domain_b, v_machine);
|
|
68
|
+
end
|
|
69
|
+
$$;
|
|
70
|
+
|
|
71
|
+
-- (1) Cross-owner re-register of an existing fingerprint RAISES 42501.
|
|
72
|
+
-- Mutation pin: neutering the `where owner_agent_id = v_actor_agent_id` gate on the
|
|
73
|
+
-- do-update (e.g. `where true`) lets the cross-owner update proceed and this THROWS_OK
|
|
74
|
+
-- flips to `not ok` (the call lives instead of raising).
|
|
75
|
+
select throws_ok(
|
|
76
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
77
|
+
(select owner_b from _hijack_fx),
|
|
78
|
+
(select domain_b from _hijack_fx),
|
|
79
|
+
'owner-b-steal',
|
|
80
|
+
'sha256:fleet-hijack-fixture',
|
|
81
|
+
'portal',
|
|
82
|
+
'{"redacted":true}'::jsonb
|
|
83
|
+
) $$,
|
|
84
|
+
'42501',
|
|
85
|
+
NULL,
|
|
86
|
+
'cross-owner re-register of an existing fingerprint raises 42501'
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
-- (2)(3) The victim row is untouched by the failed hijack: owner and lifecycle unchanged.
|
|
90
|
+
-- Mutation pin: additionally restoring `owner_agent_id = excluded.owner_agent_id` to the
|
|
91
|
+
-- do-update (the original stealing upsert) flips assertion (2) -- the owner becomes owner_b.
|
|
92
|
+
select is(
|
|
93
|
+
(select owner_agent_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
94
|
+
(select owner_a from _hijack_fx),
|
|
95
|
+
'failed hijack leaves owner_agent_id unchanged (still owner A)'
|
|
96
|
+
);
|
|
97
|
+
select is(
|
|
98
|
+
(select lifecycle_state from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
99
|
+
'active',
|
|
100
|
+
'failed hijack leaves lifecycle_state unchanged (still active)'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
-- (4)(5) No cross-owner WRITE at all: the descriptive fields the attacker supplied
|
|
104
|
+
-- (label, trust domain) are also untouched. My do-update writes machine_label and
|
|
105
|
+
-- trust_domain_id unconditionally once the owner-gate opens, so neutering that gate
|
|
106
|
+
-- (mutation 1) flips these directly -- pinning the no-cross-owner-write property, not
|
|
107
|
+
-- just the raise.
|
|
108
|
+
select is(
|
|
109
|
+
(select machine_label from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
110
|
+
'owner-a-claw',
|
|
111
|
+
'failed hijack leaves machine_label unchanged (attacker label not written)'
|
|
112
|
+
);
|
|
113
|
+
select is(
|
|
114
|
+
(select trust_domain_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
115
|
+
(select domain_a from _hijack_fx),
|
|
116
|
+
'failed hijack leaves trust_domain_id unchanged (attacker domain not written)'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
-- (6) Same-owner re-register still succeeds and updates descriptive fields.
|
|
120
|
+
select lives_ok(
|
|
121
|
+
$$ create temp table _hijack_rereg as
|
|
122
|
+
select * from agent_board.register_fleet_machine(
|
|
123
|
+
(select owner_a from _hijack_fx),
|
|
124
|
+
(select domain_a from _hijack_fx),
|
|
125
|
+
'owner-a-claw-renamed',
|
|
126
|
+
'sha256:fleet-hijack-fixture',
|
|
127
|
+
'portal',
|
|
128
|
+
'{"redacted":true}'::jsonb
|
|
129
|
+
) $$,
|
|
130
|
+
'same-owner re-register succeeds'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
-- (7)(8)(9) It is the SAME row, the label updated, and the owner is still owner A.
|
|
134
|
+
select is(
|
|
135
|
+
(select machine_id from _hijack_rereg),
|
|
136
|
+
(select machine from _hijack_fx),
|
|
137
|
+
'same-owner re-register updates the existing row (machine_id unchanged)'
|
|
138
|
+
);
|
|
139
|
+
select is(
|
|
140
|
+
(select machine_label from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
141
|
+
'owner-a-claw-renamed',
|
|
142
|
+
'same-owner re-register updates the machine label'
|
|
143
|
+
);
|
|
144
|
+
select is(
|
|
145
|
+
(select owner_agent_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
146
|
+
(select owner_a from _hijack_fx),
|
|
147
|
+
'same-owner re-register keeps owner_agent_id = owner A'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
-- (10) Owner A revokes their own machine.
|
|
151
|
+
select lives_ok(
|
|
152
|
+
$$ select * from agent_board.revoke_fleet_subject(
|
|
153
|
+
'machine',
|
|
154
|
+
(select machine from _hijack_fx),
|
|
155
|
+
(select owner_a from _hijack_fx),
|
|
156
|
+
'test revocation',
|
|
157
|
+
'{"redacted":true}'::jsonb
|
|
158
|
+
) $$,
|
|
159
|
+
'owner A revokes their machine'
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
-- (11) A same-owner re-register of the now-revoked machine still succeeds ...
|
|
163
|
+
select lives_ok(
|
|
164
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
165
|
+
(select owner_a from _hijack_fx),
|
|
166
|
+
(select domain_a from _hijack_fx),
|
|
167
|
+
'owner-a-claw-reregistered',
|
|
168
|
+
'sha256:fleet-hijack-fixture',
|
|
169
|
+
'portal',
|
|
170
|
+
'{"redacted":true}'::jsonb
|
|
171
|
+
) $$,
|
|
172
|
+
'same-owner re-register of a revoked machine succeeds'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
-- (13)(14) ... but does NOT resurrect it: lifecycle_state and revoked_at are preserved.
|
|
176
|
+
-- Mutation pin: restoring `lifecycle_state = 'active', revoked_at = null` to the do-update
|
|
177
|
+
-- (the original un-revoke) flips both assertions.
|
|
178
|
+
select is(
|
|
179
|
+
(select lifecycle_state from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
180
|
+
'revoked',
|
|
181
|
+
're-register does not resurrect a revoked machine (lifecycle_state stays revoked)'
|
|
182
|
+
);
|
|
183
|
+
select ok(
|
|
184
|
+
(select revoked_at is not null from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
185
|
+
're-register does not clear revoked_at of a revoked machine'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
select * from finish();
|
|
189
|
+
|
|
190
|
+
rollback;
|