@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,170 @@
|
|
|
1
|
+
-- Consiliency/message-board#193 pgTAP: enroll_federated_human must reject an
|
|
2
|
+
-- ABSENT (issuer, audience) BEFORE it provisions or locks the human owner.
|
|
3
|
+
--
|
|
4
|
+
-- WHY THE GATE IS WHERE IT IS. PostgreSQL cannot row-lock a row that does not
|
|
5
|
+
-- exist, so with the provider absent there was nothing for the board/provider
|
|
6
|
+
-- ordering to attach to: the function skipped it, locked auth.users, and delegated
|
|
7
|
+
-- -- and the delegate's fresh READ COMMITTED snapshot could then see a provider
|
|
8
|
+
-- committed in the gap and auto-join its default board while this transaction held
|
|
9
|
+
-- the owner. Owner -> board, inverted against fleet onboarding.
|
|
10
|
+
--
|
|
11
|
+
-- WHAT THIS FILE CAN AND CANNOT PROVE. A single pgTAP transaction cannot exhibit
|
|
12
|
+
-- the interleaving; the load-bearing proof is
|
|
13
|
+
-- tests/isolation/specs/fedsession_provider_absent_owner_lock.spec, where a guard
|
|
14
|
+
-- session holds the owner row and the pre-fix function BLOCKS while the fixed one
|
|
15
|
+
-- raises without waiting. What is pinned HERE is (a) that the rejection comes from
|
|
16
|
+
-- the pre-lock gate and not from the delegate's identical-verdict step (4) -- which
|
|
17
|
+
-- is why the two messages differ -- (b) that it is positionally ahead of the owner
|
|
18
|
+
-- provisioning in the shipped definition, and (c) that the deeper provider gates and
|
|
19
|
+
-- the auto-join allow path are unchanged.
|
|
20
|
+
create extension if not exists pgtap with schema extensions;
|
|
21
|
+
set search_path = extensions, public, agent_board;
|
|
22
|
+
|
|
23
|
+
begin;
|
|
24
|
+
|
|
25
|
+
select plan(8);
|
|
26
|
+
|
|
27
|
+
-- G-1: the ABSENT-provider rejection, identified by the pre-lock gate's own message.
|
|
28
|
+
-- The delegate raises the same 42501 with a DIFFERENT message, so an errcode-only
|
|
29
|
+
-- assertion here would pass with the gate removed.
|
|
30
|
+
select throws_ok(
|
|
31
|
+
$$ select agent_board.enroll_federated_human(
|
|
32
|
+
'https://lo193-absent.test', 'message-board', 'lo193-orphan',
|
|
33
|
+
jsonb_build_object('sub','lo193-orphan'), null) $$,
|
|
34
|
+
'42501',
|
|
35
|
+
'issuer/audience not in the identity-provider allowlist (rejected before any owner or board lock)',
|
|
36
|
+
'G-1: an absent (issuer,audience) is rejected by the PRE-LOCK gate');
|
|
37
|
+
|
|
38
|
+
-- G-2: and it is positionally ahead of owner provisioning in the shipped body.
|
|
39
|
+
--
|
|
40
|
+
-- WHAT THIS ASSERTION HAD TO SURVIVE, because each version read as evidence while
|
|
41
|
+
-- being defeatable. The marker is located on a body with comments and dollar-quoted
|
|
42
|
+
-- blocks stripped and single-quoted literals KEPT (the raise message is a literal,
|
|
43
|
+
-- so stripping literals would remove the very thing being located), and it is
|
|
44
|
+
-- anchored to the RAISE SHAPE rather than to the bare message:
|
|
45
|
+
-- * bare message on raw text -> a one-line comment quoting the message passes it
|
|
46
|
+
-- with the gate moved below _ensure_agent_auth_user;
|
|
47
|
+
-- * bare message on the stripped body -> `raise debug 'ordering note: %', '<msg>'`
|
|
48
|
+
-- or `declare v_note text := '<msg>'` passes it the same way. Both verified.
|
|
49
|
+
-- * raise-exception-shaped on the stripped body -> all four masks fail, including
|
|
50
|
+
-- a composite that also makes the provider freeze null-tolerant and goes 8/8
|
|
51
|
+
-- GREEN against every other assertion here with the owner provisioned before the
|
|
52
|
+
-- verdict.
|
|
53
|
+
-- RESIDUAL, stated rather than implied: this narrows the vector, it does not close
|
|
54
|
+
-- the shape. A dead-code `raise exception '<msg>'` earlier in the body still passes.
|
|
55
|
+
-- What that residual cannot reach is the LOCK order: a gate moved below the
|
|
56
|
+
-- auth.users FOR UPDATE is caught by
|
|
57
|
+
-- tests/isolation/specs/fedsession_provider_absent_owner_lock.spec (the call goes
|
|
58
|
+
-- `<waiting ...>` instead of raising), verified by mutation. A gate moved below
|
|
59
|
+
-- _ensure_agent_auth_user but ABOVE that lock is caught by NEITHER the spec nor the
|
|
60
|
+
-- pre-remedy G-2 -- which is precisely the hole this anchoring closes.
|
|
61
|
+
select is(
|
|
62
|
+
(select case
|
|
63
|
+
when p_gate = 0 then 'gate-missing'
|
|
64
|
+
when p_ensure = 0 or p_lock is null then 'owner-provisioning-not-found'
|
|
65
|
+
when p_gate < p_ensure and p_gate < p_lock then 'gate-before-owner'
|
|
66
|
+
else 'gate-after-owner'
|
|
67
|
+
end
|
|
68
|
+
from (
|
|
69
|
+
select coalesce(strpos(d, substring(d from
|
|
70
|
+
$g$raise\s+exception\s+'issuer/audience not in the identity-provider allowlist \(rejected before any owner or board lock\)'$g$)), 0) as p_gate,
|
|
71
|
+
strpos(d, '_ensure_agent_auth_user') as p_ensure,
|
|
72
|
+
strpos(d, substring(d from 'from\s+auth\.users[^;]{0,250}?for\s+update')) as p_lock
|
|
73
|
+
from (select regexp_replace(p.prosrc,
|
|
74
|
+
$re$(\$[A-Za-z_0-9]*\$).*?\1|('([^']|'')*')|(--[^\n]*)|(/\*([^*]|\*+[^*/])*\*+/)$re$,
|
|
75
|
+
'\2', 'g') as d
|
|
76
|
+
from pg_proc p
|
|
77
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
78
|
+
where n.nspname = 'agent_board'
|
|
79
|
+
and p.proname = 'enroll_federated_human') s
|
|
80
|
+
) t),
|
|
81
|
+
'gate-before-owner',
|
|
82
|
+
'G-2: the absence gate precedes both _ensure_agent_auth_user and the auth.users lock');
|
|
83
|
+
|
|
84
|
+
-- Fixtures for the allow side and for the deeper gates the new one must not shadow.
|
|
85
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
86
|
+
values ('193a0000-0000-0000-0000-000000000001', 'agent', 'agent',
|
|
87
|
+
'lo193-owner', 'LO193 Owner', 'active');
|
|
88
|
+
|
|
89
|
+
select count(*) from agent_board.create_board(
|
|
90
|
+
'193a0000-0000-0000-0000-000000000001', 'lo193-board', 'LO193 Board',
|
|
91
|
+
'Consiliency/message-board#193 provider-absence gate', jsonb_build_object(), 'private');
|
|
92
|
+
|
|
93
|
+
insert into agent_board.identity_providers(
|
|
94
|
+
issuer, audience, supported, policy_approved,
|
|
95
|
+
allowed_scopes, allowed_board_scopes, allowed_agent_kinds, default_board_id)
|
|
96
|
+
values
|
|
97
|
+
('https://lo193-autojoin.test', 'message-board', true, true,
|
|
98
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'],
|
|
99
|
+
(select id from agent_board.boards where slug = 'lo193-board')),
|
|
100
|
+
('https://lo193-noboard.test', 'message-board', true, true,
|
|
101
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'], null),
|
|
102
|
+
('https://lo193-unapproved.test', 'message-board', true, false,
|
|
103
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human'], null);
|
|
104
|
+
|
|
105
|
+
-- G-3/G-4: provider PRESENT with a designated default board -- enrollment succeeds
|
|
106
|
+
-- and the auto-join still lands. The gate narrowed the absent case only.
|
|
107
|
+
select lives_ok(
|
|
108
|
+
$$ select agent_board.enroll_federated_human(
|
|
109
|
+
'https://lo193-autojoin.test', 'message-board', 'lo193-joiner',
|
|
110
|
+
jsonb_build_object('sub','lo193-joiner'), 'LO193 Joiner') $$,
|
|
111
|
+
'G-3: an allowlisted provider with a default board still enrolls');
|
|
112
|
+
select is(
|
|
113
|
+
(select count(*)::int
|
|
114
|
+
from agent_board.board_memberships m
|
|
115
|
+
join agent_board.agents a on a.id = m.agent_id
|
|
116
|
+
where m.board_id = (select id from agent_board.boards where slug = 'lo193-board')
|
|
117
|
+
and a.metadata #>> ARRAY['federation','subject'] = 'lo193-joiner'
|
|
118
|
+
and m.membership_state = 'joined'),
|
|
119
|
+
1, 'G-4: the fedsession auto-join still lands a joined membership');
|
|
120
|
+
|
|
121
|
+
-- G-5: provider PRESENT with a NULL default board -- enrolls, no auto-join. This is
|
|
122
|
+
-- the neighbouring absence 20260901000140 already handled (NULL designation), kept
|
|
123
|
+
-- distinct from the provider-absent case this migration closes.
|
|
124
|
+
select lives_ok(
|
|
125
|
+
$$ select agent_board.enroll_federated_human(
|
|
126
|
+
'https://lo193-noboard.test', 'message-board', 'lo193-plain',
|
|
127
|
+
jsonb_build_object('sub','lo193-plain'), 'LO193 Plain') $$,
|
|
128
|
+
'G-5: an allowlisted provider with no default board still enrolls');
|
|
129
|
+
|
|
130
|
+
-- G-6: the deeper provider gates are NOT shadowed -- an allowlisted but
|
|
131
|
+
-- unapproved provider still fails inside the delegate, with the delegate's own
|
|
132
|
+
-- message. Asserted positively so "42501 came from somewhere" is never enough.
|
|
133
|
+
select throws_ok(
|
|
134
|
+
$$ select agent_board.enroll_federated_human(
|
|
135
|
+
'https://lo193-unapproved.test', 'message-board', 'lo193-unapproved',
|
|
136
|
+
jsonb_build_object('sub','lo193-unapproved'), null) $$,
|
|
137
|
+
'42501', 'identity provider is not policy-approved',
|
|
138
|
+
'G-6: a present-but-unapproved provider is still rejected by the delegate');
|
|
139
|
+
|
|
140
|
+
-- G-7: the pre-lock gate is the ONLY new rejection -- a present provider never
|
|
141
|
+
-- reaches it. (Mutating the gate to fire unconditionally fails G-3/G-5/G-7.)
|
|
142
|
+
select is(
|
|
143
|
+
(select count(*)::int
|
|
144
|
+
from agent_board.agents a
|
|
145
|
+
where a.kind = 'human'
|
|
146
|
+
and a.metadata #>> ARRAY['federation','subject'] in ('lo193-joiner', 'lo193-plain')),
|
|
147
|
+
2, 'G-7: both allowlisted enrollments provisioned their human agents');
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
-- G-8: the CLASS of routes that reject an absent provider, enumerated from the
|
|
151
|
+
-- catalog rather than described in prose. All four reject with 42501; none
|
|
152
|
+
-- serializes the absence, and that is the recorded decision (#195) -- the
|
|
153
|
+
-- serialization alternative was built, measured and reverted because no shipped
|
|
154
|
+
-- caller can distinguish 42501 from a retryable code. A new route joining this
|
|
155
|
+
-- class fails here and names itself.
|
|
156
|
+
select is(
|
|
157
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
158
|
+
from pg_proc p
|
|
159
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
160
|
+
where n.nspname = 'agent_board'
|
|
161
|
+
and p.prokind = 'f'
|
|
162
|
+
and regexp_replace(p.prosrc,
|
|
163
|
+
$re$(\$[A-Za-z_0-9]*\$).*?\1|('([^']|'')*')|(--[^\n]*)|(/\*([^*]|\*+[^*/])*\*+/)$re$,
|
|
164
|
+
'\2', 'g') ~ 'not in the identity-provider allowlist'),
|
|
165
|
+
'_enroll_federated_human_after_revocation_gate, _enroll_federated_identity_once, '
|
|
166
|
+
|| 'enroll_federated_human, enroll_federated_identity',
|
|
167
|
+
'G-8: exactly four routes reject an absent identity provider');
|
|
168
|
+
|
|
169
|
+
select * from finish();
|
|
170
|
+
rollback;
|
|
@@ -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;
|