@consiliency/agent-board-schema 1.12.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixtures/already-applied.json +124 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/package.json +1 -1
- package/schema-manifest.json +282 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_fedsession.test.sql +6 -4
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +29 -12
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +13 -2
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +25 -9
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
-- CLAIMGATE Phase 2 — SHADOW (#133): _can_claim_resource authority predicate + shadow stamping.
|
|
2
|
+
--
|
|
3
|
+
-- Fleet-graph authority ONLY (NEGATIVE RULE: no board-admin fallback). Fixtures are built as
|
|
4
|
+
-- the postgres superuser (RLS bypassed), then each authority probe runs `set local role
|
|
5
|
+
-- authenticated` with request.jwt.claim.sub selecting the actor — the same harness as
|
|
6
|
+
-- agent_board_fleet_machines_rls.test.sql. `_can_claim_resource` returns a boolean and never
|
|
7
|
+
-- raises, so authority probes use is((select …), true/false) directly; the acquire-side
|
|
8
|
+
-- (shadow-stamp / TTL / quota) uses lives_ok/throws_ok + a separate state read (#113).
|
|
9
|
+
--
|
|
10
|
+
-- Each destructive mutation (revoke binding / scope / harness / machine / domain) is paired with
|
|
11
|
+
-- a BEFORE assertion (authorized) and an AFTER assertion (denied) so the deny is a verified FLIP,
|
|
12
|
+
-- not an incidental false. The harness-revoked pin is the design panel's key amendment: a harness
|
|
13
|
+
-- revoke does NOT cascade to child repo-scopes, so the scope stays 'active' yet the actor must be
|
|
14
|
+
-- denied — that only holds because the chain walk re-checks fhr.lifecycle_state='active'.
|
|
15
|
+
create extension if not exists pgtap with schema extensions;
|
|
16
|
+
set search_path = extensions, public;
|
|
17
|
+
|
|
18
|
+
begin;
|
|
19
|
+
|
|
20
|
+
select plan(36);
|
|
21
|
+
|
|
22
|
+
do $$
|
|
23
|
+
declare
|
|
24
|
+
u_owner uuid; u_bound uuid; u_nonbound uuid; u_worker uuid; u_obs uuid; u_h2 uuid;
|
|
25
|
+
u_d2owner uuid; u_d2worker uuid; u_ttl uuid; u_quota uuid; u_bindrev uuid;
|
|
26
|
+
begin
|
|
27
|
+
u_owner := public.ensure_github_user('cg-owner', 'cg-owner@example.test', 'cg-owner', 'CG Owner');
|
|
28
|
+
u_bound := public.ensure_github_user('cg-bound', 'cg-bound@example.test', 'cg-bound', 'CG Bound');
|
|
29
|
+
u_nonbound := public.ensure_github_user('cg-nonbound', 'cg-nonbound@example.test', 'cg-nonbound', 'CG NonBound');
|
|
30
|
+
u_worker := public.ensure_github_user('cg-worker', 'cg-worker@example.test', 'cg-worker', 'CG Worker');
|
|
31
|
+
u_obs := public.ensure_github_user('cg-obs', 'cg-obs@example.test', 'cg-obs', 'CG Observer');
|
|
32
|
+
u_h2 := public.ensure_github_user('cg-h2', 'cg-h2@example.test', 'cg-h2', 'CG H2');
|
|
33
|
+
u_d2owner := public.ensure_github_user('cg-d2owner', 'cg-d2owner@example.test', 'cg-d2owner', 'CG D2 Owner');
|
|
34
|
+
u_d2worker := public.ensure_github_user('cg-d2worker', 'cg-d2worker@example.test', 'cg-d2worker', 'CG D2 Worker');
|
|
35
|
+
u_ttl := public.ensure_github_user('cg-ttl', 'cg-ttl@example.test', 'cg-ttl', 'CG TTL');
|
|
36
|
+
u_quota := public.ensure_github_user('cg-quota', 'cg-quota@example.test', 'cg-quota', 'CG Quota');
|
|
37
|
+
u_bindrev := public.ensure_github_user('cg-bindrev', 'cg-bindrev@example.test', 'cg-bindrev', 'CG BindRev');
|
|
38
|
+
|
|
39
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
|
|
40
|
+
values
|
|
41
|
+
('c9a70000-0000-4000-8000-0000000000a1', 'agent', 'agent', 'cg-owner', 'CG Owner', 'active', u_owner, u_owner),
|
|
42
|
+
('c9a70000-0000-4000-8000-0000000000a2', 'agent', 'agent', 'cg-bound', 'CG Bound', 'active', u_bound, u_bound),
|
|
43
|
+
('c9a70000-0000-4000-8000-0000000000a3', 'agent', 'agent', 'cg-nonbound', 'CG NonBound', 'active', u_nonbound, u_nonbound),
|
|
44
|
+
('c9a70000-0000-4000-8000-0000000000a4', 'agent', 'agent', 'cg-worker', 'CG Worker', 'active', u_worker, u_worker),
|
|
45
|
+
('c9a70000-0000-4000-8000-0000000000a5', 'agent', 'agent', 'cg-obs', 'CG Observer', 'active', u_obs, u_obs),
|
|
46
|
+
('c9a70000-0000-4000-8000-0000000000a6', 'agent', 'agent', 'cg-h2', 'CG H2', 'active', u_h2, u_h2),
|
|
47
|
+
('c9a70000-0000-4000-8000-0000000000b1', 'agent', 'agent', 'cg-d2owner', 'CG D2 Owner', 'active', u_d2owner, u_d2owner),
|
|
48
|
+
('c9a70000-0000-4000-8000-0000000000b2', 'agent', 'agent', 'cg-d2worker', 'CG D2 Worker','active', u_d2worker, u_d2worker),
|
|
49
|
+
('c9a70000-0000-4000-8000-0000000000c9', 'agent', 'agent', 'cg-ttl', 'CG TTL', 'active', u_ttl, u_ttl),
|
|
50
|
+
('c9a70000-0000-4000-8000-0000000000ca', 'agent', 'agent', 'cg-quota', 'CG Quota', 'active', u_quota, u_quota),
|
|
51
|
+
('c9a70000-0000-4000-8000-0000000000a7', 'agent', 'agent', 'cg-bindrev', 'CG BindRev', 'active', u_bindrev, u_bindrev);
|
|
52
|
+
|
|
53
|
+
-- ---- DOMAIN D1 (active) --------------------------------------------------------------
|
|
54
|
+
insert into agent_board.fleet_trust_domains(id, slug, display_name, domain_kind, owner_agent_id, lifecycle_state)
|
|
55
|
+
values ('c9a70000-0000-4000-8000-0000000000d1', 'cg-d1', 'CG Domain 1', 'tailnet', 'c9a70000-0000-4000-8000-0000000000a1', 'active');
|
|
56
|
+
|
|
57
|
+
-- M1 fpM1 (owner = A_owner); Mrev fpMrev (owner = A_owner, revoked mid-test).
|
|
58
|
+
insert into agent_board.fleet_machines(id, trust_domain_id, owner_agent_id, machine_label, machine_fingerprint, lifecycle_state)
|
|
59
|
+
values
|
|
60
|
+
('c9a70000-0000-4000-8000-0000000000f1', 'c9a70000-0000-4000-8000-0000000000d1', 'c9a70000-0000-4000-8000-0000000000a1', 'M1', 'fpM1', 'active'),
|
|
61
|
+
('c9a70000-0000-4000-8000-0000000000f2', 'c9a70000-0000-4000-8000-0000000000d1', 'c9a70000-0000-4000-8000-0000000000a1', 'Mrev', 'fpMrev', 'active');
|
|
62
|
+
|
|
63
|
+
-- Harness H1 (active) on M1: worker + observer scopes on the SAME repo (fpR1).
|
|
64
|
+
insert into agent_board.fleet_harness_registrations(id, machine_id, harness_kind, harness_label, lifecycle_state)
|
|
65
|
+
values ('c9a70000-0000-4000-8000-0000000000e1', 'c9a70000-0000-4000-8000-0000000000f1', 'claude', 'H1', 'active');
|
|
66
|
+
insert into agent_board.fleet_repo_scopes(id, harness_registration_id, repo_slug, repo_fingerprint, scope_role, lifecycle_state)
|
|
67
|
+
values
|
|
68
|
+
('c9a70000-0000-4000-8000-00000000ab01', 'c9a70000-0000-4000-8000-0000000000e1', 'repo1', 'fpR1', 'worker', 'active'),
|
|
69
|
+
('c9a70000-0000-4000-8000-00000000ab02', 'c9a70000-0000-4000-8000-0000000000e1', 'repo1', 'fpR1', 'observer', 'active'),
|
|
70
|
+
-- RSbr: a distinct active worker scope (fpR1b) for the repo-arm BINDING-revoke pin. Distinct
|
|
71
|
+
-- fingerprint so it does not collide with RSW on (harness, repo_fingerprint, scope_role).
|
|
72
|
+
('c9a70000-0000-4000-8000-00000000ab05', 'c9a70000-0000-4000-8000-0000000000e1', 'repo1b', 'fpR1b', 'worker', 'active');
|
|
73
|
+
|
|
74
|
+
-- Harness H2 (active, REVOKED mid-test) on M1: worker scope on fpR2 (the harness-revoke pin).
|
|
75
|
+
insert into agent_board.fleet_harness_registrations(id, machine_id, harness_kind, harness_label, lifecycle_state)
|
|
76
|
+
values ('c9a70000-0000-4000-8000-0000000000e2', 'c9a70000-0000-4000-8000-0000000000f1', 'codex', 'H2', 'active');
|
|
77
|
+
insert into agent_board.fleet_repo_scopes(id, harness_registration_id, repo_slug, repo_fingerprint, scope_role, lifecycle_state)
|
|
78
|
+
values ('c9a70000-0000-4000-8000-00000000ab03', 'c9a70000-0000-4000-8000-0000000000e2', 'repo2', 'fpR2', 'worker', 'active');
|
|
79
|
+
|
|
80
|
+
-- Bindings on M1.
|
|
81
|
+
insert into agent_board.fleet_machine_agents(id, machine_id, agent_id, repo_scope_id, lifecycle_state)
|
|
82
|
+
values
|
|
83
|
+
('c9a70000-0000-4000-8000-00000000ac02', 'c9a70000-0000-4000-8000-0000000000f1', 'c9a70000-0000-4000-8000-0000000000a2', null, 'active'),
|
|
84
|
+
('c9a70000-0000-4000-8000-00000000ac04', 'c9a70000-0000-4000-8000-0000000000f1', 'c9a70000-0000-4000-8000-0000000000a4', 'c9a70000-0000-4000-8000-00000000ab01', 'active'),
|
|
85
|
+
('c9a70000-0000-4000-8000-00000000ac05', 'c9a70000-0000-4000-8000-0000000000f1', 'c9a70000-0000-4000-8000-0000000000a5', 'c9a70000-0000-4000-8000-00000000ab02', 'active'),
|
|
86
|
+
('c9a70000-0000-4000-8000-00000000ac06', 'c9a70000-0000-4000-8000-0000000000f1', 'c9a70000-0000-4000-8000-0000000000a6', 'c9a70000-0000-4000-8000-00000000ab03', 'active'),
|
|
87
|
+
('c9a70000-0000-4000-8000-00000000ac07', 'c9a70000-0000-4000-8000-0000000000f1', 'c9a70000-0000-4000-8000-0000000000a7', 'c9a70000-0000-4000-8000-00000000ab05', 'active');
|
|
88
|
+
|
|
89
|
+
-- ---- DOMAIN D2 (active, REVOKED mid-test) --------------------------------------------
|
|
90
|
+
insert into agent_board.fleet_trust_domains(id, slug, display_name, domain_kind, owner_agent_id, lifecycle_state)
|
|
91
|
+
values ('c9a70000-0000-4000-8000-0000000000d2', 'cg-d2', 'CG Domain 2', 'tailnet', 'c9a70000-0000-4000-8000-0000000000b1', 'active');
|
|
92
|
+
insert into agent_board.fleet_machines(id, trust_domain_id, owner_agent_id, machine_label, machine_fingerprint, lifecycle_state)
|
|
93
|
+
values ('c9a70000-0000-4000-8000-0000000000f3', 'c9a70000-0000-4000-8000-0000000000d2', 'c9a70000-0000-4000-8000-0000000000b1', 'M2', 'fpM2', 'active');
|
|
94
|
+
insert into agent_board.fleet_harness_registrations(id, machine_id, harness_kind, harness_label, lifecycle_state)
|
|
95
|
+
values ('c9a70000-0000-4000-8000-0000000000e3', 'c9a70000-0000-4000-8000-0000000000f3', 'claude', 'H3', 'active');
|
|
96
|
+
insert into agent_board.fleet_repo_scopes(id, harness_registration_id, repo_slug, repo_fingerprint, scope_role, lifecycle_state)
|
|
97
|
+
values ('c9a70000-0000-4000-8000-00000000ab04', 'c9a70000-0000-4000-8000-0000000000e3', 'repo3', 'fpR3', 'worker', 'active');
|
|
98
|
+
insert into agent_board.fleet_machine_agents(id, machine_id, agent_id, repo_scope_id, lifecycle_state)
|
|
99
|
+
values ('c9a70000-0000-4000-8000-00000000ac23', 'c9a70000-0000-4000-8000-0000000000f3', 'c9a70000-0000-4000-8000-0000000000b2', 'c9a70000-0000-4000-8000-00000000ab04', 'active');
|
|
100
|
+
|
|
101
|
+
-- NEGATIVE RULE fixture: A_nonbound is a board OWNER but has NO fleet binding — must be denied.
|
|
102
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id)
|
|
103
|
+
values ('c9a70000-0000-4000-8000-00000000ad01', 'cg-neg-board', 'CG Neg Board', 'c9a70000-0000-4000-8000-0000000000a3');
|
|
104
|
+
|
|
105
|
+
perform set_config('_cg.owner', u_owner::text, true);
|
|
106
|
+
perform set_config('_cg.bound', u_bound::text, true);
|
|
107
|
+
perform set_config('_cg.nonbound', u_nonbound::text, true);
|
|
108
|
+
perform set_config('_cg.worker', u_worker::text, true);
|
|
109
|
+
perform set_config('_cg.obs', u_obs::text, true);
|
|
110
|
+
perform set_config('_cg.h2', u_h2::text, true);
|
|
111
|
+
perform set_config('_cg.d2owner', u_d2owner::text, true);
|
|
112
|
+
perform set_config('_cg.d2worker', u_d2worker::text, true);
|
|
113
|
+
perform set_config('_cg.ttl', u_ttl::text, true);
|
|
114
|
+
perform set_config('_cg.quota', u_quota::text, true);
|
|
115
|
+
perform set_config('_cg.bindrev', u_bindrev::text, true);
|
|
116
|
+
end
|
|
117
|
+
$$;
|
|
118
|
+
|
|
119
|
+
-- Seed 49 = (quota - 1) active file claims for the quota agent (postgres insert, RLS bypassed).
|
|
120
|
+
-- The 50th acquire reaches the cap and lives; the 51st is denied.
|
|
121
|
+
insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
|
|
122
|
+
select 'c9a70000-0000-4000-8000-0000000000ca', 'file', 'cg-quota-seed-' || g, now() + interval '1 hour'
|
|
123
|
+
from generate_series(1, agent_board._claimgate_ownerless_quota() - 1) g;
|
|
124
|
+
|
|
125
|
+
set local role authenticated;
|
|
126
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
127
|
+
|
|
128
|
+
-- ============================ MACHINE ARM ============================================
|
|
129
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.owner'), true);
|
|
130
|
+
select is((select agent_board._can_claim_resource('machine', 'fpM1')), true,
|
|
131
|
+
'machine: the machine OWNER is authorized');
|
|
132
|
+
|
|
133
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.bound'), true);
|
|
134
|
+
select is((select agent_board._can_claim_resource('machine', 'fpM1')), true,
|
|
135
|
+
'machine: an actively BOUND agent is authorized');
|
|
136
|
+
|
|
137
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.nonbound'), true);
|
|
138
|
+
select is((select agent_board._can_claim_resource('machine', 'fpM1')), false,
|
|
139
|
+
'machine: a non-bound agent is DENIED');
|
|
140
|
+
|
|
141
|
+
-- machine-revoked flip (owner of Mrev, fpMrev): authorized BEFORE, denied AFTER revoke.
|
|
142
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.owner'), true);
|
|
143
|
+
select is((select agent_board._can_claim_resource('machine', 'fpMrev')), true,
|
|
144
|
+
'machine: owner authorized on an active machine (pre-revoke)');
|
|
145
|
+
reset role;
|
|
146
|
+
update agent_board.fleet_machines set lifecycle_state = 'revoked', revoked_at = now()
|
|
147
|
+
where id = 'c9a70000-0000-4000-8000-0000000000f2';
|
|
148
|
+
set local role authenticated;
|
|
149
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
150
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.owner'), true);
|
|
151
|
+
select is((select agent_board._can_claim_resource('machine', 'fpMrev')), false,
|
|
152
|
+
'machine: a REVOKED machine denies even its owner (flip)');
|
|
153
|
+
|
|
154
|
+
-- revoked-binding flip (A_bound on M1): authorized above, denied after its binding is revoked.
|
|
155
|
+
reset role;
|
|
156
|
+
update agent_board.fleet_machine_agents set lifecycle_state = 'revoked', revoked_at = now()
|
|
157
|
+
where id = 'c9a70000-0000-4000-8000-00000000ac02';
|
|
158
|
+
set local role authenticated;
|
|
159
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
160
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.bound'), true);
|
|
161
|
+
select is((select agent_board._can_claim_resource('machine', 'fpM1')), false,
|
|
162
|
+
'machine: a REVOKED binding denies (flip)');
|
|
163
|
+
|
|
164
|
+
-- ============================ REPO ARM ==============================================
|
|
165
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.worker'), true);
|
|
166
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR1')), true,
|
|
167
|
+
'repo: an active non-observer (worker) scope is authorized');
|
|
168
|
+
select is((select agent_board._can_claim_resource('repo_file', 'fpR1:src/db.sql')), true,
|
|
169
|
+
'repo_file: fp extracted before the first colon is authorized (worker scope)');
|
|
170
|
+
|
|
171
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.obs'), true);
|
|
172
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR1')), false,
|
|
173
|
+
'repo: an OBSERVER scope is DENIED');
|
|
174
|
+
|
|
175
|
+
-- repo-arm BINDING-revoke flip (A_bindrev on fpR1b): authorized BEFORE, denied AFTER only its
|
|
176
|
+
-- own fleet_machine_agents binding is revoked (scope + harness + machine + domain stay active).
|
|
177
|
+
-- This is the sole pin on the repo arm's `fma.lifecycle_state='active'` edge — the entire effect
|
|
178
|
+
-- of revoke_fleet_subject('agent',…), which revokes ONLY bindings, flows through that one line.
|
|
179
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.bindrev'), true);
|
|
180
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR1b')), true,
|
|
181
|
+
'repo: an active binding to an active non-observer scope is authorized (pre binding-revoke)');
|
|
182
|
+
reset role;
|
|
183
|
+
update agent_board.fleet_machine_agents set lifecycle_state = 'revoked', revoked_at = now()
|
|
184
|
+
where id = 'c9a70000-0000-4000-8000-00000000ac07';
|
|
185
|
+
set local role authenticated;
|
|
186
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
187
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.bindrev'), true);
|
|
188
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR1b')), false,
|
|
189
|
+
'repo: a REVOKED binding denies the repo arm though scope/harness/machine stay active (flip)');
|
|
190
|
+
|
|
191
|
+
-- harness-revoked pin (A_h2 on fpR2): authorized BEFORE, denied AFTER the HARNESS is revoked,
|
|
192
|
+
-- while its repo-scope stays active (revoke does not cascade — the load-bearing fhr edge check).
|
|
193
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.h2'), true);
|
|
194
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR2')), true,
|
|
195
|
+
'repo: worker scope under an active harness is authorized (pre harness-revoke)');
|
|
196
|
+
reset role;
|
|
197
|
+
update agent_board.fleet_harness_registrations set lifecycle_state = 'revoked', revoked_at = now()
|
|
198
|
+
where id = 'c9a70000-0000-4000-8000-0000000000e2';
|
|
199
|
+
select is(
|
|
200
|
+
(select count(*)::int from agent_board.fleet_repo_scopes
|
|
201
|
+
where id = 'c9a70000-0000-4000-8000-00000000ab03' and lifecycle_state = 'active'),
|
|
202
|
+
1, 'repo: the child repo-scope is STILL active after the harness revoke (no cascade)');
|
|
203
|
+
set local role authenticated;
|
|
204
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
205
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.h2'), true);
|
|
206
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR2')), false,
|
|
207
|
+
'repo: a REVOKED HARNESS denies though the scope is active (PANEL amendment pin, flip)');
|
|
208
|
+
|
|
209
|
+
-- revoked-scope flip (A_worker on fpR1): revoke the worker scope -> denied.
|
|
210
|
+
reset role;
|
|
211
|
+
update agent_board.fleet_repo_scopes set lifecycle_state = 'revoked', revoked_at = now()
|
|
212
|
+
where id = 'c9a70000-0000-4000-8000-00000000ab01';
|
|
213
|
+
set local role authenticated;
|
|
214
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
215
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.worker'), true);
|
|
216
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR1')), false,
|
|
217
|
+
'repo: a REVOKED repo-scope denies (flip)');
|
|
218
|
+
|
|
219
|
+
-- domain-revoked flip (D2): A_d2owner (machine) and A_d2worker (repo) authorized BEFORE, denied AFTER.
|
|
220
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.d2owner'), true);
|
|
221
|
+
select is((select agent_board._can_claim_resource('machine', 'fpM2')), true,
|
|
222
|
+
'domain: D2 machine owner authorized (pre domain-revoke)');
|
|
223
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.d2worker'), true);
|
|
224
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR3')), true,
|
|
225
|
+
'domain: D2 repo worker authorized (pre domain-revoke)');
|
|
226
|
+
reset role;
|
|
227
|
+
update agent_board.fleet_trust_domains set lifecycle_state = 'revoked', revoked_at = now()
|
|
228
|
+
where id = 'c9a70000-0000-4000-8000-0000000000d2';
|
|
229
|
+
set local role authenticated;
|
|
230
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
231
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.d2owner'), true);
|
|
232
|
+
select is((select agent_board._can_claim_resource('machine', 'fpM2')), false,
|
|
233
|
+
'domain: a REVOKED trust domain denies the machine arm (flip)');
|
|
234
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.d2worker'), true);
|
|
235
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR3')), false,
|
|
236
|
+
'domain: a REVOKED trust domain denies the repo arm (flip)');
|
|
237
|
+
|
|
238
|
+
-- ============================ NEGATIVE RULE ==========================================
|
|
239
|
+
-- A board OWNER with no fleet binding is denied on BOTH arms (fleet graph only, no fallback).
|
|
240
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.nonbound'), true);
|
|
241
|
+
select is((select agent_board._can_claim_resource('machine', 'fpM1')), false,
|
|
242
|
+
'NEGATIVE: a board owner NOT in the fleet graph is denied (machine)');
|
|
243
|
+
select is((select agent_board._can_claim_resource('repository', 'fpR1')), false,
|
|
244
|
+
'NEGATIVE: a board owner NOT in the fleet graph is denied (repository)');
|
|
245
|
+
|
|
246
|
+
-- ============================ OWNERLESS TAIL (predicate returns true; guards live in acquire) ===
|
|
247
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.nonbound'), true);
|
|
248
|
+
select is((select agent_board._can_claim_resource('file', 'anything')), true,
|
|
249
|
+
'ownerless: file is not fleet-gated (predicate true)');
|
|
250
|
+
select is((select agent_board._can_claim_resource('task', gen_random_uuid()::text)), true,
|
|
251
|
+
'task: not this predicate''s concern (predicate true)');
|
|
252
|
+
|
|
253
|
+
-- ============================ CATALOG-DERIVED COMPLETENESS ============================
|
|
254
|
+
-- Every resource_kind the CHECK constraint admits resolves to a non-null verdict — a new kind
|
|
255
|
+
-- (added to the constraint without a predicate arm) makes this fail and names itself.
|
|
256
|
+
select is(
|
|
257
|
+
(select count(*)::int
|
|
258
|
+
from (
|
|
259
|
+
select unnest(array['task','file','repo_file','repository','machine','tool','custom']) as k
|
|
260
|
+
) kinds
|
|
261
|
+
where agent_board._can_claim_resource(
|
|
262
|
+
kinds.k,
|
|
263
|
+
case kinds.k when 'repo_file' then 'fp:path' when 'task' then gen_random_uuid()::text else 'x' end
|
|
264
|
+
) is null),
|
|
265
|
+
0, 'completeness: every catalog resource_kind resolves to a non-null verdict');
|
|
266
|
+
select is(
|
|
267
|
+
(select array_agg(distinct m[1] order by m[1])
|
|
268
|
+
from pg_constraint c
|
|
269
|
+
cross join lateral regexp_matches(pg_get_constraintdef(c.oid), '''([a-z_]+)''', 'g') as m
|
|
270
|
+
where c.conname = 'claims_resource_kind_check'
|
|
271
|
+
and c.conrelid = 'agent_board.claims'::regclass),
|
|
272
|
+
array['custom','file','machine','repo_file','repository','task','tool']::text[],
|
|
273
|
+
'completeness: the catalog kind set is exactly the 7 the predicate enumerates (guards a silent new kind)');
|
|
274
|
+
|
|
275
|
+
-- ============================ SHADOW STAMP — BLOCKS NOTHING (denied actor STILL acquires) ===
|
|
276
|
+
-- The discriminating case: acquire as a fleet-DENIED actor. The claim MUST be created and the
|
|
277
|
+
-- stamp MUST record authorized=false. (With an authorized actor this would prove nothing.)
|
|
278
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.nonbound'), true);
|
|
279
|
+
select lives_ok($$
|
|
280
|
+
select agent_board.acquire_claim(
|
|
281
|
+
null, 'machine', 'fpM1', now() + interval '1 hour', null, null, '{}'::jsonb)
|
|
282
|
+
$$, 'shadow: a fleet-DENIED actor can still acquire (blocks nothing)');
|
|
283
|
+
|
|
284
|
+
select is(
|
|
285
|
+
(select (metadata #>> '{claimgate_shadow,authorized}')::boolean
|
|
286
|
+
from agent_board.claims
|
|
287
|
+
where resource_kind = 'machine' and resource_key = 'fpM1'
|
|
288
|
+
and agent_id = 'c9a70000-0000-4000-8000-0000000000a3'),
|
|
289
|
+
false, 'shadow: the stamp on the denied actor''s claim records authorized=false');
|
|
290
|
+
|
|
291
|
+
select is(
|
|
292
|
+
(select metadata #>> '{claimgate_shadow,kind}'
|
|
293
|
+
from agent_board.claims
|
|
294
|
+
where resource_kind = 'machine' and resource_key = 'fpM1'
|
|
295
|
+
and agent_id = 'c9a70000-0000-4000-8000-0000000000a3'),
|
|
296
|
+
'machine', 'shadow: the stamp records the resource kind');
|
|
297
|
+
|
|
298
|
+
-- Authorized-verdict stamp: an ownerless (file) acquire as A_owner records authorized=true.
|
|
299
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.owner'), true);
|
|
300
|
+
select lives_ok($$
|
|
301
|
+
select agent_board.acquire_claim(
|
|
302
|
+
null, 'file', 'cg-owner-file-1', now() + interval '1 hour', null, null, '{}'::jsonb)
|
|
303
|
+
$$, 'shadow: an authorized (ownerless) acquire succeeds');
|
|
304
|
+
select is(
|
|
305
|
+
(select (metadata #>> '{claimgate_shadow,authorized}')::boolean
|
|
306
|
+
from agent_board.claims
|
|
307
|
+
where resource_kind = 'file' and resource_key = 'cg-owner-file-1'),
|
|
308
|
+
true, 'shadow: an authorized verdict stamps authorized=true');
|
|
309
|
+
|
|
310
|
+
-- ============================ OWNERLESS GUARDS — OFF BY DEFAULT (blocks nothing) ======
|
|
311
|
+
-- The TTL cap + quota are gated behind _claimgate_ownerless_guards_enabled(), which DEFAULTS
|
|
312
|
+
-- FALSE (no flag row). Per the census amendment SHADOW ships with guards OFF, so an over-cap
|
|
313
|
+
-- AND an over-quota ownerless acquire both SUCCEED — SHADOW literally blocks nothing.
|
|
314
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.ttl'), true);
|
|
315
|
+
select lives_ok($$
|
|
316
|
+
select agent_board.acquire_claim(
|
|
317
|
+
null, 'file', 'cg-ttl-offcap', now() + interval '40 days', null, null, '{}'::jsonb)
|
|
318
|
+
$$, 'guards OFF (default): an over-cap ownerless lease SUCCEEDS (blocks nothing)');
|
|
319
|
+
|
|
320
|
+
-- u_quota holds (quota-1)=49 seeded; with guards off, pushing to and past the quota still succeeds.
|
|
321
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.quota'), true);
|
|
322
|
+
select lives_ok($$
|
|
323
|
+
select agent_board.acquire_claim(
|
|
324
|
+
null, 'file', 'cg-quota-off-a', now() + interval '1 hour', null, null, '{}'::jsonb)
|
|
325
|
+
$$, 'guards OFF: the quota-th ownerless acquire succeeds');
|
|
326
|
+
select lives_ok($$
|
|
327
|
+
select agent_board.acquire_claim(
|
|
328
|
+
null, 'file', 'cg-quota-off-b', now() + interval '1 hour', null, null, '{}'::jsonb)
|
|
329
|
+
$$, 'guards OFF (default): an over-quota ownerless acquire SUCCEEDS (blocks nothing)');
|
|
330
|
+
|
|
331
|
+
-- ============================ OWNERLESS GUARDS — FLIP ON (operator post-census) =======
|
|
332
|
+
-- The operator enables the guards after the census ratifies the cap; now the cap + quota enforce.
|
|
333
|
+
reset role;
|
|
334
|
+
insert into agent_board.claimgate_enforcement_flags(scope_key, enabled) values ('ownerless_guards', true)
|
|
335
|
+
on conflict (scope_key) do update set enabled = true;
|
|
336
|
+
set local role authenticated;
|
|
337
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
338
|
+
|
|
339
|
+
-- u_quota now holds 49 + 2 = 51 active >= quota(50): any further ownerless acquire is denied.
|
|
340
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.quota'), true);
|
|
341
|
+
select throws_ok($$
|
|
342
|
+
select agent_board.acquire_claim(
|
|
343
|
+
null, 'file', 'cg-quota-on', now() + interval '1 hour', null, null, '{}'::jsonb)
|
|
344
|
+
$$, '54000', null, 'guards ON: an over-quota ownerless acquire is denied');
|
|
345
|
+
|
|
346
|
+
-- TTL cap enforces once ON: over-horizon rejected (checked before quota), within-horizon allowed.
|
|
347
|
+
select set_config('request.jwt.claim.sub', current_setting('_cg.ttl'), true);
|
|
348
|
+
select throws_ok($$
|
|
349
|
+
select agent_board.acquire_claim(
|
|
350
|
+
null, 'file', 'cg-ttl-on-over', now() + interval '40 days', null, null, '{}'::jsonb)
|
|
351
|
+
$$, '22023', null, 'guards ON: an over-cap ownerless lease is rejected');
|
|
352
|
+
select lives_ok($$
|
|
353
|
+
select agent_board.acquire_claim(
|
|
354
|
+
null, 'file', 'cg-ttl-on-ok', now() + interval '1 hour', null, null, '{}'::jsonb)
|
|
355
|
+
$$, 'guards ON: a within-horizon ownerless lease is allowed');
|
|
356
|
+
|
|
357
|
+
reset role;
|
|
358
|
+
select * from finish();
|
|
359
|
+
|
|
360
|
+
rollback;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
-- CLAIMGATE Phase 1 — CLAIMKEY (#133) pgTAP.
|
|
2
|
+
--
|
|
3
|
+
-- Pins IF-0-CLAIMKEY-1 (the canonical claim-key grammar) and the NORMALIZE-ONLY wiring into
|
|
4
|
+
-- acquire_claim / renew_claim / release_claim. Every property below is mutation-pinned: the
|
|
5
|
+
-- deliberate break that flips each assertion is noted inline.
|
|
6
|
+
--
|
|
7
|
+
-- Runs as the postgres db role; request.jwt.claim.role='service_role' takes the operator
|
|
8
|
+
-- carve-out so acquire honours the supplied agent id and renew/release skip the ownership
|
|
9
|
+
-- check (we exercise the KEY NORMALIZATION, not the authz gates, which are unchanged here).
|
|
10
|
+
--
|
|
11
|
+
-- Allow-side is lives_ok(...) + a separate state read (never is((select … from rpc()))) —
|
|
12
|
+
-- repo rule #113. The normalizer itself is a PURE function (no side effect, cannot abort the
|
|
13
|
+
-- txn), so its shape checks use is() directly.
|
|
14
|
+
create extension if not exists pgtap with schema extensions;
|
|
15
|
+
set search_path = extensions, public, agent_board;
|
|
16
|
+
|
|
17
|
+
begin;
|
|
18
|
+
|
|
19
|
+
select plan(25);
|
|
20
|
+
|
|
21
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
22
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
23
|
+
|
|
24
|
+
create temp table _ck_fixture (
|
|
25
|
+
agent uuid,
|
|
26
|
+
tok_dup_legacy uuid, -- 'machine:fpDup' — active, aliased with a canonical twin
|
|
27
|
+
tok_heal uuid, -- 'machine:fpHeal' — active, lone (canonical is free)
|
|
28
|
+
tok_release uuid -- 'machine:fpRel' — active, lone
|
|
29
|
+
) on commit drop;
|
|
30
|
+
|
|
31
|
+
do $$
|
|
32
|
+
declare
|
|
33
|
+
v_agent uuid := gen_random_uuid();
|
|
34
|
+
v_tok_dup_legacy uuid;
|
|
35
|
+
v_tok_heal uuid;
|
|
36
|
+
v_tok_release uuid;
|
|
37
|
+
begin
|
|
38
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
39
|
+
values (v_agent, 'agent', 'ck-agent-' || substr(v_agent::text, 1, 8), 'CK Agent', 'active');
|
|
40
|
+
|
|
41
|
+
-- Aliased pair for the renew never-raise pin: a legacy-shaped key and its canonical twin,
|
|
42
|
+
-- both ACTIVE (distinct resource_key values, so the partial unique index permits both).
|
|
43
|
+
insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
|
|
44
|
+
values (v_agent, 'machine', 'machine:fpDup', now() + interval '1 hour')
|
|
45
|
+
returning lease_token into v_tok_dup_legacy;
|
|
46
|
+
insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
|
|
47
|
+
values (v_agent, 'machine', 'fpDup', now() + interval '1 hour');
|
|
48
|
+
|
|
49
|
+
-- Lone legacy claim whose canonical form is FREE — renew must self-heal it.
|
|
50
|
+
insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
|
|
51
|
+
values (v_agent, 'machine', 'machine:fpHeal', now() + interval '1 hour')
|
|
52
|
+
returning lease_token into v_tok_heal;
|
|
53
|
+
|
|
54
|
+
-- Lone legacy claim for the release canonicalization pin.
|
|
55
|
+
insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
|
|
56
|
+
values (v_agent, 'machine', 'machine:fpRel', now() + interval '1 hour')
|
|
57
|
+
returning lease_token into v_tok_release;
|
|
58
|
+
|
|
59
|
+
-- MUST-FIX 3: still-active LEGACY-keyed claims; a fresh acquire whose canonical form aliases
|
|
60
|
+
-- one must be rejected (no second live hold on the same machine during the legacy window).
|
|
61
|
+
-- Two independent legacy rows so the prefixed-input and bare-input pins don't contaminate each
|
|
62
|
+
-- other (a first wrongful insert must not satisfy the second's conflict check).
|
|
63
|
+
insert into agent_board.claims(agent_id, resource_kind, resource_key, lease_expires_at)
|
|
64
|
+
values (v_agent, 'machine', 'machine:fpDH', now() + interval '1 hour'),
|
|
65
|
+
(v_agent, 'machine', 'machine:fpDH2', now() + interval '1 hour');
|
|
66
|
+
|
|
67
|
+
insert into _ck_fixture values (v_agent, v_tok_dup_legacy, v_tok_heal, v_tok_release);
|
|
68
|
+
end
|
|
69
|
+
$$;
|
|
70
|
+
|
|
71
|
+
-- ============================================================================
|
|
72
|
+
-- A. _canonical_claim_key grammar (IF-0-CLAIMKEY-1) — pure function shape pins
|
|
73
|
+
-- Mutation: revert the normalizer to identity (return btrim(p_resource_key))
|
|
74
|
+
-- -> assertions 1,3-8 flip (bare/trim cases hold; transforms fail).
|
|
75
|
+
-- ============================================================================
|
|
76
|
+
select is(agent_board._canonical_claim_key('machine', 'machine:fp1'), 'fp1',
|
|
77
|
+
'machine: strips a legacy machine: prefix to the bare fingerprint');
|
|
78
|
+
select is(agent_board._canonical_claim_key('machine', 'fp1'), 'fp1',
|
|
79
|
+
'machine: a bare fingerprint is already canonical');
|
|
80
|
+
select is(agent_board._canonical_claim_key('machine', ' fp2 '), 'fp2',
|
|
81
|
+
'machine: surrounding whitespace is trimmed');
|
|
82
|
+
select is(agent_board._canonical_claim_key('repository', 'repository:r1'), 'r1',
|
|
83
|
+
'repository: strips a legacy repository: prefix');
|
|
84
|
+
select is(agent_board._canonical_claim_key('repository', 'repo:r1'), 'r1',
|
|
85
|
+
'repository: strips a legacy repo: prefix');
|
|
86
|
+
select is(agent_board._canonical_claim_key('repo_file', 'repo_file:fpR:src/db.sql'), 'fpR:src/db.sql',
|
|
87
|
+
'repo_file: strips the kind prefix and keeps <fp>:<path>');
|
|
88
|
+
select is(agent_board._canonical_claim_key('repo_file', 'fpR:src/db.sql'), 'fpR:src/db.sql',
|
|
89
|
+
'repo_file: a bare <fp>:<path> is already canonical');
|
|
90
|
+
select is(agent_board._canonical_claim_key('repo_file', 'fpR:a:b'), 'fpR:a:b',
|
|
91
|
+
'repo_file: only the FIRST colon splits fp|path (path may contain colons)');
|
|
92
|
+
select is(agent_board._canonical_claim_key('custom', ' ck '), ' ck ',
|
|
93
|
+
'custom: free-form key passes through VERBATIM — not even trimmed (SHOULD-FIX 4; " x " != "x")');
|
|
94
|
+
select is(
|
|
95
|
+
agent_board._canonical_claim_key('task', '11111111-2222-3333-4444-555555555555'),
|
|
96
|
+
'11111111-2222-3333-4444-555555555555',
|
|
97
|
+
'task: uuid key passes through unchanged');
|
|
98
|
+
-- Unmappable keys are shadow-logged and returned UNCHANGED — never rejected (normalize-only).
|
|
99
|
+
select is(agent_board._canonical_claim_key('machine', 'machine:'), 'machine:',
|
|
100
|
+
'machine: an empty-after-strip key is unmappable — returned unchanged, not raised');
|
|
101
|
+
select is(agent_board._canonical_claim_key('repo_file', 'repo_file:justfp'), 'repo_file:justfp',
|
|
102
|
+
'repo_file: a single-component key has no <fp>:<path> split — unmappable, returned unchanged (not rejected)');
|
|
103
|
+
|
|
104
|
+
-- ============================================================================
|
|
105
|
+
-- B. acquire_claim normalizes the SUPPLIED key before insert/uniqueness
|
|
106
|
+
-- Mutation: delete the `p_resource_key := _canonical_claim_key(...)` line in
|
|
107
|
+
-- acquire_claim -> assertion 14 (stored key) and 15 (canonical dedup) flip.
|
|
108
|
+
-- ============================================================================
|
|
109
|
+
select lives_ok(
|
|
110
|
+
(select format($q$select agent_board.acquire_claim(%L::uuid, 'machine', 'machine:fpAcq', now() + interval '1 hour')$q$, agent)
|
|
111
|
+
from _ck_fixture),
|
|
112
|
+
'acquire_claim accepts a legacy-prefixed machine key (normalize-only, no rejection)');
|
|
113
|
+
select is(
|
|
114
|
+
(select resource_key from agent_board.claims
|
|
115
|
+
where resource_kind = 'machine' and status = 'active'
|
|
116
|
+
and agent_id = (select agent from _ck_fixture)
|
|
117
|
+
and resource_key = 'fpAcq'),
|
|
118
|
+
'fpAcq',
|
|
119
|
+
'acquire_claim STORES the canonical bare fingerprint, not the prefixed input');
|
|
120
|
+
-- The bare canonical form now contends on the SAME uniqueness slot -> conflict.
|
|
121
|
+
select throws_ok(
|
|
122
|
+
(select format($q$select agent_board.acquire_claim(%L::uuid, 'machine', 'fpAcq', now() + interval '1 hour')$q$, agent)
|
|
123
|
+
from _ck_fixture),
|
|
124
|
+
'23505', 'conflicting active claim',
|
|
125
|
+
'a bare key canonically aliasing an active prefixed acquire conflicts (normalization feeds the index)');
|
|
126
|
+
|
|
127
|
+
-- ============================================================================
|
|
128
|
+
-- C. renew_claim is NON-RAISING on a canonical alias via retry-once (MUST-FIX 1)
|
|
129
|
+
-- The canonical twin 'fpDup' is committed-visible, so renew's first UPDATE toward 'fpDup'
|
|
130
|
+
-- deterministically collides (23505) and the unique_violation handler retries keeping the
|
|
131
|
+
-- legacy key — exercising the SAME handler that covers the concurrent race.
|
|
132
|
+
-- Mutation: drop the exception handler (bare unconditional-canonical UPDATE) ->
|
|
133
|
+
-- assertion 16 (lives_ok) flips red with 23505. (Concurrent race pinned by the
|
|
134
|
+
-- isolation spec claimkey_renew_race.spec.)
|
|
135
|
+
-- ============================================================================
|
|
136
|
+
select lives_ok(
|
|
137
|
+
(select format($q$select agent_board.renew_claim(%L::uuid, now() + interval '2 hours')$q$, tok_dup_legacy)
|
|
138
|
+
from _ck_fixture),
|
|
139
|
+
'renew_claim does NOT raise when the canonical key is already held by an active twin');
|
|
140
|
+
select is(
|
|
141
|
+
(select resource_key from agent_board.claims
|
|
142
|
+
where lease_token = (select tok_dup_legacy from _ck_fixture)),
|
|
143
|
+
'machine:fpDup',
|
|
144
|
+
'renew_claim KEEPS the legacy key when its canonical form is occupied (deferred alias for ENFORCE)');
|
|
145
|
+
|
|
146
|
+
-- ============================================================================
|
|
147
|
+
-- D. renew_claim self-heals a lone legacy key to canonical
|
|
148
|
+
-- Mutation: drop the resource_key CASE from renew's UPDATE set-list ->
|
|
149
|
+
-- assertion 19 flips (key stays 'machine:fpHeal').
|
|
150
|
+
-- ============================================================================
|
|
151
|
+
select lives_ok(
|
|
152
|
+
(select format($q$select agent_board.renew_claim(%L::uuid, now() + interval '2 hours')$q$, tok_heal)
|
|
153
|
+
from _ck_fixture),
|
|
154
|
+
'renew_claim renews a lone legacy-keyed lease');
|
|
155
|
+
select is(
|
|
156
|
+
(select resource_key from agent_board.claims
|
|
157
|
+
where lease_token = (select tok_heal from _ck_fixture)),
|
|
158
|
+
'fpHeal',
|
|
159
|
+
'renew_claim canonicalizes a lone legacy key when the canonical form is free (self-heal)');
|
|
160
|
+
|
|
161
|
+
-- ============================================================================
|
|
162
|
+
-- E. release_claim canonicalizes the stored key (always safe — row leaves index)
|
|
163
|
+
-- Mutation: drop the resource_key assignment from release's UPDATE ->
|
|
164
|
+
-- assertion 21 flips (key stays 'machine:fpRel').
|
|
165
|
+
-- ============================================================================
|
|
166
|
+
select lives_ok(
|
|
167
|
+
(select format($q$select agent_board.release_claim(%L::uuid)$q$, tok_release)
|
|
168
|
+
from _ck_fixture),
|
|
169
|
+
'release_claim releases a legacy-keyed lease');
|
|
170
|
+
select is(
|
|
171
|
+
(select resource_key || '|' || status from agent_board.claims
|
|
172
|
+
where lease_token = (select tok_release from _ck_fixture)),
|
|
173
|
+
'fpRel|released',
|
|
174
|
+
'release_claim canonicalizes the stored key as it releases');
|
|
175
|
+
|
|
176
|
+
-- ============================================================================
|
|
177
|
+
-- E2. acquire is ALIAS-AWARE against an un-healed legacy row (MUST-FIX 3)
|
|
178
|
+
-- 'machine:fpDH' is active (legacy); an acquire whose canonical form is 'fpDH' must
|
|
179
|
+
-- conflict — otherwise two agents hold the same machine during the legacy window.
|
|
180
|
+
-- Mutation: revert acquire's sweep/conflict to exact `resource_key = p_resource_key`
|
|
181
|
+
-- -> both acquires below succeed and assertions 22,23 flip to lives (throws_ok fails).
|
|
182
|
+
-- ============================================================================
|
|
183
|
+
select throws_ok(
|
|
184
|
+
(select format($q$select agent_board.acquire_claim(%L::uuid, 'machine', 'machine:fpDH', now() + interval '1 hour')$q$, agent)
|
|
185
|
+
from _ck_fixture),
|
|
186
|
+
'23505', 'conflicting active claim',
|
|
187
|
+
'acquire of a legacy-prefixed key aliasing an active legacy row conflicts (no dual live hold)');
|
|
188
|
+
select throws_ok(
|
|
189
|
+
(select format($q$select agent_board.acquire_claim(%L::uuid, 'machine', 'fpDH2', now() + interval '1 hour')$q$, agent)
|
|
190
|
+
from _ck_fixture),
|
|
191
|
+
'23505', 'conflicting active claim',
|
|
192
|
+
'acquire of the bare canonical key aliasing an active legacy row also conflicts (independent row)');
|
|
193
|
+
|
|
194
|
+
-- ============================================================================
|
|
195
|
+
-- F. Catalog-derived completeness (repo rule #1): the resource_key writer class,
|
|
196
|
+
-- enumerated from pg_proc — NOT from a hand list. A new claims writer changes
|
|
197
|
+
-- the set (set_eq names it) and, if it skips the normalizer, fails assertion 25.
|
|
198
|
+
-- Regexes are case-insensitive, whitespace-tolerant, and match an UNQUALIFIED
|
|
199
|
+
-- `update claims` (schema on search_path) — a qualifier/spacing dodge no longer
|
|
200
|
+
-- fails-open (MUST-FIX 2). Mutation: add any new agent_board function that writes
|
|
201
|
+
-- claims without routing through _canonical_claim_key -> both 24 and 25 flip and name it.
|
|
202
|
+
-- ============================================================================
|
|
203
|
+
select set_eq(
|
|
204
|
+
$$ select p.proname
|
|
205
|
+
from pg_proc p
|
|
206
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
207
|
+
where n.nspname = 'agent_board'
|
|
208
|
+
and p.prokind = 'f'
|
|
209
|
+
and pg_get_functiondef(p.oid) ~* '(insert\s+into|update)\s+(agent_board\.)?claims\M' $$,
|
|
210
|
+
-- reclaim_fleet_machine (#150) is a fourth writer: on operator reclaim it RELEASES the squatter's
|
|
211
|
+
-- active machine claim(s) on the reclaimed fingerprint so the new owner can acquire. It routes the
|
|
212
|
+
-- key through _canonical_claim_key (assertion 25 stays 0); its positive state claim (status set to
|
|
213
|
+
-- 'released') is pinned in agent_board_fleet_machine_reclaim.test.sql.
|
|
214
|
+
array['acquire_claim', 'renew_claim', 'release_claim', 'reclaim_fleet_machine'],
|
|
215
|
+
'exactly acquire/renew/release/reclaim write agent_board.claims (catalog-enumerated writer class)');
|
|
216
|
+
select is(
|
|
217
|
+
(select count(*)::int
|
|
218
|
+
from pg_proc p
|
|
219
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
220
|
+
where n.nspname = 'agent_board'
|
|
221
|
+
and p.prokind = 'f'
|
|
222
|
+
and pg_get_functiondef(p.oid) ~* '(insert\s+into|update)\s+(agent_board\.)?claims\M'
|
|
223
|
+
and pg_get_functiondef(p.oid) !~ '_canonical_claim_key'),
|
|
224
|
+
0,
|
|
225
|
+
'every catalog-enumerated claims writer routes its resource_key through _canonical_claim_key');
|
|
226
|
+
|
|
227
|
+
select * from finish();
|
|
228
|
+
|
|
229
|
+
rollback;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- message-board#103 — BEHAVIOURAL test of the MBCONSENT-A (20260806000100) backfill.
|
|
2
|
+
--
|
|
3
|
+
-- The test runner seeds a legacy 'requested' board_membership BEFORE that migration
|
|
4
|
+
-- (tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql), so the SHIPPED backfill
|
|
5
|
+
-- runs against a real row. This asserts what the shipped migration actually wrote — it FAILS if
|
|
6
|
+
-- the backfill is removed or miscorrelated, with the migration unmodified (message-board#103
|
|
7
|
+
-- acceptance). Reads persisted replay state (no fixture of its own); the begin/rollback wrapper
|
|
8
|
+
-- the runner adds does not undo the already-applied backfill.
|
|
9
|
+
create extension if not exists pgtap with schema extensions;
|
|
10
|
+
set search_path = extensions, public;
|
|
11
|
+
|
|
12
|
+
begin;
|
|
13
|
+
|
|
14
|
+
select plan(3);
|
|
15
|
+
|
|
16
|
+
-- The seeded pending subject received exactly ONE backfill event, with the exact shape the
|
|
17
|
+
-- shipped backfill promises: neutral kind, the pending state preserved, board-authored provenance,
|
|
18
|
+
-- and the board creator as the actor. Remove the backfill -> 0 rows -> fails. Miscorrelate the
|
|
19
|
+
-- not-exists / join (e.agent_id, bm.board_id, created_by) -> wrong shape or 0 -> fails.
|
|
20
|
+
select is(
|
|
21
|
+
(select count(*)::int from agent_board.board_membership_events e
|
|
22
|
+
where e.board_id = 'bf103bbb-0000-0000-0000-0000000000b1'
|
|
23
|
+
and e.agent_id = 'bf103aaa-0000-0000-0000-000000000002'
|
|
24
|
+
and e.event_kind = 'backfill_neutral'
|
|
25
|
+
and e.result_state = 'requested'
|
|
26
|
+
and e.provenance = 'board-authored'
|
|
27
|
+
and e.actor_agent_id = 'bf103aaa-0000-0000-0000-000000000001'),
|
|
28
|
+
1,
|
|
29
|
+
'shipped MBCONSENT-A backfill emitted the neutral board-authored event for the seeded pending row');
|
|
30
|
+
|
|
31
|
+
-- Exactly one event for the subject (the DISTINCT ON / not-exists de-duplication held).
|
|
32
|
+
select is(
|
|
33
|
+
(select count(*)::int from agent_board.board_membership_events e
|
|
34
|
+
where e.board_id = 'bf103bbb-0000-0000-0000-0000000000b1'
|
|
35
|
+
and e.agent_id = 'bf103aaa-0000-0000-0000-000000000002'),
|
|
36
|
+
1,
|
|
37
|
+
'the seeded subject received exactly one backfill event (no duplicate emission)');
|
|
38
|
+
|
|
39
|
+
-- The predicate derived consent presence from the backfilled ledger: a board-authored neutral
|
|
40
|
+
-- event is NOT the subject's own consent act, so the subject is not consent-present.
|
|
41
|
+
select is(
|
|
42
|
+
agent_board._membership_consent_present('bf103bbb-0000-0000-0000-0000000000b1', 'bf103aaa-0000-0000-0000-000000000002'),
|
|
43
|
+
false,
|
|
44
|
+
'a backfilled board-authored neutral event does not by itself establish subject consent');
|
|
45
|
+
|
|
46
|
+
select * from finish();
|
|
47
|
+
|
|
48
|
+
rollback;
|