@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.
@@ -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 = 142)', () => {
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(142);
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
- select plan(14);
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(29);
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;