@consiliency/agent-board-schema 1.10.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/fixtures/already-applied.json +136 -0
  2. package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
  3. package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
  4. package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -0
  5. package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
  6. package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
  7. package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
  8. package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
  9. package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
  10. package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
  11. package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
  12. package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
  13. package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
  14. package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
  15. package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
  16. package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
  17. package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
  18. package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
  19. package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
  20. package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
  21. package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
  22. package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
  23. package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
  24. package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
  25. package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
  26. package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
  27. package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
  28. package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
  29. package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
  30. package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
  31. package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
  32. package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
  33. package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
  34. package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
  35. package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
  36. package/package.json +1 -1
  37. package/schema-manifest.json +309 -3
  38. package/src/tooling.test.mjs +123 -2
  39. package/tests/agent_board_actorspoof.test.sql +349 -0
  40. package/tests/agent_board_agentsdir.test.sql +154 -0
  41. package/tests/agent_board_authbound_boundary.test.sql +9 -1
  42. package/tests/agent_board_claimgate_enforce.test.sql +464 -0
  43. package/tests/agent_board_claimgate_predicate.test.sql +360 -0
  44. package/tests/agent_board_claimkey.test.sql +229 -0
  45. package/tests/agent_board_consent_backfill.test.sql +48 -0
  46. package/tests/agent_board_consent_emission.test.sql +601 -0
  47. package/tests/agent_board_consent_gating.test.sql +489 -0
  48. package/tests/agent_board_consent_substrate.test.sql +720 -0
  49. package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
  50. package/tests/agent_board_delegation_enforcement.test.sql +55 -6
  51. package/tests/agent_board_delegation_policies.test.sql +107 -1
  52. package/tests/agent_board_fedsession.test.sql +6 -4
  53. package/tests/agent_board_fedsession_autojoin.test.sql +181 -0
  54. package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
  55. package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
  56. package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
  57. package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
  58. package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
  59. package/tests/agent_board_fleet_policy.test.sql +2 -2
  60. package/tests/agent_board_gatenull_membstate.test.sql +352 -0
  61. package/tests/agent_board_grants_hygiene.test.sql +408 -0
  62. package/tests/agent_board_identity_providers.test.sql +9 -0
  63. package/tests/agent_board_inbox_reads.test.sql +29 -12
  64. package/tests/agent_board_inbox_rpcs.test.sql +44 -34
  65. package/tests/agent_board_invitepolicy.test.sql +205 -0
  66. package/tests/agent_board_leaveidem.test.sql +194 -0
  67. package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
  68. package/tests/agent_board_mint_harden.test.sql +303 -0
  69. package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
  70. package/tests/agent_board_obsgate.test.sql +178 -0
  71. package/tests/agent_board_obsgate2.test.sql +153 -0
  72. package/tests/agent_board_obsgate3.test.sql +170 -0
  73. package/tests/agent_board_obsgate4.test.sql +196 -0
  74. package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
  75. package/tests/agent_board_pubexec_class.test.sql +53 -0
  76. package/tests/agent_board_rejectsem.test.sql +271 -0
  77. package/tests/agent_board_resigning_refresh.test.sql +1 -1
  78. package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
  79. package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
  80. package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
  81. package/tests/agent_board_rolereset.test.sql +351 -0
  82. package/tests/agent_board_roomdeleg.test.sql +5 -3
  83. package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
  84. package/tests/agent_board_roomlink.test.sql +25 -9
  85. package/tests/agent_board_rowid.test.sql +1274 -0
  86. package/tests/agent_board_signature_rls_reads.test.sql +3 -3
  87. package/tests/agent_board_signed_write_gateway.test.sql +3 -3
  88. package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
  89. package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
  90. package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
  91. package/tests/agent_board_signing_store.test.sql +6 -6
  92. package/tests/fixtures/isolation-substrate.sql +93 -0
  93. package/tests/fixtures/test-prereqs.sql +33 -0
  94. package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
  95. package/tests/isolation/expected/claimkey_renew_race.out +38 -0
  96. package/tests/isolation/expected/consent_approval_lock.out +43 -0
  97. package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
  98. package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
  99. package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
  100. package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
  101. package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
  102. package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
  103. package/tests/isolation/specs/consent_approval_lock.spec +98 -0
  104. package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
  105. package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
  106. package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
  107. package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
  108. package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
  109. package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
