@consiliency/agent-board-schema 1.12.0 → 1.23.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 +160 -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/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +363 -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_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -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_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +51 -5
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- 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 +45 -31
- 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_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
- 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 +27 -11
- 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/agent_board_wake_recovery.test.sql +756 -0
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -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/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -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/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -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
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
-- #146 operator squatting-reclaim — reassign a squatted machine + revoke squatter access.
|
|
2
|
+
--
|
|
3
|
+
-- reclaim_fleet_machine (service_role only) is the arbitration escape hatch for a machine whose
|
|
4
|
+
-- globally-unique fingerprint was pre-registered by a SQUATTER (someone other than the rightful
|
|
5
|
+
-- owner), which #141 otherwise locks permanently. It (1) reassigns owner_agent_id to the
|
|
6
|
+
-- arbitrated new owner, (2) revokes every ACTIVE fleet_machine_agents binding on THIS machine so
|
|
7
|
+
-- the squatter loses access (machine-scoped, NOT a global revoke_fleet_subject('agent')), (3)
|
|
8
|
+
-- leaves the machine active for the new owner (stamps reactivated_at), and (4, #150 panel MUST-FIX)
|
|
9
|
+
-- optionally MOVES the machine into a trust domain owned by the new owner to close the squatter's
|
|
10
|
+
-- trust-domain kill switch.
|
|
11
|
+
--
|
|
12
|
+
-- These assertions pin:
|
|
13
|
+
-- * reclaim reassigns owner_agent_id (state) — PIN: skip the owner reassign ⇒ (3) fails and the
|
|
14
|
+
-- squatter's ownership-based access (10) stays true.
|
|
15
|
+
-- * reclaim revokes the squatter's binding (state) — PIN: skip the binding revoke ⇒ (4)/(5) fail
|
|
16
|
+
-- and the worker's binding-based access (12) stays true.
|
|
17
|
+
-- * MACHINE-SCOPING of the binding revoke — a worker bound on a DIFFERENT machine SURVIVES (8) —
|
|
18
|
+
-- PIN: drop the `machine_id = p_machine_id` predicate ⇒ (8) fails.
|
|
19
|
+
-- * ACCESS BRACKET (rule #2/#3): each "loses access" probe is bracketed by a BEFORE=true probe
|
|
20
|
+
-- under the same authenticated principal, proving current_agent_id() resolves — so AFTER=false
|
|
21
|
+
-- is a genuine revocation, not a null-actor artifact. Owner probe (A) isolates the ownership
|
|
22
|
+
-- lever (owns m1, no binding); worker probe (W) isolates the binding lever (bound, owns nothing).
|
|
23
|
+
-- * the new owner CAN claim the reclaimed machine, and fresh bindings work post-reclaim.
|
|
24
|
+
-- * a squatted-THEN-revoked machine comes back active for the new owner (v_fleet_machine_status).
|
|
25
|
+
-- * KILL SWITCH (#150 MUST-FIX): with p_new_trust_domain_id the machine MOVES (19) into the new
|
|
26
|
+
-- owner's domain, and the squatter revoking the OLD domain can no longer deny it (20) — PIN:
|
|
27
|
+
-- skip the domain move ⇒ (19) and (20) fail. Domain validation: a domain not owned by the new
|
|
28
|
+
-- owner is rejected (24) — PIN: skip the validation ⇒ (24) fails.
|
|
29
|
+
-- * reclaim is operator-only: an authenticated caller (the victim) is refused 42501 (21) — PIN:
|
|
30
|
+
-- remove the _is_service_role() body guard ⇒ (21) fails (body guard isolated from the ACL by a
|
|
31
|
+
-- temp grant to authenticated).
|
|
32
|
+
-- * reclaim validates the new owner and the provenance agent exist (22, 23).
|
|
33
|
+
-- * CLASS (rule #1 / EC-4): register_fleet_machine + reclaim_fleet_machine are the ONLY writers of
|
|
34
|
+
-- fleet_machines.owner_agent_id (25) — catalog-derived, so a future owner-writer names itself.
|
|
35
|
+
--
|
|
36
|
+
-- Tests run as session_user=postgres ⇒ _is_service_role() is true; probes and the authz assertion
|
|
37
|
+
-- flip to role=authenticated (jwt claims) to exercise the principal / body guard.
|
|
38
|
+
|
|
39
|
+
create extension if not exists pgtap with schema extensions;
|
|
40
|
+
set search_path = extensions, public, agent_board;
|
|
41
|
+
|
|
42
|
+
begin;
|
|
43
|
+
|
|
44
|
+
select plan(29);
|
|
45
|
+
|
|
46
|
+
-- (1) Shape.
|
|
47
|
+
select has_function(
|
|
48
|
+
'agent_board', 'reclaim_fleet_machine',
|
|
49
|
+
array['uuid', 'uuid', 'uuid', 'text', 'uuid'],
|
|
50
|
+
'reclaim_fleet_machine exists'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
create temp table _reclaim_fx (
|
|
54
|
+
operator uuid, -- arbitration actor (p_reclaimed_by); no auth mapping needed
|
|
55
|
+
squatter uuid, -- pre-registers the fingerprints; owner of m1..m5 and of the OLD domain
|
|
56
|
+
worker uuid, -- squatter's sub-agent, BOUND to m1 (access purely via binding)
|
|
57
|
+
worker2 uuid, -- bound on m2; used to pin machine-scoping (its binding must survive)
|
|
58
|
+
new_owner uuid, -- arbitrated rightful owner; owns the NEW domain
|
|
59
|
+
victim uuid, -- rightful human/agent locked out; used for the authz probe
|
|
60
|
+
m1 uuid, m2 uuid, m3 uuid, m4 uuid, m5 uuid, m6 uuid, m7 uuid,
|
|
61
|
+
fp1 text, fp5 text, fp6 text, fp7 text,
|
|
62
|
+
dom_old uuid, -- squatter-owned domain (the kill switch)
|
|
63
|
+
dom_new uuid, -- new-owner-owned domain (kill switch closed by moving into it)
|
|
64
|
+
squatter_user uuid,
|
|
65
|
+
worker_user uuid,
|
|
66
|
+
new_owner_user uuid,
|
|
67
|
+
victim_user uuid
|
|
68
|
+
) on commit drop;
|
|
69
|
+
|
|
70
|
+
grant all on table _reclaim_fx to authenticated, service_role;
|
|
71
|
+
|
|
72
|
+
create temp table _reclaim_probe (label text, allowed boolean) on commit drop;
|
|
73
|
+
grant all on table _reclaim_probe to authenticated, service_role;
|
|
74
|
+
|
|
75
|
+
do $$
|
|
76
|
+
declare
|
|
77
|
+
v_operator uuid := gen_random_uuid();
|
|
78
|
+
v_squatter uuid := gen_random_uuid();
|
|
79
|
+
v_worker uuid := gen_random_uuid();
|
|
80
|
+
v_worker2 uuid := gen_random_uuid();
|
|
81
|
+
v_new_owner uuid := gen_random_uuid();
|
|
82
|
+
v_victim uuid := gen_random_uuid();
|
|
83
|
+
v_dom_old uuid;
|
|
84
|
+
v_dom_new uuid;
|
|
85
|
+
v_m1 uuid; v_m2 uuid; v_m3 uuid; v_m4 uuid; v_m5 uuid; v_m6 uuid; v_m7 uuid;
|
|
86
|
+
begin
|
|
87
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
88
|
+
values
|
|
89
|
+
(v_operator, 'agent', 'reclaim-operator', 'Reclaim Operator'),
|
|
90
|
+
(v_squatter, 'agent', 'reclaim-squatter', 'Reclaim Squatter'),
|
|
91
|
+
(v_worker, 'agent', 'reclaim-worker', 'Reclaim Worker'),
|
|
92
|
+
(v_worker2, 'agent', 'reclaim-worker2', 'Reclaim Worker 2'),
|
|
93
|
+
(v_new_owner, 'agent', 'reclaim-new-owner', 'Reclaim New Owner'),
|
|
94
|
+
(v_victim, 'agent', 'reclaim-victim', 'Reclaim Victim');
|
|
95
|
+
|
|
96
|
+
-- The squatter owns the OLD domain and registers the fingerprints under it. The new owner owns a
|
|
97
|
+
-- separate NEW domain that the machine can be moved into to close the kill switch.
|
|
98
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
99
|
+
values
|
|
100
|
+
('reclaim-domain-old', 'Reclaim Domain Old', 'tailnet', v_squatter, 'active', now()),
|
|
101
|
+
('reclaim-domain-new', 'Reclaim Domain New', 'tailnet', v_new_owner, 'active', now());
|
|
102
|
+
select id into v_dom_old from agent_board.fleet_trust_domains where slug = 'reclaim-domain-old';
|
|
103
|
+
select id into v_dom_new from agent_board.fleet_trust_domains where slug = 'reclaim-domain-new';
|
|
104
|
+
|
|
105
|
+
select machine_id into v_m1 from agent_board.register_fleet_machine(v_squatter, v_dom_old, 'reclaim-m1', 'sha256:reclaim-m1', 'portal', '{"redacted":true}'::jsonb);
|
|
106
|
+
select machine_id into v_m2 from agent_board.register_fleet_machine(v_squatter, v_dom_old, 'reclaim-m2', 'sha256:reclaim-m2', 'portal', '{"redacted":true}'::jsonb);
|
|
107
|
+
select machine_id into v_m3 from agent_board.register_fleet_machine(v_squatter, v_dom_old, 'reclaim-m3', 'sha256:reclaim-m3', 'portal', '{"redacted":true}'::jsonb);
|
|
108
|
+
select machine_id into v_m4 from agent_board.register_fleet_machine(v_squatter, v_dom_old, 'reclaim-m4', 'sha256:reclaim-m4', 'portal', '{"redacted":true}'::jsonb);
|
|
109
|
+
select machine_id into v_m5 from agent_board.register_fleet_machine(v_squatter, v_dom_old, 'reclaim-m5', 'sha256:reclaim-m5', 'portal', '{"redacted":true}'::jsonb);
|
|
110
|
+
select machine_id into v_m6 from agent_board.register_fleet_machine(v_squatter, v_dom_old, 'reclaim-m6', 'sha256:reclaim-m6', 'portal', '{"redacted":true}'::jsonb);
|
|
111
|
+
select machine_id into v_m7 from agent_board.register_fleet_machine(v_squatter, v_dom_old, 'reclaim-m7', 'sha256:reclaim-m7', 'portal', '{"redacted":true}'::jsonb);
|
|
112
|
+
|
|
113
|
+
-- Worker bound to m1 (access purely via binding); worker2 bound to a DIFFERENT machine m2 (its
|
|
114
|
+
-- binding must SURVIVE an m1 reclaim — pins the machine_id scoping). The squatter's own access to
|
|
115
|
+
-- m1 is purely via ownership — this separates the ownership and binding revocation levers. The new
|
|
116
|
+
-- owner is bound to m7 (an unusual but valid case: the rightful owner was a worker on the squatted
|
|
117
|
+
-- machine) — reclaiming m7 to the new owner must NOT revoke the new owner's own binding.
|
|
118
|
+
insert into agent_board.fleet_machine_agents(machine_id, agent_id, agent_role)
|
|
119
|
+
values
|
|
120
|
+
(v_m1, v_worker, 'phase_worker'),
|
|
121
|
+
(v_m2, v_worker2, 'phase_worker'),
|
|
122
|
+
(v_m7, v_new_owner, 'phase_worker');
|
|
123
|
+
|
|
124
|
+
insert into _reclaim_fx(operator, squatter, worker, worker2, new_owner, victim, m1, m2, m3, m4, m5, m6, m7, fp1, fp5, fp6, fp7, dom_old, dom_new)
|
|
125
|
+
values (v_operator, v_squatter, v_worker, v_worker2, v_new_owner, v_victim, v_m1, v_m2, v_m3, v_m4, v_m5, v_m6, v_m7,
|
|
126
|
+
'sha256:reclaim-m1', 'sha256:reclaim-m5', 'sha256:reclaim-m6', 'sha256:reclaim-m7', v_dom_old, v_dom_new);
|
|
127
|
+
end
|
|
128
|
+
$$;
|
|
129
|
+
|
|
130
|
+
-- Map the squatter, worker, new owner and victim agents to auth users so the authenticated
|
|
131
|
+
-- probes/authz call resolve a principal via current_agent_id() → auth.uid().
|
|
132
|
+
do $$
|
|
133
|
+
declare
|
|
134
|
+
v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
135
|
+
v_squatter_user uuid;
|
|
136
|
+
v_worker_user uuid;
|
|
137
|
+
v_new_owner_user uuid;
|
|
138
|
+
v_victim_user uuid;
|
|
139
|
+
begin
|
|
140
|
+
v_squatter_user := public.ensure_github_user('reclaim-squatter-' || v_sfx, 'reclaim-squatter-' || v_sfx || '@example.test', 'reclaim-squatter-' || v_sfx, 'Reclaim Squatter ' || v_sfx);
|
|
141
|
+
v_worker_user := public.ensure_github_user('reclaim-worker-' || v_sfx, 'reclaim-worker-' || v_sfx || '@example.test', 'reclaim-worker-' || v_sfx, 'Reclaim Worker ' || v_sfx);
|
|
142
|
+
v_new_owner_user := public.ensure_github_user('reclaim-newowner-' || v_sfx, 'reclaim-newowner-' || v_sfx || '@example.test', 'reclaim-newowner-' || v_sfx, 'Reclaim New Owner ' || v_sfx);
|
|
143
|
+
v_victim_user := public.ensure_github_user('reclaim-victim-' || v_sfx, 'reclaim-victim-' || v_sfx || '@example.test', 'reclaim-victim-' || v_sfx, 'Reclaim Victim ' || v_sfx);
|
|
144
|
+
|
|
145
|
+
update agent_board.agents set auth_user_id = v_squatter_user where id = (select squatter from _reclaim_fx);
|
|
146
|
+
update agent_board.agents set auth_user_id = v_worker_user where id = (select worker from _reclaim_fx);
|
|
147
|
+
update agent_board.agents set auth_user_id = v_new_owner_user where id = (select new_owner from _reclaim_fx);
|
|
148
|
+
update agent_board.agents set auth_user_id = v_victim_user where id = (select victim from _reclaim_fx);
|
|
149
|
+
|
|
150
|
+
update _reclaim_fx set squatter_user = v_squatter_user, worker_user = v_worker_user,
|
|
151
|
+
new_owner_user = v_new_owner_user, victim_user = v_victim_user;
|
|
152
|
+
end
|
|
153
|
+
$$;
|
|
154
|
+
|
|
155
|
+
------------------------------------------------------------------------------------------
|
|
156
|
+
-- BEFORE probes (rule #2/#3 bracket): under each authenticated principal, capture that they CAN
|
|
157
|
+
-- claim m1 today. A true here proves the principal resolves — so AFTER=false below is real.
|
|
158
|
+
------------------------------------------------------------------------------------------
|
|
159
|
+
set local role authenticated;
|
|
160
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
161
|
+
select set_config('request.jwt.claim.sub', (select squatter_user::text from _reclaim_fx), true);
|
|
162
|
+
do $$ begin
|
|
163
|
+
insert into _reclaim_probe values ('squatter_before', agent_board._can_claim_resource('machine', (select fp1 from _reclaim_fx)));
|
|
164
|
+
end $$;
|
|
165
|
+
select set_config('request.jwt.claim.sub', (select worker_user::text from _reclaim_fx), true);
|
|
166
|
+
do $$ begin
|
|
167
|
+
insert into _reclaim_probe values ('worker_before', agent_board._can_claim_resource('machine', (select fp1 from _reclaim_fx)));
|
|
168
|
+
end $$;
|
|
169
|
+
reset role;
|
|
170
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
171
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
172
|
+
|
|
173
|
+
------------------------------------------------------------------------------------------
|
|
174
|
+
-- CORE: operator reclaims m1 from the squatter to the new owner (no domain move — the m1 case
|
|
175
|
+
-- exercises the ownership/binding levers).
|
|
176
|
+
------------------------------------------------------------------------------------------
|
|
177
|
+
-- (2) Reclaim succeeds (allow-side lives_ok + separate state reads, #113).
|
|
178
|
+
select lives_ok(
|
|
179
|
+
$$ create temp table _reclaim_m1 as
|
|
180
|
+
select * from agent_board.reclaim_fleet_machine(
|
|
181
|
+
(select m1 from _reclaim_fx), (select new_owner from _reclaim_fx),
|
|
182
|
+
(select operator from _reclaim_fx), 'operator arbitrated: fingerprint belongs to new owner') $$,
|
|
183
|
+
'core: service_role reclaim of m1 succeeds'
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
-- (3) Owner is reassigned. MUTATION PIN: skipping the owner reassignment leaves owner=squatter.
|
|
187
|
+
select is(
|
|
188
|
+
(select owner_agent_id from agent_board.fleet_machines where id = (select m1 from _reclaim_fx)),
|
|
189
|
+
(select new_owner from _reclaim_fx),
|
|
190
|
+
'core: reclaim reassigns owner_agent_id to the new owner'
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
-- (4) The squatter's (worker's) binding on m1 is revoked. MUTATION PIN: skipping the binding revoke
|
|
194
|
+
-- leaves it 'active'.
|
|
195
|
+
select is(
|
|
196
|
+
(select lifecycle_state from agent_board.fleet_machine_agents
|
|
197
|
+
where machine_id = (select m1 from _reclaim_fx) and agent_id = (select worker from _reclaim_fx)),
|
|
198
|
+
'revoked',
|
|
199
|
+
'core: reclaim revokes the squatter''s binding on the machine'
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
-- (5) The returned revoked-binding count reflects the one active binding removed.
|
|
203
|
+
select is((select revoked_binding_count from _reclaim_m1), 1,
|
|
204
|
+
'core: reclaim reports revoked_binding_count = 1');
|
|
205
|
+
|
|
206
|
+
-- (6) Audit APPENDED to metadata.reclaim_history: last record has operator + prior owner (#150
|
|
207
|
+
-- SHOULD-FIX: append, not overwrite).
|
|
208
|
+
select ok(
|
|
209
|
+
(select (metadata -> 'reclaim_history' -> -1 ->> 'reclaimed_by') = (select operator::text from _reclaim_fx)
|
|
210
|
+
and (metadata -> 'reclaim_history' -> -1 ->> 'prior_owner') = (select squatter::text from _reclaim_fx)
|
|
211
|
+
from agent_board.fleet_machines where id = (select m1 from _reclaim_fx)),
|
|
212
|
+
'core: reclaim audit (reclaimed_by + prior owner) appended to metadata.reclaim_history'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
-- (7) The machine is left active for the new owner, with the supersession marker stamped.
|
|
216
|
+
select ok(
|
|
217
|
+
(select lifecycle_state = 'active' and revoked_at is null and reactivated_at is not null
|
|
218
|
+
from agent_board.fleet_machines where id = (select m1 from _reclaim_fx)),
|
|
219
|
+
'core: reclaim leaves the machine active and stamps reactivated_at'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
-- (8) MACHINE-SCOPING: worker2's binding on the OTHER machine m2 SURVIVES the m1 reclaim.
|
|
223
|
+
-- MUTATION PIN: dropping the `machine_id = p_machine_id` predicate revokes worker2 too → this flips.
|
|
224
|
+
select is(
|
|
225
|
+
(select lifecycle_state from agent_board.fleet_machine_agents
|
|
226
|
+
where machine_id = (select m2 from _reclaim_fx) and agent_id = (select worker2 from _reclaim_fx)),
|
|
227
|
+
'active',
|
|
228
|
+
'scoping: a binding on a different machine survives the reclaim'
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
------------------------------------------------------------------------------------------
|
|
232
|
+
-- AFTER probes: claim authority on m1 post-reclaim.
|
|
233
|
+
------------------------------------------------------------------------------------------
|
|
234
|
+
set local role authenticated;
|
|
235
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
236
|
+
select set_config('request.jwt.claim.sub', (select squatter_user::text from _reclaim_fx), true);
|
|
237
|
+
do $$ begin
|
|
238
|
+
insert into _reclaim_probe values ('squatter_after', agent_board._can_claim_resource('machine', (select fp1 from _reclaim_fx)));
|
|
239
|
+
end $$;
|
|
240
|
+
select set_config('request.jwt.claim.sub', (select worker_user::text from _reclaim_fx), true);
|
|
241
|
+
do $$ begin
|
|
242
|
+
insert into _reclaim_probe values ('worker_after', agent_board._can_claim_resource('machine', (select fp1 from _reclaim_fx)));
|
|
243
|
+
end $$;
|
|
244
|
+
select set_config('request.jwt.claim.sub', (select new_owner_user::text from _reclaim_fx), true);
|
|
245
|
+
do $$ begin
|
|
246
|
+
insert into _reclaim_probe values ('new_owner_after', agent_board._can_claim_resource('machine', (select fp1 from _reclaim_fx)));
|
|
247
|
+
end $$;
|
|
248
|
+
reset role;
|
|
249
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
250
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
251
|
+
|
|
252
|
+
-- (9)(10) Owner lever: could claim before (bracket) → cannot after (PIN skip owner reassign).
|
|
253
|
+
select is((select allowed from _reclaim_probe where label = 'squatter_before'), true,
|
|
254
|
+
'bracket: the squatter (owner) could claim m1 before reclaim');
|
|
255
|
+
select is((select allowed from _reclaim_probe where label = 'squatter_after'), false,
|
|
256
|
+
'squatter: loses ownership-based claim authority after reclaim');
|
|
257
|
+
-- (11)(12) Binding lever: could claim before (bracket) → cannot after (PIN skip binding revoke).
|
|
258
|
+
select is((select allowed from _reclaim_probe where label = 'worker_before'), true,
|
|
259
|
+
'bracket: the squatter''s worker could claim m1 before reclaim (via binding)');
|
|
260
|
+
select is((select allowed from _reclaim_probe where label = 'worker_after'), false,
|
|
261
|
+
'worker: loses binding-based claim authority after reclaim');
|
|
262
|
+
-- (13) The new owner CAN claim the reclaimed machine.
|
|
263
|
+
select is((select allowed from _reclaim_probe where label = 'new_owner_after'), true,
|
|
264
|
+
'new owner: gains claim authority over the reclaimed machine');
|
|
265
|
+
|
|
266
|
+
------------------------------------------------------------------------------------------
|
|
267
|
+
-- FRESH BINDING: the new owner can bind agents on the reclaimed machine.
|
|
268
|
+
------------------------------------------------------------------------------------------
|
|
269
|
+
-- (14)
|
|
270
|
+
select lives_ok(
|
|
271
|
+
$$ insert into agent_board.fleet_machine_agents(machine_id, agent_id, agent_role)
|
|
272
|
+
values ((select m1 from _reclaim_fx), (select new_owner from _reclaim_fx), 'phase_worker') $$,
|
|
273
|
+
'fresh binding: a new binding for the new owner on the reclaimed machine succeeds'
|
|
274
|
+
);
|
|
275
|
+
-- (15)
|
|
276
|
+
select is(
|
|
277
|
+
(select lifecycle_state from agent_board.fleet_machine_agents
|
|
278
|
+
where machine_id = (select m1 from _reclaim_fx) and agent_id = (select new_owner from _reclaim_fx)),
|
|
279
|
+
'active',
|
|
280
|
+
'fresh binding: the new owner''s binding is active'
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
------------------------------------------------------------------------------------------
|
|
284
|
+
-- REVOKED-THEN-RECLAIM (m4): a squatted machine that was revoked comes back active for the new
|
|
285
|
+
-- owner; v_fleet_machine_status agrees.
|
|
286
|
+
------------------------------------------------------------------------------------------
|
|
287
|
+
do $$
|
|
288
|
+
begin
|
|
289
|
+
perform agent_board.revoke_fleet_subject('machine', (select m4 from _reclaim_fx), (select squatter from _reclaim_fx), 'squatter revoke before reclaim', '{"redacted":true}'::jsonb);
|
|
290
|
+
end
|
|
291
|
+
$$;
|
|
292
|
+
-- (16)
|
|
293
|
+
select lives_ok(
|
|
294
|
+
$$ select * from agent_board.reclaim_fleet_machine(
|
|
295
|
+
(select m4 from _reclaim_fx), (select new_owner from _reclaim_fx),
|
|
296
|
+
(select operator from _reclaim_fx), 'reclaim a revoked-then-squatted machine') $$,
|
|
297
|
+
'revoked-reclaim: reclaiming a revoked machine succeeds'
|
|
298
|
+
);
|
|
299
|
+
-- (17)
|
|
300
|
+
select is(
|
|
301
|
+
(select machine_revoked from agent_board.v_fleet_machine_status where machine_id = (select m4 from _reclaim_fx)),
|
|
302
|
+
false,
|
|
303
|
+
'revoked-reclaim: v_fleet_machine_status reports machine_revoked=false after reclaim'
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
------------------------------------------------------------------------------------------
|
|
307
|
+
-- KILL SWITCH (#150 MUST-FIX, m5): reclaim WITH p_new_trust_domain_id moves the machine into the
|
|
308
|
+
-- new owner's domain; the squatter revoking the OLD domain can no longer deny it.
|
|
309
|
+
------------------------------------------------------------------------------------------
|
|
310
|
+
-- (18) Reclaim m5 moving it into the new owner's domain.
|
|
311
|
+
select lives_ok(
|
|
312
|
+
$$ select * from agent_board.reclaim_fleet_machine(
|
|
313
|
+
(select m5 from _reclaim_fx), (select new_owner from _reclaim_fx),
|
|
314
|
+
(select operator from _reclaim_fx), 'reclaim + move into new owner domain',
|
|
315
|
+
(select dom_new from _reclaim_fx)) $$,
|
|
316
|
+
'kill-switch: reclaim with a destination domain succeeds'
|
|
317
|
+
);
|
|
318
|
+
-- (19) The machine now sits in the new owner's domain. MUTATION PIN: skipping the domain move
|
|
319
|
+
-- leaves it in the squatter''s old domain → this flips.
|
|
320
|
+
select is(
|
|
321
|
+
(select trust_domain_id from agent_board.fleet_machines where id = (select m5 from _reclaim_fx)),
|
|
322
|
+
(select dom_new from _reclaim_fx),
|
|
323
|
+
'kill-switch: reclaim moves the machine into the destination trust domain'
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
-- The squatter (still owner of the OLD domain) revokes it via the authenticated domain-owner path.
|
|
327
|
+
set local role authenticated;
|
|
328
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
329
|
+
select set_config('request.jwt.claim.sub', (select squatter_user::text from _reclaim_fx), true);
|
|
330
|
+
do $$ begin
|
|
331
|
+
perform agent_board.revoke_fleet_subject('trust_domain', (select dom_old from _reclaim_fx), (select squatter from _reclaim_fx), 'squatter tries to kill the reclaimed machine', '{"redacted":true}'::jsonb);
|
|
332
|
+
end $$;
|
|
333
|
+
-- New owner probes claim authority on the moved machine (still in the live NEW domain).
|
|
334
|
+
select set_config('request.jwt.claim.sub', (select new_owner_user::text from _reclaim_fx), true);
|
|
335
|
+
do $$ begin
|
|
336
|
+
insert into _reclaim_probe values ('killswitch_new_owner', agent_board._can_claim_resource('machine', (select fp5 from _reclaim_fx)));
|
|
337
|
+
end $$;
|
|
338
|
+
reset role;
|
|
339
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
340
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
341
|
+
|
|
342
|
+
-- (20) The squatter's OLD-domain revocation cannot reach the moved machine. MUTATION PIN: skipping
|
|
343
|
+
-- the domain move leaves m5 in the old domain → the revocation denies → this flips to false.
|
|
344
|
+
select is((select allowed from _reclaim_probe where label = 'killswitch_new_owner'), true,
|
|
345
|
+
'kill-switch: the squatter revoking the old domain cannot deny the moved machine');
|
|
346
|
+
|
|
347
|
+
------------------------------------------------------------------------------------------
|
|
348
|
+
-- AUTHZ (m2): operator-only. An authenticated caller (the victim) is refused. The BODY guard is
|
|
349
|
+
-- isolated from the service_role-only grant by a temp grant to authenticated.
|
|
350
|
+
------------------------------------------------------------------------------------------
|
|
351
|
+
create temp table _reclaim_authz (denied boolean) on commit drop;
|
|
352
|
+
grant all on table _reclaim_authz to authenticated, service_role;
|
|
353
|
+
|
|
354
|
+
grant execute on function agent_board.reclaim_fleet_machine(uuid, uuid, uuid, text, uuid) to authenticated;
|
|
355
|
+
|
|
356
|
+
set local role authenticated;
|
|
357
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
358
|
+
select set_config('request.jwt.claim.sub', (select victim_user::text from _reclaim_fx), true);
|
|
359
|
+
do $$
|
|
360
|
+
begin
|
|
361
|
+
begin
|
|
362
|
+
perform * from agent_board.reclaim_fleet_machine(
|
|
363
|
+
(select m2 from _reclaim_fx), (select victim from _reclaim_fx),
|
|
364
|
+
(select victim from _reclaim_fx), 'victim self-reclaim attempt');
|
|
365
|
+
insert into _reclaim_authz values (false); -- reached completion → NOT denied
|
|
366
|
+
exception
|
|
367
|
+
when sqlstate '42501' then insert into _reclaim_authz values (true);
|
|
368
|
+
end;
|
|
369
|
+
end
|
|
370
|
+
$$;
|
|
371
|
+
reset role;
|
|
372
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
373
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
374
|
+
revoke execute on function agent_board.reclaim_fleet_machine(uuid, uuid, uuid, text, uuid) from authenticated;
|
|
375
|
+
|
|
376
|
+
-- (21) MUTATION PIN: removing the _is_service_role() body guard lets the authenticated call
|
|
377
|
+
-- complete → denied=false → this flips.
|
|
378
|
+
select ok(
|
|
379
|
+
(select denied from _reclaim_authz),
|
|
380
|
+
'authz: authenticated caller (the victim) cannot reclaim (42501) — operator-only'
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
------------------------------------------------------------------------------------------
|
|
384
|
+
-- VALIDATION.
|
|
385
|
+
------------------------------------------------------------------------------------------
|
|
386
|
+
-- (22) unknown new owner
|
|
387
|
+
select throws_ok(
|
|
388
|
+
format($$ select * from agent_board.reclaim_fleet_machine(%L::uuid, %L::uuid, %L::uuid, 'unknown new owner') $$,
|
|
389
|
+
(select m3 from _reclaim_fx), gen_random_uuid(), (select operator from _reclaim_fx)),
|
|
390
|
+
'22023', NULL, 'validate: reclaim to an unknown new owner raises'
|
|
391
|
+
);
|
|
392
|
+
-- (23) unknown provenance
|
|
393
|
+
select throws_ok(
|
|
394
|
+
format($$ select * from agent_board.reclaim_fleet_machine(%L::uuid, %L::uuid, %L::uuid, 'unknown provenance') $$,
|
|
395
|
+
(select m3 from _reclaim_fx), (select new_owner from _reclaim_fx), gen_random_uuid()),
|
|
396
|
+
'22023', NULL, 'validate: reclaim with an unknown reclaimed_by agent raises'
|
|
397
|
+
);
|
|
398
|
+
-- (24) destination domain not owned by the new owner (here dom_old is owned by the squatter).
|
|
399
|
+
-- MUTATION PIN: skipping the p_new_trust_domain_id validation lets this through → no raise → flips.
|
|
400
|
+
select throws_ok(
|
|
401
|
+
format($$ select * from agent_board.reclaim_fleet_machine(%L::uuid, %L::uuid, %L::uuid, 'domain not owned by new owner', %L::uuid) $$,
|
|
402
|
+
(select m3 from _reclaim_fx), (select new_owner from _reclaim_fx), (select operator from _reclaim_fx), (select dom_old from _reclaim_fx)),
|
|
403
|
+
'42501', NULL, 'validate: a destination domain not owned by the new owner is rejected'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
------------------------------------------------------------------------------------------
|
|
407
|
+
-- CLASS (rule #1 / EC-4): catalog-derived — register_fleet_machine + reclaim_fleet_machine are the
|
|
408
|
+
-- ONLY writers of fleet_machines.owner_agent_id. A future owner-writer appears here BY NAME.
|
|
409
|
+
-- WRITER = a function whose definition either INSERTs owner_agent_id into fleet_machines (register)
|
|
410
|
+
-- or UPDATEs fleet_machines assigning owner_agent_id (reclaim). Reads (WHERE owner_agent_id = ...)
|
|
411
|
+
-- do not match. Aggregates excluded (pg_get_functiondef errors; prokind='f').
|
|
412
|
+
------------------------------------------------------------------------------------------
|
|
413
|
+
-- (25)
|
|
414
|
+
select is(
|
|
415
|
+
(
|
|
416
|
+
select string_agg(proname, ', ' order by proname)
|
|
417
|
+
from (
|
|
418
|
+
select p.proname
|
|
419
|
+
from pg_proc p
|
|
420
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
421
|
+
where n.nspname = 'agent_board'
|
|
422
|
+
and p.prokind = 'f'
|
|
423
|
+
and (
|
|
424
|
+
pg_get_functiondef(p.oid) ~ 'insert into agent_board\.fleet_machines\s*\([^)]*owner_agent_id'
|
|
425
|
+
or pg_get_functiondef(p.oid) ~ 'update agent_board\.fleet_machines[^;]*owner_agent_id\s*='
|
|
426
|
+
)
|
|
427
|
+
) writers
|
|
428
|
+
),
|
|
429
|
+
'reclaim_fleet_machine, register_fleet_machine',
|
|
430
|
+
'class: register + reclaim are the only writers of fleet_machines.owner_agent_id (a new writer names itself)'
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
------------------------------------------------------------------------------------------
|
|
434
|
+
-- AUDIT APPEND (#150 SHOULD-FIX, rule #2): a SECOND reclaim of the same machine APPENDS to
|
|
435
|
+
-- metadata.reclaim_history rather than overwriting it, so prior reclaim records are preserved.
|
|
436
|
+
------------------------------------------------------------------------------------------
|
|
437
|
+
do $$
|
|
438
|
+
begin
|
|
439
|
+
perform * from agent_board.reclaim_fleet_machine(
|
|
440
|
+
(select m1 from _reclaim_fx), (select victim from _reclaim_fx),
|
|
441
|
+
(select operator from _reclaim_fx), 'second arbitration on the same machine');
|
|
442
|
+
end
|
|
443
|
+
$$;
|
|
444
|
+
-- (26) MUTATION PIN: reverting the audit to overwrite (a single-element array / flat keys) leaves
|
|
445
|
+
-- reclaim_history at length 1 → this flips.
|
|
446
|
+
select is(
|
|
447
|
+
(select jsonb_array_length(metadata -> 'reclaim_history') from agent_board.fleet_machines where id = (select m1 from _reclaim_fx)),
|
|
448
|
+
2,
|
|
449
|
+
'audit: a repeated reclaim appends to reclaim_history (prior record preserved)'
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
------------------------------------------------------------------------------------------
|
|
453
|
+
-- CLAIM RELEASE (#150 codex MUST-FIX 2, m6): a squatter's ACTIVE machine claim on the reclaimed
|
|
454
|
+
-- fingerprint is released so the new owner can acquire immediately. Acquire as service_role with
|
|
455
|
+
-- explicit agent_ids (service_role short-circuits authority; _resolved_actor_agent_id returns the
|
|
456
|
+
-- passed id). FUTURE lease so acquire_claim's own stale-sweep does not clear it — only reclaim's
|
|
457
|
+
-- release does (else the pin is fake).
|
|
458
|
+
------------------------------------------------------------------------------------------
|
|
459
|
+
do $$
|
|
460
|
+
begin
|
|
461
|
+
perform * from agent_board.acquire_claim(
|
|
462
|
+
(select squatter from _reclaim_fx), 'machine', (select fp6 from _reclaim_fx), now() + interval '1 hour');
|
|
463
|
+
perform * from agent_board.reclaim_fleet_machine(
|
|
464
|
+
(select m6 from _reclaim_fx), (select new_owner from _reclaim_fx),
|
|
465
|
+
(select operator from _reclaim_fx), 'reclaim releases the squatter claim');
|
|
466
|
+
end
|
|
467
|
+
$$;
|
|
468
|
+
|
|
469
|
+
-- (27) STATE PIN: the squatter's claim on the fingerprint is released. MUTATION PIN: skipping the
|
|
470
|
+
-- claim-release step leaves it 'active'.
|
|
471
|
+
select is(
|
|
472
|
+
(select status from agent_board.claims
|
|
473
|
+
where resource_kind = 'machine' and resource_key = (select fp6 from _reclaim_fx)
|
|
474
|
+
and agent_id = (select squatter from _reclaim_fx)),
|
|
475
|
+
'released',
|
|
476
|
+
'claim: reclaim releases the squatter''s active machine claim on the fingerprint'
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
-- (28) AVAILABILITY: the new owner can now acquire the machine claim. MUTATION PIN: skipping the
|
|
480
|
+
-- claim release leaves the squatter's active claim → acquire raises 23505 → this lives_ok flips.
|
|
481
|
+
select lives_ok(
|
|
482
|
+
$$ select * from agent_board.acquire_claim(
|
|
483
|
+
(select new_owner from _reclaim_fx), 'machine', (select fp6 from _reclaim_fx), now() + interval '1 hour') $$,
|
|
484
|
+
'claim: the new owner can acquire the reclaimed machine claim immediately after reclaim'
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
------------------------------------------------------------------------------------------
|
|
488
|
+
-- DEFENSIVE (#150 codex, m7): reclaiming to a new owner who ALREADY holds a binding on the machine
|
|
489
|
+
-- must NOT revoke the new owner's own binding.
|
|
490
|
+
------------------------------------------------------------------------------------------
|
|
491
|
+
do $$
|
|
492
|
+
begin
|
|
493
|
+
perform * from agent_board.reclaim_fleet_machine(
|
|
494
|
+
(select m7 from _reclaim_fx), (select new_owner from _reclaim_fx),
|
|
495
|
+
(select operator from _reclaim_fx), 'reclaim preserves the new owner binding');
|
|
496
|
+
end
|
|
497
|
+
$$;
|
|
498
|
+
-- (29) MUTATION PIN: dropping `and fma.agent_id <> p_new_owner_agent_id` from the binding revoke
|
|
499
|
+
-- revokes the new owner's own binding → this flips.
|
|
500
|
+
select is(
|
|
501
|
+
(select lifecycle_state from agent_board.fleet_machine_agents
|
|
502
|
+
where machine_id = (select m7 from _reclaim_fx) and agent_id = (select new_owner from _reclaim_fx)),
|
|
503
|
+
'active',
|
|
504
|
+
'defensive: reclaim does not revoke the new owner''s own existing binding'
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
select * from finish();
|
|
508
|
+
|
|
509
|
+
rollback;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
-- #132 — the fleet_machines authenticated SELECT policy must let a bound agent (and the owner)
|
|
2
|
+
-- read its machine, and no one else. Before the correlation fix the binding arm was dead
|
|
3
|
+
-- (`fma.machine_id = fma.id`), so assertion 1 is the mutation pin: it returns 0 with the bug and
|
|
4
|
+
-- 1 with the fix. Runs the RLS probes under `set local role authenticated` (RLS is bypassed for
|
|
5
|
+
-- the postgres/superuser connection, so the fixture is built as postgres then probed as
|
|
6
|
+
-- authenticated with request.jwt.claim.sub selecting the actor).
|
|
7
|
+
create extension if not exists pgtap with schema extensions;
|
|
8
|
+
set search_path = extensions, public;
|
|
9
|
+
|
|
10
|
+
begin;
|
|
11
|
+
|
|
12
|
+
select plan(4);
|
|
13
|
+
|
|
14
|
+
do $$
|
|
15
|
+
declare
|
|
16
|
+
v_owner_user uuid; v_bound_user uuid; v_nonbound_user uuid;
|
|
17
|
+
begin
|
|
18
|
+
v_owner_user := public.ensure_github_user('fm132-owner', 'fm132-owner@example.test', 'fm132-owner', 'FM132 Owner');
|
|
19
|
+
v_bound_user := public.ensure_github_user('fm132-bound', 'fm132-bound@example.test', 'fm132-bound', 'FM132 Bound');
|
|
20
|
+
v_nonbound_user := public.ensure_github_user('fm132-non', 'fm132-non@example.test', 'fm132-non', 'FM132 NonBound');
|
|
21
|
+
|
|
22
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
|
|
23
|
+
values
|
|
24
|
+
('fee70000-0000-0000-0000-00000000a001', 'agent', 'agent', 'fm132-owner', 'FM132 Owner', 'active', v_owner_user, v_owner_user),
|
|
25
|
+
('fee70000-0000-0000-0000-00000000a002', 'agent', 'agent', 'fm132-bound', 'FM132 Bound', 'active', v_bound_user, v_bound_user),
|
|
26
|
+
('fee70000-0000-0000-0000-00000000a003', 'agent', 'agent', 'fm132-non', 'FM132 NonBound', 'active', v_nonbound_user, v_nonbound_user);
|
|
27
|
+
|
|
28
|
+
insert into agent_board.fleet_trust_domains(id, slug, display_name, domain_kind, owner_agent_id)
|
|
29
|
+
values ('fee70000-0000-0000-0000-0000000000d1', 'fm132-domain', 'FM132 Domain', 'tailnet', 'fee70000-0000-0000-0000-00000000a001');
|
|
30
|
+
|
|
31
|
+
insert into agent_board.fleet_machines(id, trust_domain_id, owner_agent_id, machine_label, machine_fingerprint, lifecycle_state)
|
|
32
|
+
values ('fee70000-0000-0000-0000-0000000000c1', 'fee70000-0000-0000-0000-0000000000d1', 'fee70000-0000-0000-0000-00000000a001', 'FM132 Machine', 'fm132-fingerprint', 'active');
|
|
33
|
+
|
|
34
|
+
-- active binding: the BOUND agent runs on the machine.
|
|
35
|
+
insert into agent_board.fleet_machine_agents(id, machine_id, agent_id, lifecycle_state)
|
|
36
|
+
values ('fee70000-0000-0000-0000-0000000000e1', 'fee70000-0000-0000-0000-0000000000c1', 'fee70000-0000-0000-0000-00000000a002', 'active');
|
|
37
|
+
|
|
38
|
+
perform set_config('_fm132.owner', v_owner_user::text, true);
|
|
39
|
+
perform set_config('_fm132.bound', v_bound_user::text, true);
|
|
40
|
+
perform set_config('_fm132.non', v_nonbound_user::text, true);
|
|
41
|
+
end
|
|
42
|
+
$$;
|
|
43
|
+
|
|
44
|
+
set local role authenticated;
|
|
45
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
46
|
+
|
|
47
|
+
-- THE FIX (mutation pin): a bound agent sees its machine via the binding arm. Was 0 (dead
|
|
48
|
+
-- self-comparison), now 1.
|
|
49
|
+
select set_config('request.jwt.claim.sub', current_setting('_fm132.bound'), true);
|
|
50
|
+
select is(
|
|
51
|
+
(select count(*)::int from agent_board.fleet_machines where id = 'fee70000-0000-0000-0000-0000000000c1'),
|
|
52
|
+
1, 'a bound active agent can SELECT its own machine (correlation fixed)');
|
|
53
|
+
|
|
54
|
+
-- a non-bound agent sees nothing.
|
|
55
|
+
select set_config('request.jwt.claim.sub', current_setting('_fm132.non'), true);
|
|
56
|
+
select is(
|
|
57
|
+
(select count(*)::int from agent_board.fleet_machines where id = 'fee70000-0000-0000-0000-0000000000c1'),
|
|
58
|
+
0, 'a non-bound agent cannot see the machine');
|
|
59
|
+
|
|
60
|
+
-- the owner sees it via the owner arm (unaffected by the fix).
|
|
61
|
+
select set_config('request.jwt.claim.sub', current_setting('_fm132.owner'), true);
|
|
62
|
+
select is(
|
|
63
|
+
(select count(*)::int from agent_board.fleet_machines where id = 'fee70000-0000-0000-0000-0000000000c1'),
|
|
64
|
+
1, 'the machine owner can SELECT its machine (owner arm)');
|
|
65
|
+
|
|
66
|
+
-- a REVOKED binding no longer grants visibility.
|
|
67
|
+
reset role;
|
|
68
|
+
update agent_board.fleet_machine_agents set lifecycle_state = 'revoked', revoked_at = now()
|
|
69
|
+
where id = 'fee70000-0000-0000-0000-0000000000e1';
|
|
70
|
+
set local role authenticated;
|
|
71
|
+
select set_config('request.jwt.claim.sub', current_setting('_fm132.bound'), true);
|
|
72
|
+
select is(
|
|
73
|
+
(select count(*)::int from agent_board.fleet_machines where id = 'fee70000-0000-0000-0000-0000000000c1'),
|
|
74
|
+
0, 'a revoked binding no longer grants machine visibility');
|
|
75
|
+
|
|
76
|
+
reset role;
|
|
77
|
+
select * from finish();
|
|
78
|
+
|
|
79
|
+
rollback;
|
|
@@ -110,8 +110,8 @@ begin
|
|
|
110
110
|
|
|
111
111
|
insert into agent_board.agent_signing_keys(agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
112
112
|
values
|
|
113
|
-
(v_sender, 'primary', 1,
|
|
114
|
-
(v_revoked_key_sender, 'revoked', 1,
|
|
113
|
+
(v_sender, 'primary', 1, public.test_pubkey(v_sender::text), 'raw_ed25519_base64url', public.test_key_fingerprint(v_sender::text), 'active', 'proven'),
|
|
114
|
+
(v_revoked_key_sender, 'revoked', 1, public.test_pubkey(v_revoked_key_sender::text), 'raw_ed25519_base64url', public.test_key_fingerprint(v_revoked_key_sender::text), 'active', 'proven');
|
|
115
115
|
|
|
116
116
|
select id into v_signing_key from agent_board.agent_signing_keys where agent_id = v_sender;
|
|
117
117
|
select id into v_revoked_signing_key from agent_board.agent_signing_keys where agent_id = v_revoked_key_sender;
|