@consiliency/agent-board-schema 1.44.0 → 1.48.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 +16 -0
- package/migrations/20260925000100_agent_board_ownerless_renew_ttl_cap.sql +166 -0
- package/migrations/20260926000100_agent_board_ownerless_quota_census.sql +268 -0
- package/migrations/20260927000100_agent_board_fleet_domain_no_downgrade.sql +393 -0
- package/migrations/20260928000100_agent_board_ownerless_quota_renew_convergence.sql +562 -0
- package/package.json +1 -1
- package/schema-manifest.json +39 -3
- package/src/tooling.test.mjs +35 -2
- package/tests/agent_board_fleet_machine_hijack.test.sql +270 -1
- package/tests/agent_board_fleet_machine_reclaim.test.sql +28 -2
- package/tests/agent_board_ownerless_quota_census.test.sql +400 -0
- package/tests/agent_board_ownerless_renew_quota.test.sql +551 -0
- package/tests/agent_board_ownerless_renew_ttl.test.sql +226 -0
- package/tests/catalog/p_metadata_parameters.json +12 -0
- package/tests/isolation/expected/fleet_domain_no_downgrade_demote_race.out +447 -0
- package/tests/isolation/specs/fleet_domain_no_downgrade_demote_race.spec +292 -0
package/src/tooling.test.mjs
CHANGED
|
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
|
|
|
38
38
|
expect(manifest.schemaVersion).toBe(8);
|
|
39
39
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
40
40
|
});
|
|
41
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
|
|
41
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL + one RTCREDCLAIM + one ORIGINPOSTURE + one BOARDCLAIMPOLICY + one HEARTBEATPRESERVE + one LIVENESSDENOM + one OWNERLESSRENEWTTL + one OWNERLESSQUOTACENSUS + one FLEETDOMAINNODOWNGRADE + one OWNERLESSQUOTACONVERGE forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 146)', () => {
|
|
42
42
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
43
|
-
expect(manifest.migrationCount).toBe(
|
|
43
|
+
expect(manifest.migrationCount).toBe(146);
|
|
44
44
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
45
45
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
46
46
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -271,6 +271,39 @@ describe('manifest integrity', () => {
|
|
|
271
271
|
// agent_liveness_summary() always emits reporting_agents beside the stale count so a caller
|
|
272
272
|
// cannot render "0 stale" as "all clear".
|
|
273
273
|
'20260924000100_agent_board_agent_liveness_denominator.sql',
|
|
274
|
+
// #373 OWNERLESSRENEWTTL. _claimgate_ownerless_ttl_cap bounded a file/tool/custom lease at
|
|
275
|
+
// ACQUIRE and nowhere else, so an agent could renew past the 30-day cap indefinitely and the
|
|
276
|
+
// advertised bound did not exist. renew_claim now clamps (rather than raises -- a raise would
|
|
277
|
+
// kill a live lease) under acquire's own four conditions, so one operator flag governs both
|
|
278
|
+
// paths and an advisory board is exempt on both.
|
|
279
|
+
'20260925000100_agent_board_ownerless_renew_ttl_cap.sql',
|
|
280
|
+
// #388 OWNERLESSQUOTACENSUS. The quota bounds new acquisitions and never CONVERGES an agent
|
|
281
|
+
// already over it: renew does not check it (deliberately -- refusing would kill a live
|
|
282
|
+
// lease), so an agent over the ceiling renews forever and stays over. Three real routes in:
|
|
283
|
+
// claims taken while ownerless_guards was off, claims taken under an advisory board later
|
|
284
|
+
// set back to inherit, and a quota lowered afterwards. v_ownerless_claim_quota +
|
|
285
|
+
// ownerless_quota_census() are READ-ONLY: they count with acquire_claim's own predicate and
|
|
286
|
+
// always emit the population beside the finding, so "0 over quota" can never be read as
|
|
287
|
+
// "all clear" the way #367's zero rows were. Enforcement is deferred to a product decision.
|
|
288
|
+
'20260926000100_agent_board_ownerless_quota_census.sql',
|
|
289
|
+
// #395 FLEETDOMAINNODOWNGRADE. register_fleet_machine's on-conflict branch MOVES the machine
|
|
290
|
+
// (trust_domain_id = excluded), the destination gate admits a never-approved 'pending' domain,
|
|
291
|
+
// lifecycle_state is deliberately not updated (#141) and fleet_machine_agents keys on
|
|
292
|
+
// machine_id -- so an owner re-registering their own fingerprint relocated a still-'active',
|
|
293
|
+
// still-BOUND machine into a domain nobody approved, and evaluate_fleet_sender_policy never
|
|
294
|
+
// reads the domain. The move is now refused when the destination's approval rank
|
|
295
|
+
// (active=2 > pending=1 > quarantined=suspended=revoked=0) is LOWER than the machine's
|
|
296
|
+
// current domain's; equal ranks, upgrades and first registrations are unaffected.
|
|
297
|
+
'20260927000100_agent_board_fleet_domain_no_downgrade.sql',
|
|
298
|
+
// #388 OWNERLESSQUOTACONVERGE. The enforcement half the census deferred. renew_claim now
|
|
299
|
+
// asks the quota too, and while the holder is OVER it a renew may not POSTPONE a deadline:
|
|
300
|
+
// it still succeeds and the held lease is preserved exactly, so each claim lapses on its own
|
|
301
|
+
// schedule and the count falls to the ceiling. NOT a raise (that kills live work, the design
|
|
302
|
+
// #387 rejected) and NOT a short grace horizon (an agent at 60 just renews all 60 hourly and
|
|
303
|
+
// never converges). `> quota`, not acquire's `>= quota`, so an agent legitimately AT the
|
|
304
|
+
// ceiling is untouched. acquire's inline count is extracted into
|
|
305
|
+
// _claimgate_active_ownerless_claims() so the two enforcement paths cannot drift.
|
|
306
|
+
'20260928000100_agent_board_ownerless_quota_renew_convergence.sql',
|
|
274
307
|
]) {
|
|
275
308
|
expect(names.has(f)).toBe(true);
|
|
276
309
|
}
|
|
@@ -17,7 +17,15 @@ set search_path = extensions, public, agent_board;
|
|
|
17
17
|
|
|
18
18
|
begin;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
-- #395 (from assertion 15) — a re-registration may not MOVE the machine into a LESS APPROVED
|
|
21
|
+
-- trust domain. Assertion 28 (#397 round 4) gates the concurrency-safety property structurally:
|
|
22
|
+
-- the post-move decision must read both ranks in ONE statement, which no behavioural test can
|
|
23
|
+
-- check. Same function, same conflict branch, so the assertions live here rather than in
|
|
24
|
+
-- a parallel file. The approval ordering is active=2 > pending=1 > quarantined=suspended=
|
|
25
|
+
-- revoked=0, defined once in agent_board._fleet_trust_domain_approval_rank and pinned against
|
|
26
|
+
-- the column CHECK below, so a new lifecycle value fails an assertion and names itself.
|
|
27
|
+
|
|
28
|
+
select plan(28);
|
|
21
29
|
|
|
22
30
|
select has_function(
|
|
23
31
|
'agent_board', 'register_fleet_machine',
|
|
@@ -185,6 +193,267 @@ select ok(
|
|
|
185
193
|
're-register does not clear revoked_at of a revoked machine'
|
|
186
194
|
);
|
|
187
195
|
|
|
196
|
+
------------------------------------------------------------------------------------------
|
|
197
|
+
-- #395 — the no-downgrade guard on the domain MOVE.
|
|
198
|
+
--
|
|
199
|
+
-- Own fixture, own fingerprints: the block above ends with its machine deliberately revoked,
|
|
200
|
+
-- and reusing it would make every result below depend on that state.
|
|
201
|
+
-- nodown-active-1 / nodown-active-2 : two ACTIVE domains owned by owner A (equal rank)
|
|
202
|
+
-- nodown-pending : a PENDING domain owned by owner A (lower rank)
|
|
203
|
+
------------------------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
create temp table _nodown_fx (
|
|
206
|
+
owner_a uuid,
|
|
207
|
+
active_1 uuid,
|
|
208
|
+
active_2 uuid,
|
|
209
|
+
pending uuid,
|
|
210
|
+
machine_active uuid
|
|
211
|
+
) on commit drop;
|
|
212
|
+
|
|
213
|
+
grant all on table _nodown_fx to authenticated, service_role;
|
|
214
|
+
|
|
215
|
+
do $$
|
|
216
|
+
declare
|
|
217
|
+
v_owner_a uuid := (select owner_a from _hijack_fx);
|
|
218
|
+
v_active_1 uuid;
|
|
219
|
+
v_active_2 uuid;
|
|
220
|
+
v_pending uuid;
|
|
221
|
+
v_machine uuid;
|
|
222
|
+
begin
|
|
223
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
224
|
+
values
|
|
225
|
+
('fleet-nodown-active-1', 'No-downgrade Active 1', 'tailnet', v_owner_a, 'active', now()),
|
|
226
|
+
('fleet-nodown-active-2', 'No-downgrade Active 2', 'tailnet', v_owner_a, 'active', now()),
|
|
227
|
+
('fleet-nodown-pending', 'No-downgrade Pending', 'external', v_owner_a, 'pending', null);
|
|
228
|
+
|
|
229
|
+
select id into v_active_1 from agent_board.fleet_trust_domains where slug = 'fleet-nodown-active-1';
|
|
230
|
+
select id into v_active_2 from agent_board.fleet_trust_domains where slug = 'fleet-nodown-active-2';
|
|
231
|
+
select id into v_pending from agent_board.fleet_trust_domains where slug = 'fleet-nodown-pending';
|
|
232
|
+
|
|
233
|
+
-- The bound machine the move would relocate: owner A, ACTIVE domain, lifecycle active.
|
|
234
|
+
select machine_id
|
|
235
|
+
into v_machine
|
|
236
|
+
from agent_board.register_fleet_machine(
|
|
237
|
+
v_owner_a, v_active_1, 'nodown-claw', 'sha256:fleet-nodown-active', 'portal', '{"redacted":true}'::jsonb
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
insert into _nodown_fx(owner_a, active_1, active_2, pending, machine_active)
|
|
241
|
+
values (v_owner_a, v_active_1, v_active_2, v_pending, v_machine);
|
|
242
|
+
end
|
|
243
|
+
$$;
|
|
244
|
+
|
|
245
|
+
-- (15)(16) The defect: re-registering an ACTIVE-domain machine into a PENDING domain is refused,
|
|
246
|
+
-- 42501, with a message that names the reason (not #141's ownership message).
|
|
247
|
+
-- MUTATION PIN: deleting the `and v_dest_rank >= (select ... )` conjunct from the ON CONFLICT
|
|
248
|
+
-- DO UPDATE's WHERE lets the move proceed → the call lives → (15) and (16) flip.
|
|
249
|
+
select throws_ok(
|
|
250
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
251
|
+
(select owner_a from _nodown_fx),
|
|
252
|
+
(select pending from _nodown_fx),
|
|
253
|
+
'nodown-claw-moved',
|
|
254
|
+
'sha256:fleet-nodown-active',
|
|
255
|
+
'portal',
|
|
256
|
+
'{"redacted":true}'::jsonb
|
|
257
|
+
) $$,
|
|
258
|
+
'42501',
|
|
259
|
+
NULL,
|
|
260
|
+
'a re-registration into a LESS approved (pending) domain raises 42501'
|
|
261
|
+
);
|
|
262
|
+
select throws_like(
|
|
263
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
264
|
+
(select owner_a from _nodown_fx),
|
|
265
|
+
(select pending from _nodown_fx),
|
|
266
|
+
'nodown-claw-moved',
|
|
267
|
+
'sha256:fleet-nodown-active',
|
|
268
|
+
'portal',
|
|
269
|
+
'{"redacted":true}'::jsonb
|
|
270
|
+
) $$,
|
|
271
|
+
'%less approved trust domain%',
|
|
272
|
+
'the refusal names the downgrade, not #141 ownership'
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
-- (17) And it is a REFUSAL, not a partial write: the machine is still in the active domain.
|
|
276
|
+
-- MUTATION PIN: same as (15) — with the conjunct gone the domain becomes the pending one.
|
|
277
|
+
select is(
|
|
278
|
+
(select trust_domain_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-nodown-active'),
|
|
279
|
+
(select active_1 from _nodown_fx),
|
|
280
|
+
'the refused move leaves trust_domain_id on the active domain'
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
-- (18)(19) THE TWIN. A guard that refused every move would pass (15)-(17) and break a legitimate
|
|
284
|
+
-- relocation. An EQUALLY approved destination (active → active) must still work, and must
|
|
285
|
+
-- actually MOVE the row.
|
|
286
|
+
-- MUTATION PIN: `v_dest_rank >= (...)` → `v_dest_rank > (...)` refuses the equal-rank move →
|
|
287
|
+
-- (18) raises and (19) still reads active_1.
|
|
288
|
+
select lives_ok(
|
|
289
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
290
|
+
(select owner_a from _nodown_fx),
|
|
291
|
+
(select active_2 from _nodown_fx),
|
|
292
|
+
'nodown-claw-relocated',
|
|
293
|
+
'sha256:fleet-nodown-active',
|
|
294
|
+
'portal',
|
|
295
|
+
'{"redacted":true}'::jsonb
|
|
296
|
+
) $$,
|
|
297
|
+
'a re-registration into an EQUALLY approved (active) domain still succeeds'
|
|
298
|
+
);
|
|
299
|
+
select is(
|
|
300
|
+
(select trust_domain_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-nodown-active'),
|
|
301
|
+
(select active_2 from _nodown_fx),
|
|
302
|
+
'the equal-rank relocation actually moved the machine'
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
-- (20)(21) A FIRST registration has no current domain, so the guard cannot reach it: registering
|
|
306
|
+
-- a NEW fingerprint directly into a PENDING domain is unaffected and still succeeds.
|
|
307
|
+
-- MUTATION PIN: narrowing the destination gate to `ftd.lifecycle_state = 'active'` refuses it →
|
|
308
|
+
-- (20) raises and (21) finds no row.
|
|
309
|
+
select lives_ok(
|
|
310
|
+
$$ create temp table _nodown_fresh as
|
|
311
|
+
select * from agent_board.register_fleet_machine(
|
|
312
|
+
(select owner_a from _nodown_fx),
|
|
313
|
+
(select pending from _nodown_fx),
|
|
314
|
+
'nodown-fresh',
|
|
315
|
+
'sha256:fleet-nodown-fresh',
|
|
316
|
+
'portal',
|
|
317
|
+
'{"redacted":true}'::jsonb
|
|
318
|
+
) $$,
|
|
319
|
+
'a FIRST registration into a pending domain is unaffected and succeeds'
|
|
320
|
+
);
|
|
321
|
+
select is(
|
|
322
|
+
(select trust_domain_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-nodown-fresh'),
|
|
323
|
+
(select pending from _nodown_fx),
|
|
324
|
+
'the first registration landed in the pending domain it named'
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
-- (22)(23) An UPGRADE (pending → active) is permitted, and lands.
|
|
328
|
+
-- MUTATION PIN: `v_dest_rank >= (...)` → `v_dest_rank = (...)` permits only equal ranks →
|
|
329
|
+
-- (22) raises and (23) still reads the pending domain.
|
|
330
|
+
select lives_ok(
|
|
331
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
332
|
+
(select owner_a from _nodown_fx),
|
|
333
|
+
(select active_1 from _nodown_fx),
|
|
334
|
+
'nodown-fresh-promoted',
|
|
335
|
+
'sha256:fleet-nodown-fresh',
|
|
336
|
+
'portal',
|
|
337
|
+
'{"redacted":true}'::jsonb
|
|
338
|
+
) $$,
|
|
339
|
+
'a re-registration into a MORE approved (active) domain succeeds'
|
|
340
|
+
);
|
|
341
|
+
select is(
|
|
342
|
+
(select trust_domain_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-nodown-fresh'),
|
|
343
|
+
(select active_1 from _nodown_fx),
|
|
344
|
+
'the upgrade actually moved the machine into the active domain'
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
-- (24) #141 SURVIVES the two-reason refusal. The do-update now matches no row for TWO reasons,
|
|
348
|
+
-- and the function re-reads the conflicting row to name the right one — so the cross-owner case
|
|
349
|
+
-- must still raise the OWNERSHIP message, not the downgrade one. (Assertion (1) pins only the
|
|
350
|
+
-- errcode, which both branches share.)
|
|
351
|
+
-- MUTATION PIN: inverting the disambiguation test (`v_existing.owner_agent_id <> v_actor_agent_id`
|
|
352
|
+
-- → `=`) makes the cross-owner case emit the downgrade message → (24) flips.
|
|
353
|
+
select throws_like(
|
|
354
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
355
|
+
(select owner_b from _hijack_fx),
|
|
356
|
+
(select domain_b from _hijack_fx),
|
|
357
|
+
'owner-b-steal-again',
|
|
358
|
+
'sha256:fleet-hijack-fixture',
|
|
359
|
+
'portal',
|
|
360
|
+
'{"redacted":true}'::jsonb
|
|
361
|
+
) $$,
|
|
362
|
+
'%registered to another owner%',
|
|
363
|
+
'a cross-owner conflict still raises #141''s ownership message, not the downgrade one'
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
-- (25) FAIL CLOSED. A lifecycle value the ordering does not know ranks NULL, and a NULL rank
|
|
367
|
+
-- makes the `>=` conjunct not-true — i.e. refuse — rather than fail open.
|
|
368
|
+
-- MUTATION PIN: `else null` → `else 0` in _fleet_trust_domain_approval_rank flips this.
|
|
369
|
+
select is(
|
|
370
|
+
agent_board._fleet_trust_domain_approval_rank('not-a-lifecycle-state'),
|
|
371
|
+
NULL::integer,
|
|
372
|
+
'an unranked lifecycle value ranks NULL (the rank comparison then refuses)'
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
-- (26) CLASS (rule 1): the ordering is derived against the COLUMN CHECK, not against a list
|
|
376
|
+
-- written here. Every value the CHECK admits appears with its rank; a new admitted value appears
|
|
377
|
+
-- as `<value>=UNRANKED` and fails this assertion BY NAME, and a changed rank fails it too.
|
|
378
|
+
-- MUTATION PIN: changing `when 'quarantined' then 0` to `then 1` flips this.
|
|
379
|
+
select is(
|
|
380
|
+
(select string_agg(
|
|
381
|
+
s.state || '=' || coalesce(agent_board._fleet_trust_domain_approval_rank(s.state)::text, 'UNRANKED'),
|
|
382
|
+
', ' order by s.state)
|
|
383
|
+
from (
|
|
384
|
+
select (regexp_matches(pg_get_constraintdef(c.oid), '''([a-z_]+)''::text', 'g'))[1] as state
|
|
385
|
+
from pg_constraint c
|
|
386
|
+
where c.conrelid = 'agent_board.fleet_trust_domains'::regclass
|
|
387
|
+
and c.contype = 'c'
|
|
388
|
+
and pg_get_constraintdef(c.oid) ~ 'lifecycle_state = ANY'
|
|
389
|
+
) s),
|
|
390
|
+
'active=2, pending=1, quarantined=0, revoked=0, suspended=0',
|
|
391
|
+
'class: every CHECK-admitted domain lifecycle value has its approval rank (a new one names itself)'
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
-- (27) CLASS (rule 1): catalog-derived — register_fleet_machine + reclaim_fleet_machine are the
|
|
395
|
+
-- ONLY writers of fleet_machines.trust_domain_id, so a future third writer that could move a
|
|
396
|
+
-- machine appears here BY NAME and has to answer for this guard. Mirrors the owner_agent_id pin
|
|
397
|
+
-- in agent_board_fleet_machine_reclaim.test.sql; register matches on the INSERT column-list
|
|
398
|
+
-- disjunct (an upsert's text is `do update set`, never `update agent_board.fleet_machines`) and
|
|
399
|
+
-- reclaim on the UPDATE disjunct. Read through stripped_body so a COMMENT quoting the assignment
|
|
400
|
+
-- cannot satisfy it.
|
|
401
|
+
-- MUTATION PIN: removing `trust_domain_id = coalesce(p_new_trust_domain_id, fm.trust_domain_id)`
|
|
402
|
+
-- from reclaim_fleet_machine's UPDATE drops it from this list → flips.
|
|
403
|
+
select is(
|
|
404
|
+
(
|
|
405
|
+
select string_agg(proname, ', ' order by proname)
|
|
406
|
+
from (
|
|
407
|
+
select p.proname
|
|
408
|
+
from pg_proc p
|
|
409
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
410
|
+
where n.nspname = 'agent_board'
|
|
411
|
+
and p.prokind = 'f'
|
|
412
|
+
and (
|
|
413
|
+
agent_board_test.stripped_body(p.oid) ~ 'insert into agent_board\.fleet_machines\s*\([^)]*trust_domain_id'
|
|
414
|
+
or agent_board_test.stripped_body(p.oid) ~ 'update agent_board\.fleet_machines[^;]*trust_domain_id\s*='
|
|
415
|
+
)
|
|
416
|
+
) writers
|
|
417
|
+
),
|
|
418
|
+
'reclaim_fleet_machine, register_fleet_machine',
|
|
419
|
+
'class: register + reclaim are the only writers of fleet_machines.trust_domain_id (a new writer names itself)'
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
------------------------------------------------------------------------------------------
|
|
423
|
+
-- (28) SINGLE SNAPSHOT (#397 round 4) — STRUCTURAL, because it cannot be behavioural.
|
|
424
|
+
--
|
|
425
|
+
-- The safety property of this guard is that the post-move decision reads the LANDED domain's
|
|
426
|
+
-- rank and the SOURCE domain's rank from ONE snapshot. Under READ COMMITTED that is true of one
|
|
427
|
+
-- statement and FALSE of two: PostgreSQL explicitly permits another transaction to commit between
|
|
428
|
+
-- successive commands, so a split read reintroduces exactly the cross-time comparison that
|
|
429
|
+
-- rounds 1-3 of #397 each closed one instance of.
|
|
430
|
+
--
|
|
431
|
+
-- No pgTAP or isolation case can catch the split. Both reads see identical state single-session,
|
|
432
|
+
-- and no permutation can interleave between two statements of a function that is not blocked
|
|
433
|
+
-- there — so a behavioural test would pass on the split form. That is what makes this the ONE
|
|
434
|
+
-- property that would otherwise ship argued rather than gated (rule 2), and structural is how
|
|
435
|
+
-- this repo gates what it cannot derive behaviourally.
|
|
436
|
+
--
|
|
437
|
+
-- WHAT THE REGEX ESTABLISHES, precisely: a `select` whose text reaches BOTH rank calls and the
|
|
438
|
+
-- `into v_landed_rank, v_source_rank` target list WITHOUT crossing a `;`. Statement termination
|
|
439
|
+
-- is the `;`, so "no `;` between them" IS "same statement" — a scan that merely counted two rank
|
|
440
|
+
-- calls would pass on the split form and prove nothing. Read through stripped_body, so a comment
|
|
441
|
+
-- quoting the statement (or a string literal containing it) cannot satisfy it.
|
|
442
|
+
--
|
|
443
|
+
-- MUTATION PIN: split the single SELECT into two SELECTs assigning the same two variables — the
|
|
444
|
+
-- values are identical, every other assertion and every isolation permutation stays green, and
|
|
445
|
+
-- this assertion alone flips.
|
|
446
|
+
------------------------------------------------------------------------------------------
|
|
447
|
+
select ok(
|
|
448
|
+
(select agent_board_test.stripped_body(p.oid) ~
|
|
449
|
+
'select[^;]*_fleet_trust_domain_approval_rank\s*\([^;]*_fleet_trust_domain_approval_rank\s*\([^;]*into\s+v_landed_rank\s*,\s*v_source_rank[^;]*;'
|
|
450
|
+
from pg_proc p
|
|
451
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
452
|
+
where n.nspname = 'agent_board'
|
|
453
|
+
and p.proname = 'register_fleet_machine'),
|
|
454
|
+
'single snapshot: the post-move decision reads the landed and source ranks in ONE statement'
|
|
455
|
+
);
|
|
456
|
+
|
|
188
457
|
select * from finish();
|
|
189
458
|
|
|
190
459
|
rollback;
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
-- * reclaim validates the new owner and the provenance agent exist (22, 23).
|
|
33
33
|
-- * CLASS (rule #1 / EC-4): register_fleet_machine + reclaim_fleet_machine are the ONLY writers of
|
|
34
34
|
-- fleet_machines.owner_agent_id (25) — catalog-derived, so a future owner-writer names itself.
|
|
35
|
+
-- * #395 EXEMPTION (30): reclaim did NOT gain register's no-downgrade guard because its own
|
|
36
|
+
-- destination gate is strictly stronger (active-only). Asserted positively — a PENDING domain
|
|
37
|
+
-- the new owner owns is still refused — PIN: widen the lifecycle conjunct ⇒ (30) fails.
|
|
35
38
|
--
|
|
36
39
|
-- Tests run as session_user=postgres ⇒ _is_service_role() is true; probes and the authz assertion
|
|
37
40
|
-- flip to role=authenticated (jwt claims) to exercise the principal / body guard.
|
|
@@ -41,7 +44,7 @@ set search_path = extensions, public, agent_board;
|
|
|
41
44
|
|
|
42
45
|
begin;
|
|
43
46
|
|
|
44
|
-
select plan(
|
|
47
|
+
select plan(30);
|
|
45
48
|
|
|
46
49
|
-- (1) Shape.
|
|
47
50
|
select has_function(
|
|
@@ -98,7 +101,10 @@ begin
|
|
|
98
101
|
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
99
102
|
values
|
|
100
103
|
('reclaim-domain-old', 'Reclaim Domain Old', 'tailnet', v_squatter, 'active', now()),
|
|
101
|
-
('reclaim-domain-new', 'Reclaim Domain New', 'tailnet', v_new_owner, 'active', now())
|
|
104
|
+
('reclaim-domain-new', 'Reclaim Domain New', 'tailnet', v_new_owner, 'active', now()),
|
|
105
|
+
-- #395 exemption pin (assertion 30): a PENDING domain the NEW OWNER owns — the only thing
|
|
106
|
+
-- separating it from dom_new is its lifecycle, so a refusal there is about approval alone.
|
|
107
|
+
('reclaim-domain-new-pending', 'Reclaim Domain New (pending)', 'tailnet', v_new_owner, 'pending', null);
|
|
102
108
|
select id into v_dom_old from agent_board.fleet_trust_domains where slug = 'reclaim-domain-old';
|
|
103
109
|
select id into v_dom_new from agent_board.fleet_trust_domains where slug = 'reclaim-domain-new';
|
|
104
110
|
|
|
@@ -504,6 +510,26 @@ select is(
|
|
|
504
510
|
'defensive: reclaim does not revoke the new owner''s own existing binding'
|
|
505
511
|
);
|
|
506
512
|
|
|
513
|
+
------------------------------------------------------------------------------------------
|
|
514
|
+
-- (30) #395 EXEMPTION, ASSERTED POSITIVELY. register_fleet_machine gained a no-downgrade guard
|
|
515
|
+
-- (20260927000100); reclaim_fleet_machine is the other writer of fleet_machines.trust_domain_id
|
|
516
|
+
-- and did NOT gain one. The reason is that its destination gate is STRICTLY STRONGER, not weaker:
|
|
517
|
+
-- p_new_trust_domain_id must be lifecycle_state = 'active' AND owned by the new owner, so its
|
|
518
|
+
-- destination is always the top rank and it cannot express a downgrade. Rule 1 says an exemption
|
|
519
|
+
-- is asserted positively rather than left as an absence: a PENDING destination — owned by the new
|
|
520
|
+
-- owner, so ONLY its lifecycle differs from the accepted dom_new of (19) — is refused 42501.
|
|
521
|
+
-- Appended rather than filed next to (24) so the existing assertions keep their TAP numbers.
|
|
522
|
+
-- MUTATION PIN: weakening the lifecycle conjunct (`ftd.lifecycle_state = 'active'` →
|
|
523
|
+
-- `ftd.lifecycle_state in ('active','pending')`) admits it → no raise → this flips.
|
|
524
|
+
------------------------------------------------------------------------------------------
|
|
525
|
+
select throws_ok(
|
|
526
|
+
format($$ select * from agent_board.reclaim_fleet_machine(%L::uuid, %L::uuid, %L::uuid, 'pending destination domain', %L::uuid) $$,
|
|
527
|
+
(select m3 from _reclaim_fx), (select new_owner from _reclaim_fx), (select operator from _reclaim_fx),
|
|
528
|
+
(select id from agent_board.fleet_trust_domains where slug = 'reclaim-domain-new-pending')),
|
|
529
|
+
'42501', NULL,
|
|
530
|
+
'validate: a PENDING destination domain is rejected even though the new owner owns it (#395 exemption)'
|
|
531
|
+
);
|
|
532
|
+
|
|
507
533
|
select * from finish();
|
|
508
534
|
|
|
509
535
|
rollback;
|