@@ -0,0 +1,181 @@
1
+ -- FEDSESSION-AUTOJOIN pgTAP: enroll_federated_human auto-joins a freshly provisioned
2
+ -- federated human to the provider's designated default board.
3
+ --
4
+ -- Proves: the additive default_board_id column + the service_role-only helper; a first
5
+ -- provision with an ACTIVE designated board yields exactly one baseline 'member'/'joined'
6
+ -- self-created membership tagged source=fedsession-autojoin; re-provision is idempotent (no
7
+ -- duplicate); a NULL default_board_id yields no membership; an INACTIVE designated board is
8
+ -- a non-fatal skip (the human still provisions, no membership); and a human who LEFT the
9
+ -- board is NOT re-joined on re-provision (first-time-only). Runs as postgres
10
+ -- (service_role-equivalent), mirroring agent_board_fedsession.test.sql.
11
+
12
+ create extension if not exists pgtap with schema extensions;
13
+ set search_path = extensions, public, agent_board;
14
+
15
+ begin;
16
+
17
+ select plan(14);
18
+
19
+ -- ---------------------------------------------------------------------------
20
+ -- Phase 0: metadata + privileges
21
+ -- ---------------------------------------------------------------------------
22
+ -- 1
23
+ select has_column('agent_board', 'identity_providers', 'default_board_id',
24
+ 'identity_providers.default_board_id exists');
25
+ -- 2
26
+ select has_function('agent_board', '_fedsession_autojoin_default_board',
27
+ array['agent_board.identity_providers', 'uuid'],
28
+ '_fedsession_autojoin_default_board(identity_providers,uuid) exists');
29
+ -- 3
30
+ select function_privs_are('agent_board', '_fedsession_autojoin_default_board',
31
+ array['agent_board.identity_providers', 'uuid'], 'service_role', array['EXECUTE'],
32
+ 'service_role may EXECUTE _fedsession_autojoin_default_board');
33
+ -- 4
34
+ select function_privs_are('agent_board', '_fedsession_autojoin_default_board',
35
+ array['agent_board.identity_providers', 'uuid'], 'authenticated', array[]::text[],
36
+ 'authenticated may NOT EXECUTE _fedsession_autojoin_default_board');
37
+
38
+ -- ---------------------------------------------------------------------------
39
+ -- Fixtures + provisioning
40
+ -- ---------------------------------------------------------------------------
41
+ create temp table _aj (
42
+ board_active uuid, board_inactive uuid,
43
+ agent_h1 uuid, agent_h2 uuid, agent_h3 uuid,
44
+ iss1 text, sub1 text
45
+ ) on commit drop;
46
+ grant all on table _aj to authenticated, service_role;
47
+
48
+ do $$
49
+ declare
50
+ s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
51
+ v_owner_agent uuid;
52
+ v_board_active uuid; v_board_inactive uuid;
53
+ v_iss1 text; v_iss2 text; v_iss3 text;
54
+ v_sub1 text := 'aj-user1-';
55
+ v_h1 uuid; v_h2 uuid; v_h3 uuid;
56
+ begin
57
+ v_iss1 := 'https://aj1-' || s || '.test';
58
+ v_iss2 := 'https://aj2-' || s || '.test';
59
+ v_iss3 := 'https://aj3-' || s || '.test';
60
+ v_sub1 := 'aj-user1-' || s;
61
+
62
+ -- A board-owner agent (created_by for the seeded boards).
63
+ insert into agent_board.agents(agent_type, kind, handle, display_name, status)
64
+ values ('system', 'system'::agent_board.agent_kind, 'aj-owner-' || s, 'AJ Owner', 'active')
65
+ returning id into v_owner_agent;
66
+
67
+ -- An ACTIVE default board and an ARCHIVED (inactive) board.
68
+ insert into agent_board.boards(slug, name, created_by_agent_id, visibility, status)
69
+ values ('aj-active-' || s, 'AJ Active', v_owner_agent, 'private', 'active')
70
+ returning id into v_board_active;
71
+ insert into agent_board.boards(slug, name, created_by_agent_id, visibility, status)
72
+ values ('aj-inactive-' || s, 'AJ Inactive', v_owner_agent, 'private', 'archived')
73
+ returning id into v_board_inactive;
74
+
75
+ -- P1: default board = ACTIVE board -> provision H1 (should auto-join).
76
+ insert into agent_board.identity_providers
77
+ (issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
78
+ values (v_iss1, 'message-board', true, true, '{human}', v_board_active);
79
+ select e.agent_id into v_h1 from agent_board.enroll_federated_human(
80
+ v_iss1, 'message-board', v_sub1,
81
+ jsonb_build_object('sub', v_sub1, 'email_verified', false), 'H1') e;
82
+
83
+ -- P2: default board NULL -> provision H2 (no auto-join).
84
+ insert into agent_board.identity_providers
85
+ (issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
86
+ values (v_iss2, 'message-board', true, true, '{human}', null);
87
+ select e.agent_id into v_h2 from agent_board.enroll_federated_human(
88
+ v_iss2, 'message-board', 'aj-user2-' || s,
89
+ jsonb_build_object('sub', 'aj-user2-' || s, 'email_verified', false), 'H2') e;
90
+
91
+ -- P3: default board = INACTIVE board -> provision H3 (no auto-join; provision still succeeds).
92
+ insert into agent_board.identity_providers
93
+ (issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
94
+ values (v_iss3, 'message-board', true, true, '{human}', v_board_inactive);
95
+ select e.agent_id into v_h3 from agent_board.enroll_federated_human(
96
+ v_iss3, 'message-board', 'aj-user3-' || s,
97
+ jsonb_build_object('sub', 'aj-user3-' || s, 'email_verified', false), 'H3') e;
98
+
99
+ -- Idempotent re-provision of H1 (same issuer/sub) -> must NOT add a second membership.
100
+ perform e.agent_id from agent_board.enroll_federated_human(
101
+ v_iss1, 'message-board', v_sub1,
102
+ jsonb_build_object('sub', v_sub1, 'email_verified', false), 'H1') e;
103
+
104
+ insert into _aj values (v_board_active, v_board_inactive, v_h1, v_h2, v_h3, v_iss1, v_sub1);
105
+ end
106
+ $$;
107
+
108
+ -- H1 auto-joined the ACTIVE default board ------------------------------------------------
109
+ -- 5
110
+ select is(
111
+ (select count(*)::int from agent_board.board_memberships m, _aj
112
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
113
+ 1, 'H1 has exactly one membership on the active default board (idempotent after re-provision)');
114
+ -- 6
115
+ select is(
116
+ (select m.role from agent_board.board_memberships m, _aj
117
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
118
+ 'member', 'auto-join role is baseline member (never owner/admin)');
119
+ -- 7
120
+ select is(
121
+ (select m.membership_state from agent_board.board_memberships m, _aj
122
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
123
+ 'joined', 'auto-join membership_state is joined');
124
+ -- 8
125
+ select is(
126
+ (select m.created_by_agent_id from agent_board.board_memberships m, _aj
127
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
128
+ (select agent_h1 from _aj), 'auto-join created_by is the fed-human itself (self-join)');
129
+ -- 9
130
+ select is(
131
+ (select m.metadata->>'source' from agent_board.board_memberships m, _aj
132
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
133
+ 'fedsession-autojoin', 'auto-join membership tagged source=fedsession-autojoin');
134
+ -- 10
135
+ select is(
136
+ (select count(distinct m.id)::int from agent_board.board_memberships m, _aj
137
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
138
+ 1, 'no duplicate membership rows after re-provision (idempotent)');
139
+
140
+ -- NULL default board -> no auto-join -----------------------------------------------------
141
+ -- 11
142
+ select is(
143
+ (select count(*)::int from agent_board.board_memberships m, _aj
144
+ where m.agent_id = _aj.agent_h2),
145
+ 0, 'H2 (provider default_board_id NULL) got NO auto-join membership');
146
+
147
+ -- INACTIVE default board -> non-fatal skip -----------------------------------------------
148
+ -- 12
149
+ select ok((select agent_h3 is not null from _aj),
150
+ 'H3 provisioned successfully even though its designated default board is inactive');
151
+ -- 13
152
+ select is(
153
+ (select count(*)::int from agent_board.board_memberships m, _aj
154
+ where m.board_id = _aj.board_inactive and m.agent_id = _aj.agent_h3),
155
+ 0, 'inactive default board yields NO auto-join (non-fatal skip)');
156
+
157
+ -- Left-respect: a human who LEFT the board is NOT re-joined on re-provision ---------------
158
+ update agent_board.board_memberships m
159
+ set membership_state = 'left', left_at = now()
160
+ from _aj
161
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1;
162
+
163
+ do $$
164
+ declare r record;
165
+ begin
166
+ select iss1, sub1 into r from _aj;
167
+ perform e.agent_id from agent_board.enroll_federated_human(
168
+ r.iss1, 'message-board', r.sub1,
169
+ jsonb_build_object('sub', r.sub1, 'email_verified', false), 'H1') e;
170
+ end
171
+ $$;
172
+
173
+ -- 14
174
+ select is(
175
+ (select count(*)::int from agent_board.board_memberships m, _aj
176
+ where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1
177
+ and m.membership_state = 'joined'),
178
+ 0, 'a fed-human who LEFT the default board is NOT re-joined on re-provision (first-time-only)');
179
+
180
+ select * from finish();
181
+ 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;