@consiliency/agent-board-schema 1.48.0 → 1.51.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/20260929000100_agent_board_advisory_revert_cap_reset.sql +371 -0
- package/migrations/20260930000100_agent_board_claim_policy_race.sql +922 -0
- package/migrations/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql +158 -0
- package/migrations/20261002000100_agent_board_session_challenge_supersede.sql +383 -0
- package/package.json +1 -1
- package/schema-manifest.json +39 -3
- package/src/tooling.test.mjs +33 -2
- package/tests/agent_board_board_claim_policy.test.sql +261 -1
- package/tests/agent_board_claim_subject_gate.test.sql +11 -2
- package/tests/agent_board_claimkey.test.sql +17 -2
- package/tests/agent_board_claimpolicy_race.test.sql +1374 -0
- package/tests/agent_board_fk_lock_edges.test.sql +20 -0
- package/tests/agent_board_ownerless_quota_census.test.sql +6 -0
- package/tests/agent_board_ownerless_renew_quota.test.sql +37 -8
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +391 -36
- package/tests/catalog/p_metadata_parameters.json +16 -0
- package/tests/fixtures/test-prereqs.sql +186 -0
- package/tests/isolation/expected/challenge_supersede_lock_order.out +213 -0
- package/tests/isolation/expected/claimpolicy_acquire_revert_race.out +66 -0
- package/tests/isolation/expected/claimpolicy_board_scope.out +73 -0
- package/tests/isolation/expected/claimpolicy_claimrow_lock_order.out +80 -0
- package/tests/isolation/expected/claimpolicy_isolation_refusal.out +114 -0
- package/tests/isolation/expected/claimpolicy_renew_revert_race.out +66 -0
- package/tests/isolation/expected/claimpolicy_writer_keyshare.out +49 -0
- package/tests/isolation/specs/challenge_supersede_lock_order.spec +183 -0
- package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +118 -0
- package/tests/isolation/specs/claimpolicy_board_scope.spec +115 -0
- package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +200 -0
- package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +162 -0
- package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +150 -0
- package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +107 -0
- package/tests/seeds/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql.pre.sql +99 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Parsed test spec with 5 sessions
|
|
2
|
+
|
|
3
|
+
starting permutation: rr_begin rr_acq rr_rollback chk
|
|
4
|
+
step rr_begin: BEGIN ISOLATION LEVEL REPEATABLE READ; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
|
|
5
|
+
step rr_acq:
|
|
6
|
+
select resource_key
|
|
7
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
|
|
8
|
+
null, '40500000-0000-4000-8000-000000000050');
|
|
9
|
+
|
|
10
|
+
ERROR: claim policy enforcement requires transaction isolation level read committed (got repeatable read)
|
|
11
|
+
step rr_rollback: ROLLBACK;
|
|
12
|
+
step chk:
|
|
13
|
+
select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
|
|
14
|
+
from agent_board.claims
|
|
15
|
+
where agent_id = '40500000-0000-4000-8000-000000000004'
|
|
16
|
+
and status = 'active' and released_at is null;
|
|
17
|
+
|
|
18
|
+
claims
|
|
19
|
+
------
|
|
20
|
+
<none>
|
|
21
|
+
(1 row)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
starting permutation: rc_begin rc_acq rc_commit chk
|
|
25
|
+
step rc_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
|
|
26
|
+
step rc_acq:
|
|
27
|
+
select resource_key
|
|
28
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
|
|
29
|
+
null, '40500000-0000-4000-8000-000000000050');
|
|
30
|
+
|
|
31
|
+
resource_key
|
|
32
|
+
------------
|
|
33
|
+
cpr-iso-key
|
|
34
|
+
(1 row)
|
|
35
|
+
|
|
36
|
+
step rc_commit: COMMIT;
|
|
37
|
+
step chk:
|
|
38
|
+
select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
|
|
39
|
+
from agent_board.claims
|
|
40
|
+
where agent_id = '40500000-0000-4000-8000-000000000004'
|
|
41
|
+
and status = 'active' and released_at is null;
|
|
42
|
+
|
|
43
|
+
claims
|
|
44
|
+
-----------
|
|
45
|
+
cpr-iso-key
|
|
46
|
+
(1 row)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
starting permutation: rr_begin rr_machine rr_rollback chk
|
|
50
|
+
step rr_begin: BEGIN ISOLATION LEVEL REPEATABLE READ; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
|
|
51
|
+
step rr_machine:
|
|
52
|
+
select resource_key
|
|
53
|
+
from agent_board.acquire_claim(null, 'machine', 'cpr-iso-machine', now() + interval '1 hour',
|
|
54
|
+
null, null);
|
|
55
|
+
|
|
56
|
+
resource_key
|
|
57
|
+
---------------
|
|
58
|
+
cpr-iso-machine
|
|
59
|
+
(1 row)
|
|
60
|
+
|
|
61
|
+
step rr_rollback: ROLLBACK;
|
|
62
|
+
step chk:
|
|
63
|
+
select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
|
|
64
|
+
from agent_board.claims
|
|
65
|
+
where agent_id = '40500000-0000-4000-8000-000000000004'
|
|
66
|
+
and status = 'active' and released_at is null;
|
|
67
|
+
|
|
68
|
+
claims
|
|
69
|
+
------
|
|
70
|
+
<none>
|
|
71
|
+
(1 row)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
starting permutation: sz_begin sz_acq sz_rollback chk
|
|
75
|
+
step sz_begin: BEGIN ISOLATION LEVEL SERIALIZABLE; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
|
|
76
|
+
step sz_acq:
|
|
77
|
+
select resource_key
|
|
78
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
|
|
79
|
+
null, '40500000-0000-4000-8000-000000000050');
|
|
80
|
+
|
|
81
|
+
ERROR: claim policy enforcement requires transaction isolation level read committed (got serializable)
|
|
82
|
+
step sz_rollback: ROLLBACK;
|
|
83
|
+
step chk:
|
|
84
|
+
select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
|
|
85
|
+
from agent_board.claims
|
|
86
|
+
where agent_id = '40500000-0000-4000-8000-000000000004'
|
|
87
|
+
and status = 'active' and released_at is null;
|
|
88
|
+
|
|
89
|
+
claims
|
|
90
|
+
------
|
|
91
|
+
<none>
|
|
92
|
+
(1 row)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
starting permutation: ru_begin ru_acq ru_rollback chk
|
|
96
|
+
step ru_begin: BEGIN ISOLATION LEVEL READ UNCOMMITTED; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
|
|
97
|
+
step ru_acq:
|
|
98
|
+
select resource_key
|
|
99
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
|
|
100
|
+
null, '40500000-0000-4000-8000-000000000050');
|
|
101
|
+
|
|
102
|
+
ERROR: claim policy enforcement requires transaction isolation level read committed (got read uncommitted)
|
|
103
|
+
step ru_rollback: ROLLBACK;
|
|
104
|
+
step chk:
|
|
105
|
+
select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
|
|
106
|
+
from agent_board.claims
|
|
107
|
+
where agent_id = '40500000-0000-4000-8000-000000000004'
|
|
108
|
+
and status = 'active' and released_at is null;
|
|
109
|
+
|
|
110
|
+
claims
|
|
111
|
+
------
|
|
112
|
+
<none>
|
|
113
|
+
(1 row)
|
|
114
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Parsed test spec with 3 sessions
|
|
2
|
+
|
|
3
|
+
starting permutation: w_begin w_flip w_mark r_begin r_renew w_commit r_commit chk
|
|
4
|
+
step w_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000b1';
|
|
5
|
+
step w_flip:
|
|
6
|
+
select claim_policy
|
|
7
|
+
from agent_board.set_board_claim_policy(
|
|
8
|
+
(select id from agent_board.boards where slug = 'cpr-ren-board'), 'inherit');
|
|
9
|
+
|
|
10
|
+
claim_policy
|
|
11
|
+
------------
|
|
12
|
+
inherit
|
|
13
|
+
(1 row)
|
|
14
|
+
|
|
15
|
+
step w_mark:
|
|
16
|
+
insert into public.cpr_renew_mark(lease)
|
|
17
|
+
select lease_expires_at from agent_board.claims where id = '40500000-0000-4000-8000-000000000030';
|
|
18
|
+
|
|
19
|
+
step r_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000b1';
|
|
20
|
+
step r_renew:
|
|
21
|
+
select resource_key
|
|
22
|
+
from agent_board.renew_claim('40500000-0000-4000-8000-0000000000f1', now() + interval '400 days');
|
|
23
|
+
<waiting ...>
|
|
24
|
+
step w_commit: COMMIT;
|
|
25
|
+
step r_renew: <... completed>
|
|
26
|
+
resource_key
|
|
27
|
+
------------
|
|
28
|
+
cpr-ren-key
|
|
29
|
+
(1 row)
|
|
30
|
+
|
|
31
|
+
step r_commit: COMMIT;
|
|
32
|
+
step chk:
|
|
33
|
+
select (c.lease_expires_at > now() + interval '31 days') as over_cap_lease,
|
|
34
|
+
(c.lease_expires_at = (select m.lease from public.cpr_renew_mark m)) as lease_kept_by_revert
|
|
35
|
+
from agent_board.claims c
|
|
36
|
+
where c.id = '40500000-0000-4000-8000-000000000030';
|
|
37
|
+
|
|
38
|
+
over_cap_lease|lease_kept_by_revert
|
|
39
|
+
--------------+--------------------
|
|
40
|
+
f |t
|
|
41
|
+
(1 row)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
starting permutation: r_begin r_renew r_commit chk
|
|
45
|
+
step r_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000b1';
|
|
46
|
+
step r_renew:
|
|
47
|
+
select resource_key
|
|
48
|
+
from agent_board.renew_claim('40500000-0000-4000-8000-0000000000f1', now() + interval '400 days');
|
|
49
|
+
|
|
50
|
+
resource_key
|
|
51
|
+
------------
|
|
52
|
+
cpr-ren-key
|
|
53
|
+
(1 row)
|
|
54
|
+
|
|
55
|
+
step r_commit: COMMIT;
|
|
56
|
+
step chk:
|
|
57
|
+
select (c.lease_expires_at > now() + interval '31 days') as over_cap_lease,
|
|
58
|
+
(c.lease_expires_at = (select m.lease from public.cpr_renew_mark m)) as lease_kept_by_revert
|
|
59
|
+
from agent_board.claims c
|
|
60
|
+
where c.id = '40500000-0000-4000-8000-000000000030';
|
|
61
|
+
|
|
62
|
+
over_cap_lease|lease_kept_by_revert
|
|
63
|
+
--------------+--------------------
|
|
64
|
+
t |
|
|
65
|
+
(1 row)
|
|
66
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Parsed test spec with 3 sessions
|
|
2
|
+
|
|
3
|
+
starting permutation: t_begin t_keyshare w_begin w_flip t_acq w_commit t_commit chk
|
|
4
|
+
step t_begin: BEGIN;
|
|
5
|
+
step t_keyshare: select slug from agent_board.boards where slug = 'cpr-ks-board' for key share;
|
|
6
|
+
slug
|
|
7
|
+
------------
|
|
8
|
+
cpr-ks-board
|
|
9
|
+
(1 row)
|
|
10
|
+
|
|
11
|
+
step w_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000e1';
|
|
12
|
+
step w_flip:
|
|
13
|
+
select claim_policy
|
|
14
|
+
from agent_board.set_board_claim_policy(
|
|
15
|
+
(select id from agent_board.boards where slug = 'cpr-ks-board'), 'inherit');
|
|
16
|
+
|
|
17
|
+
claim_policy
|
|
18
|
+
------------
|
|
19
|
+
inherit
|
|
20
|
+
(1 row)
|
|
21
|
+
|
|
22
|
+
step t_acq:
|
|
23
|
+
SET LOCAL ROLE authenticated;
|
|
24
|
+
SET LOCAL "request.jwt.claim.role" = 'authenticated';
|
|
25
|
+
SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000e1';
|
|
26
|
+
select resource_key
|
|
27
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-ks-key', now() + interval '1 hour',
|
|
28
|
+
null, '40500000-0000-4000-8000-000000000060');
|
|
29
|
+
<waiting ...>
|
|
30
|
+
step w_commit: COMMIT;
|
|
31
|
+
step t_acq: <... completed>
|
|
32
|
+
resource_key
|
|
33
|
+
------------
|
|
34
|
+
cpr-ks-key
|
|
35
|
+
(1 row)
|
|
36
|
+
|
|
37
|
+
step t_commit: COMMIT;
|
|
38
|
+
step chk:
|
|
39
|
+
select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims,
|
|
40
|
+
(select claim_policy from agent_board.boards where slug = 'cpr-ks-board') as policy
|
|
41
|
+
from agent_board.claims
|
|
42
|
+
where agent_id = '40500000-0000-4000-8000-000000000005'
|
|
43
|
+
and status = 'active' and released_at is null;
|
|
44
|
+
|
|
45
|
+
claims |policy
|
|
46
|
+
----------+-------
|
|
47
|
+
cpr-ks-key|inherit
|
|
48
|
+
(1 row)
|
|
49
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# F1 (code-review-2026-09-01) — issue_signing_key_challenge must be ACYCLIC against
|
|
2
|
+
# mint_fleet_agent_session once it supersedes at the pending-challenge cap.
|
|
3
|
+
#
|
|
4
|
+
# THE CYCLE THIS CHANGE WOULD HAVE INTRODUCED. Superseding at the cap makes
|
|
5
|
+
# issue_signing_key_challenge WRITE a challenge row while it already holds the key row:
|
|
6
|
+
# agent_signing_keys(FOR UPDATE, 20260719000100 / this migration)
|
|
7
|
+
# -> agent_signing_key_challenges(UPDATE, the eviction)
|
|
8
|
+
# mint_fleet_agent_session takes exactly that pair in the OPPOSITE order:
|
|
9
|
+
# agent_signing_key_challenges(FOR UPDATE, 20260718000600:73-76)
|
|
10
|
+
# -> agent_signing_keys(FOR UPDATE, 20260718000600:95-100)
|
|
11
|
+
# That is an ABBA between two honest callers, and not a rare one: the traffic this change
|
|
12
|
+
# is FOR — a flood driving evictions while legitimate agents mint — is precisely the
|
|
13
|
+
# traffic that produces it. agent_board_fk_lock_edges.test.sql FKL-10 named the pair the
|
|
14
|
+
# first time this migration was written without the fix; that assertion is a static
|
|
15
|
+
# detector and cannot see the fix, which is why the proof lives here.
|
|
16
|
+
#
|
|
17
|
+
# THE FIX this spec pins: the eviction's row selection carries FOR UPDATE SKIP LOCKED, so
|
|
18
|
+
# the issuing transaction NEVER WAITS on a challenge row. A row a mint already holds is
|
|
19
|
+
# passed over for the next-oldest. A transaction that cannot wait cannot close a cycle.
|
|
20
|
+
#
|
|
21
|
+
# WHAT THE GOLDEN SHOWS, AND WHAT WOULD CHANGE IF THE CLAUSE WERE REMOVED. In permutation
|
|
22
|
+
# 1 `mint_lock_challenge` takes the OLDEST pending row; `issue` then runs at the cap and
|
|
23
|
+
# COMPLETES WITHOUT A `<waiting ...>` LINE, evicting the SECOND-oldest instead. Delete
|
|
24
|
+
# `skip locked` and that step blocks — the trace gains `<waiting ...>`, and once
|
|
25
|
+
# `mint_lock_key` runs the two transactions are in a cycle and the server reports
|
|
26
|
+
# `ERROR: deadlock detected`. Either outcome is a byte diff against this golden.
|
|
27
|
+
#
|
|
28
|
+
# THE "mint" SESSION IS A LOCK-EQUIVALENT STAND-IN, and here is the equivalence.
|
|
29
|
+
# * It runs mint_fleet_agent_session's two cycle-relevant acquisitions, in its order and
|
|
30
|
+
# its modes: the challenge row FOR UPDATE (:73-76), then the key row FOR UPDATE
|
|
31
|
+
# (:95-100).
|
|
32
|
+
# * It omits that function's other work — _agent_principal_active's reads, the
|
|
33
|
+
# delegation_grants lookup, the auth_user_id read, the challenge consume UPDATE and the
|
|
34
|
+
# nonfleet_sessions insert. None of those touches agent_signing_keys, and the consume
|
|
35
|
+
# UPDATE re-writes a row this session already holds, so none can add an edge to the
|
|
36
|
+
# pair under test.
|
|
37
|
+
# * A real mint call was rejected only because it needs a valid PoP challenge, an
|
|
38
|
+
# auth_user_id-bearing principal and a live lineage inside a setup block, and because
|
|
39
|
+
# every one of its rejection paths RAISES, which would abort the session mid-permutation
|
|
40
|
+
# and turn a lock-order question into an error-message question. The claim under test is
|
|
41
|
+
# a LOCK ORDER, and these are the locks.
|
|
42
|
+
#
|
|
43
|
+
# THE ISSUE SESSION IS THE REAL FUNCTION, unmodified. That side is where the fix lives, so
|
|
44
|
+
# a stand-in there would be testing the spec instead of the schema.
|
|
45
|
+
|
|
46
|
+
setup
|
|
47
|
+
{
|
|
48
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
49
|
+
values ('f1a00000-0000-4000-8000-000000000001','agent','agent','f1iso-owner','F1 ISO Owner','active');
|
|
50
|
+
|
|
51
|
+
insert into agent_board.agent_signing_keys(
|
|
52
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
53
|
+
values ('f1b00000-0000-4000-8000-000000000001','f1a00000-0000-4000-8000-000000000001',
|
|
54
|
+
'primary', 1, public.test_pubkey('f1-iso'), 'raw_ed25519_base64url',
|
|
55
|
+
public.test_key_fingerprint('f1-iso'), 'active', 'proven');
|
|
56
|
+
|
|
57
|
+
-- Exactly sixteen pending challenges: the key is AT the cap, so the next issue must
|
|
58
|
+
-- evict. created_at is set explicitly so "oldest" and "second-oldest" are unambiguous
|
|
59
|
+
-- rather than depending on insert clock resolution.
|
|
60
|
+
insert into agent_board.agent_signing_key_challenges(
|
|
61
|
+
id, signing_key_id, agent_id, challenge_nonce, challenge_bytes, expires_at, created_at)
|
|
62
|
+
select
|
|
63
|
+
('f1c00000-0000-4000-8000-0000000000' || lpad(i::text, 2, '0'))::uuid,
|
|
64
|
+
'f1b00000-0000-4000-8000-000000000001',
|
|
65
|
+
'f1a00000-0000-4000-8000-000000000001',
|
|
66
|
+
'f1-iso-nonce-' || i,
|
|
67
|
+
'agent-signing-key-proof.v1:f1-iso:' || i,
|
|
68
|
+
now() + interval '5 minutes',
|
|
69
|
+
now() - make_interval(secs => (17 - i))
|
|
70
|
+
from generate_series(1, 16) i;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
teardown
|
|
74
|
+
{
|
|
75
|
+
delete from agent_board.agent_signing_key_challenges
|
|
76
|
+
where agent_id = 'f1a00000-0000-4000-8000-000000000001';
|
|
77
|
+
delete from agent_board.agent_signing_keys
|
|
78
|
+
where agent_id = 'f1a00000-0000-4000-8000-000000000001';
|
|
79
|
+
delete from agent_board.agents where handle like 'f1iso-%';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# mint_fleet_agent_session's two cycle-relevant locks, in its order and modes.
|
|
83
|
+
session "mint"
|
|
84
|
+
step "mint_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
85
|
+
# The OLDEST pending row — the one the eviction would otherwise take.
|
|
86
|
+
step "mint_lock_challenge" {
|
|
87
|
+
select count(*)::int as locked_challenges from (
|
|
88
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
89
|
+
where c.id = 'f1c00000-0000-4000-8000-000000000001' for update) s;
|
|
90
|
+
}
|
|
91
|
+
# ALL sixteen pending rows at once. This is the adversary for the no-evictable-row branch:
|
|
92
|
+
# SKIP LOCKED can only skip, so with every candidate held the eviction finds nothing.
|
|
93
|
+
step "mint_lock_all" {
|
|
94
|
+
select count(*)::int as locked_challenges from (
|
|
95
|
+
select 1 from agent_board.agent_signing_key_challenges c
|
|
96
|
+
where c.signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
97
|
+
and c.proof_state = 'pending'
|
|
98
|
+
and c.consumed_at is null
|
|
99
|
+
order by c.id
|
|
100
|
+
for update) s;
|
|
101
|
+
}
|
|
102
|
+
step "mint_lock_key" {
|
|
103
|
+
select count(*)::int as locked_keys from (
|
|
104
|
+
select 1 from agent_board.agent_signing_keys k
|
|
105
|
+
where k.id = 'f1b00000-0000-4000-8000-000000000001' for update) s;
|
|
106
|
+
}
|
|
107
|
+
step "mint_commit" { COMMIT; }
|
|
108
|
+
|
|
109
|
+
# The real issuer, unmodified, at the cap. Only the deterministic columns are selected:
|
|
110
|
+
# challenge_id and the nonce inside `challenge` are fresh random values per run and must
|
|
111
|
+
# never reach a golden.
|
|
112
|
+
session "issue"
|
|
113
|
+
step "issue_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
114
|
+
step "issue_at_cap" {
|
|
115
|
+
select c.key_id, c.key_version
|
|
116
|
+
from agent_board.issue_signing_key_challenge(
|
|
117
|
+
'f1a00000-0000-4000-8000-000000000001', 'primary', interval '5 minutes') c;
|
|
118
|
+
}
|
|
119
|
+
step "issue_commit" { COMMIT; }
|
|
120
|
+
|
|
121
|
+
session "check"
|
|
122
|
+
# WHICH row was evicted is the observable that distinguishes skipping from waiting: with
|
|
123
|
+
# SKIP LOCKED the mint-held oldest row is untouched and the SECOND-oldest carries the
|
|
124
|
+
# marker. The pending count proves the bound still holds.
|
|
125
|
+
step "check" {
|
|
126
|
+
select
|
|
127
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
128
|
+
where id = 'f1c00000-0000-4000-8000-000000000001') as oldest_state,
|
|
129
|
+
(select proof_state from agent_board.agent_signing_key_challenges
|
|
130
|
+
where id = 'f1c00000-0000-4000-8000-000000000002') as second_oldest_state,
|
|
131
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
132
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
133
|
+
and metadata ? 'superseded_by') as evicted_rows,
|
|
134
|
+
(select count(*)::int from agent_board.agent_signing_key_challenges
|
|
135
|
+
where signing_key_id = 'f1b00000-0000-4000-8000-000000000001'
|
|
136
|
+
and proof_state = 'pending' and consumed_at is null
|
|
137
|
+
and expires_at > now()) as pending_rows;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# 1. THE CYCLE, in the order that would produce the deadlock. mint holds the oldest
|
|
141
|
+
# challenge row; issue then runs at the cap. It must NOT wait (no `<waiting ...>`), and
|
|
142
|
+
# mint's key lock must then be the only thing that blocks.
|
|
143
|
+
permutation "mint_begin" "mint_lock_challenge" "issue_begin" "issue_at_cap" "mint_lock_key" "issue_commit" "mint_commit" "check"
|
|
144
|
+
|
|
145
|
+
# 2. The reverse arrival order: issue arrives FIRST, evicts the true oldest, and mint then
|
|
146
|
+
# blocks on that row and proceeds once issue commits. This is the direction in which the
|
|
147
|
+
# eviction DOES cost a mint a wait, and it pins that the cost is a wait-then-proceed and
|
|
148
|
+
# not a deadlock.
|
|
149
|
+
#
|
|
150
|
+
# `issue_commit` MUST precede `mint_lock_key`. The first version of this permutation put
|
|
151
|
+
# the commit last, which does not test the schema at all: mint blocks on the challenge
|
|
152
|
+
# row, isolationtester will not run a blocked session's next step, and nothing in the
|
|
153
|
+
# permutation can release the lock — so the tester sat out its own 300-second cancel and
|
|
154
|
+
# baked `canceling step mint_lock_challenge after 300 seconds` into the golden. That is a
|
|
155
|
+
# property of the spec's step order, not of the lock order, and it made every iteration
|
|
156
|
+
# cost five minutes.
|
|
157
|
+
permutation "issue_begin" "issue_at_cap" "mint_begin" "mint_lock_challenge" "issue_commit" "mint_lock_key" "mint_commit" "check"
|
|
158
|
+
|
|
159
|
+
# 3. Fully serial: issue commits before mint starts. The baseline the two concurrent
|
|
160
|
+
# permutations are read against.
|
|
161
|
+
permutation "issue_begin" "issue_at_cap" "issue_commit" "mint_begin" "mint_lock_challenge" "mint_lock_key" "mint_commit" "check"
|
|
162
|
+
|
|
163
|
+
# 4. EVERY candidate locked -- the branch that exists only because SKIP LOCKED can skip.
|
|
164
|
+
#
|
|
165
|
+
# With all sixteen pending rows held by another session the eviction's
|
|
166
|
+
# `for update skip locked limit 1` returns NO ROW, so `v_evicted` is null. The function
|
|
167
|
+
# must then REFUSE (challenge_pending_cap) rather than issue: inserting a seventeenth
|
|
168
|
+
# would push the pending set past the bound the cap exists to hold, and the count that
|
|
169
|
+
# said the set was full was correct. This is the ONE path where the pre-change refusal
|
|
170
|
+
# survives, and the PR describes it as "confined to a window in which sixteen mints for
|
|
171
|
+
# one key are in flight at once" -- a claim that was visible in the SQL and asserted
|
|
172
|
+
# nowhere until this permutation.
|
|
173
|
+
#
|
|
174
|
+
# Three things the golden shows, and all three matter:
|
|
175
|
+
# * issue_at_cap ERRORS. It does not wait -- SKIP LOCKED means no `<waiting ...>`.
|
|
176
|
+
# * `evicted_rows = 0`. Nothing was superseded, so no legitimate challenge was
|
|
177
|
+
# destroyed on the way to refusing.
|
|
178
|
+
# * `pending_rows = 16`. No seventeenth row was inserted.
|
|
179
|
+
#
|
|
180
|
+
# MUTATION: delete the `if v_evicted is null then raise` guard and this permutation's
|
|
181
|
+
# golden changes -- the refusal disappears and pending_rows becomes 17, which is the
|
|
182
|
+
# bound being exceeded, i.e. exactly the failure the guard is there to prevent.
|
|
183
|
+
permutation "mint_begin" "mint_lock_all" "issue_begin" "issue_at_cap" "issue_commit" "mint_commit" "check"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Consiliency/message-board#405 — an acquire whose snapshot predates a committed
|
|
2
|
+
# advisory -> inherit revert must NOT bank a full-horizon ownerless lease.
|
|
3
|
+
#
|
|
4
|
+
# WHY ACQUIRE IS THE WORSE HALF OF THIS FAMILY. #406's cap reset clamps the
|
|
5
|
+
# board's EXISTING ownerless claims on a revert. On this path there is no claim
|
|
6
|
+
# yet: the acquire inserts AFTER the revert committed, so there is nothing for
|
|
7
|
+
# the reset to reach even in principle. Whatever horizon a stale 'advisory' read
|
|
8
|
+
# lets through is banked outright.
|
|
9
|
+
#
|
|
10
|
+
# AND IT IS THE PATH THE FIRST DESIGN MISSED. That design said the board lock
|
|
11
|
+
# goes "before the agent-scoped lock". acquire_claim's agent-scoped quota lock
|
|
12
|
+
# lives INSIDE the branch that a stale 'advisory' read SKIPS ENTIRELY, so on the
|
|
13
|
+
# dangerous path it is never reached and ordering against it means nothing. This
|
|
14
|
+
# spec drives exactly that branch: the acquirer reads 'advisory' unless it is
|
|
15
|
+
# made to wait, and if it reads 'advisory' it takes no lock at all.
|
|
16
|
+
#
|
|
17
|
+
# THE INTERLEAVE. w (the board owner) opens a transaction and reverts the board
|
|
18
|
+
# to 'inherit', holding the board-scoped advisory lock until commit. a then asks
|
|
19
|
+
# for a 400-day 'file' lease. Under READ COMMITTED a's snapshot at that moment
|
|
20
|
+
# still shows 'advisory'.
|
|
21
|
+
#
|
|
22
|
+
# WHAT THE FIX PRODUCES (this golden): a's acquire prints `<waiting ...>` on w's
|
|
23
|
+
# lock; after w commits, a RE-READS the policy in a fresh statement, sees
|
|
24
|
+
# 'inherit', and the ownerless TTL cap rejects the 400-day horizon with
|
|
25
|
+
# `ERROR: ownerless claim lease horizon exceeds cap`. The checker sees ZERO
|
|
26
|
+
# claims.
|
|
27
|
+
#
|
|
28
|
+
# MUTATION SIGNAL (verified in the PR): delete the `perform
|
|
29
|
+
# agent_board._claimgate_lock_board_policy(v_board_id)` line from acquire_claim
|
|
30
|
+
# -> a never waits, resolves the stale 'advisory', skips the whole ownerless
|
|
31
|
+
# block and INSERTS. The golden diverges on the missing `<waiting ...>`, on the
|
|
32
|
+
# missing error, and on the checker (1 claim, over-cap lease = t).
|
|
33
|
+
#
|
|
34
|
+
# THE TWIN IS NOT OPTIONAL. A guard that simply over-serialises — or one that
|
|
35
|
+
# resolved every board to 'inherit' — would pass the race permutation and break
|
|
36
|
+
# the feature. The second permutation runs the SAME acquire with no writer at
|
|
37
|
+
# all: the board is still 'advisory', so the 400-day lease must be GRANTED, with
|
|
38
|
+
# no wait anywhere in the trace.
|
|
39
|
+
#
|
|
40
|
+
# Sessions run as `authenticated` (service_role is exempt from these guards
|
|
41
|
+
# entirely, so a service_role probe would prove nothing).
|
|
42
|
+
|
|
43
|
+
setup
|
|
44
|
+
{
|
|
45
|
+
insert into agent_board.claimgate_enforcement_flags(scope_key, enabled) values ('ownerless_guards', true)
|
|
46
|
+
on conflict (scope_key) do update set enabled = true;
|
|
47
|
+
insert into auth.users (id) values ('40500000-0000-4000-8000-0000000000a1');
|
|
48
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
|
|
49
|
+
values ('40500000-0000-4000-8000-000000000001', 'agent', 'agent',
|
|
50
|
+
'cpr-acq-owner', 'CPR Acquire Owner', 'active',
|
|
51
|
+
'40500000-0000-4000-8000-0000000000a1', '40500000-0000-4000-8000-0000000000a1');
|
|
52
|
+
select count(*) from agent_board.create_board(
|
|
53
|
+
'40500000-0000-4000-8000-000000000001',
|
|
54
|
+
'cpr-acq-board', 'CPR Acquire Board',
|
|
55
|
+
'message-board#405 acquire stale-policy probe', jsonb_build_object(), 'private');
|
|
56
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
|
|
57
|
+
values ('40500000-0000-4000-8000-000000000010', 'coordination',
|
|
58
|
+
'40500000-0000-4000-8000-000000000001', 'CPR acquire thread',
|
|
59
|
+
(select id from agent_board.boards where slug = 'cpr-acq-board'));
|
|
60
|
+
-- The board starts OPTED OUT. Written directly rather than through the RPC so the setup does not
|
|
61
|
+
-- depend on the very gate this spec exercises; the RPC's own transitions are BCP's job.
|
|
62
|
+
update agent_board.boards set claim_policy = 'advisory' where slug = 'cpr-acq-board';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
teardown
|
|
66
|
+
{
|
|
67
|
+
delete from agent_board.claims where agent_id = '40500000-0000-4000-8000-000000000001';
|
|
68
|
+
delete from agent_board.threads where id = '40500000-0000-4000-8000-000000000010';
|
|
69
|
+
delete from agent_board.board_membership_events where board_id in (
|
|
70
|
+
select id from agent_board.boards where slug = 'cpr-acq-board');
|
|
71
|
+
delete from agent_board.board_memberships where board_id in (
|
|
72
|
+
select id from agent_board.boards where slug = 'cpr-acq-board');
|
|
73
|
+
delete from agent_board.boards where slug = 'cpr-acq-board';
|
|
74
|
+
delete from agent_board.agents where id = '40500000-0000-4000-8000-000000000001';
|
|
75
|
+
delete from auth.users where id = '40500000-0000-4000-8000-0000000000a1';
|
|
76
|
+
delete from agent_board.claimgate_enforcement_flags where scope_key = 'ownerless_guards';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# w: the board owner reverting the opt-out. Holds the board policy lock until commit.
|
|
80
|
+
session "w"
|
|
81
|
+
step "w_begin" { BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000a1'; }
|
|
82
|
+
step "w_flip" {
|
|
83
|
+
select claim_policy
|
|
84
|
+
from agent_board.set_board_claim_policy(
|
|
85
|
+
(select id from agent_board.boards where slug = 'cpr-acq-board'), 'inherit');
|
|
86
|
+
}
|
|
87
|
+
step "w_commit" { COMMIT; }
|
|
88
|
+
|
|
89
|
+
# a: the in-flight acquire. 400 days is far past the 30-day ownerless TTL cap, so the horizon it
|
|
90
|
+
# gets is a direct readout of which policy it resolved.
|
|
91
|
+
session "a"
|
|
92
|
+
step "a_begin" { BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000a1'; }
|
|
93
|
+
step "a_acq" {
|
|
94
|
+
select resource_key
|
|
95
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-acq-key', now() + interval '400 days',
|
|
96
|
+
null, '40500000-0000-4000-8000-000000000010');
|
|
97
|
+
}
|
|
98
|
+
step "a_commit" { COMMIT; }
|
|
99
|
+
|
|
100
|
+
# checker: a count and a boolean, never a timestamp — a golden with a clock in it is not a golden.
|
|
101
|
+
session "chk"
|
|
102
|
+
step "chk" {
|
|
103
|
+
select count(*)::int as active_claims,
|
|
104
|
+
coalesce(bool_or(lease_expires_at > now() + interval '31 days'), false) as over_cap_lease
|
|
105
|
+
from agent_board.claims
|
|
106
|
+
where agent_id = '40500000-0000-4000-8000-000000000001'
|
|
107
|
+
and resource_kind = 'file'
|
|
108
|
+
and status = 'active'
|
|
109
|
+
and released_at is null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# THE RACE: the acquire is issued while the revert is uncommitted.
|
|
113
|
+
permutation "w_begin" "w_flip" "a_begin" "a_acq" "w_commit" "a_commit" "chk"
|
|
114
|
+
|
|
115
|
+
# THE TWIN: no revert at all. The board is advisory, so the opt-out must still WORK — the 400-day
|
|
116
|
+
# lease is granted and nothing waits. A guard that over-serialises, or one that resolved every
|
|
117
|
+
# board to 'inherit', fails here and passes the permutation above.
|
|
118
|
+
permutation "a_begin" "a_acq" "a_commit" "chk"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Consiliency/message-board#405 — the policy lock is BOARD-SCOPED. Two boards
|
|
2
|
+
# must not serialise against each other.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS IS ITS OWN SPEC. Every other spec in this family passes just as well
|
|
5
|
+
# with a GLOBAL lock key, and a global key would be a fleet-wide serialisation
|
|
6
|
+
# point on the hottest write path in the schema — every ownerless acquire and
|
|
7
|
+
# every renew queued behind every other board's policy flip. "It waits when it
|
|
8
|
+
# should" and "it does not wait when it should not" are different claims, and
|
|
9
|
+
# only the second one fails on a constant key.
|
|
10
|
+
#
|
|
11
|
+
# BOTH DIRECTIONS IN ONE FILE, so the probe is discriminating rather than merely
|
|
12
|
+
# green:
|
|
13
|
+
#
|
|
14
|
+
# permutation 1 (SAME board) — the acquirer must WAIT on the flip.
|
|
15
|
+
# permutation 2 (DIFFERENT board) — the acquirer must NOT wait, and its board
|
|
16
|
+
# is still 'advisory', so its 400-day lease
|
|
17
|
+
# is granted.
|
|
18
|
+
#
|
|
19
|
+
# MUTATION SIGNAL (measured, not predicted): replace the board id in
|
|
20
|
+
# `_claimgate_lock_board_policy`'s key with a constant -> permutation 2 gains a
|
|
21
|
+
# `<waiting ...>` line and the golden diverges. Permutation 1 is UNCHANGED by that
|
|
22
|
+
# mutation, which is exactly why it cannot stand alone.
|
|
23
|
+
#
|
|
24
|
+
# w COMMITS BEFORE a2 in permutation 2, and that ordering is about the mutant rather
|
|
25
|
+
# than the fix: with a constant key and a2 committing first, nothing ever releases the
|
|
26
|
+
# lock a2 is queued on and isolationtester spends its full 300-second cancel timeout
|
|
27
|
+
# before producing the diff. Committing w first leaves the independence claim intact --
|
|
28
|
+
# a2 still runs its whole acquire while w's flip is uncommitted -- and makes the mutant
|
|
29
|
+
# fail in a second.
|
|
30
|
+
|
|
31
|
+
setup
|
|
32
|
+
{
|
|
33
|
+
insert into agent_board.claimgate_enforcement_flags(scope_key, enabled) values ('ownerless_guards', true)
|
|
34
|
+
on conflict (scope_key) do update set enabled = true;
|
|
35
|
+
insert into auth.users (id) values ('40500000-0000-4000-8000-0000000000c1');
|
|
36
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id, owner_user_id)
|
|
37
|
+
values ('40500000-0000-4000-8000-000000000003', 'agent', 'agent',
|
|
38
|
+
'cpr-scope-owner', 'CPR Scope Owner', 'active',
|
|
39
|
+
'40500000-0000-4000-8000-0000000000c1', '40500000-0000-4000-8000-0000000000c1');
|
|
40
|
+
select count(*) from agent_board.create_board(
|
|
41
|
+
'40500000-0000-4000-8000-000000000003', 'cpr-scope-b1', 'CPR Scope B1',
|
|
42
|
+
'message-board#405 board-scope probe (flipped board)', jsonb_build_object(), 'private');
|
|
43
|
+
select count(*) from agent_board.create_board(
|
|
44
|
+
'40500000-0000-4000-8000-000000000003', 'cpr-scope-b2', 'CPR Scope B2',
|
|
45
|
+
'message-board#405 board-scope probe (untouched board)', jsonb_build_object(), 'private');
|
|
46
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, board_id)
|
|
47
|
+
values ('40500000-0000-4000-8000-000000000041', 'coordination',
|
|
48
|
+
'40500000-0000-4000-8000-000000000003', 'CPR scope thread b1',
|
|
49
|
+
(select id from agent_board.boards where slug = 'cpr-scope-b1')),
|
|
50
|
+
('40500000-0000-4000-8000-000000000042', 'coordination',
|
|
51
|
+
'40500000-0000-4000-8000-000000000003', 'CPR scope thread b2',
|
|
52
|
+
(select id from agent_board.boards where slug = 'cpr-scope-b2'));
|
|
53
|
+
update agent_board.boards set claim_policy = 'advisory' where slug in ('cpr-scope-b1', 'cpr-scope-b2');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
teardown
|
|
57
|
+
{
|
|
58
|
+
delete from agent_board.claims where agent_id = '40500000-0000-4000-8000-000000000003';
|
|
59
|
+
delete from agent_board.threads where id in ('40500000-0000-4000-8000-000000000041',
|
|
60
|
+
'40500000-0000-4000-8000-000000000042');
|
|
61
|
+
delete from agent_board.board_membership_events where board_id in (
|
|
62
|
+
select id from agent_board.boards where slug in ('cpr-scope-b1', 'cpr-scope-b2'));
|
|
63
|
+
delete from agent_board.board_memberships where board_id in (
|
|
64
|
+
select id from agent_board.boards where slug in ('cpr-scope-b1', 'cpr-scope-b2'));
|
|
65
|
+
delete from agent_board.boards where slug in ('cpr-scope-b1', 'cpr-scope-b2');
|
|
66
|
+
delete from agent_board.agents where id = '40500000-0000-4000-8000-000000000003';
|
|
67
|
+
delete from auth.users where id = '40500000-0000-4000-8000-0000000000c1';
|
|
68
|
+
delete from agent_board.claimgate_enforcement_flags where scope_key = 'ownerless_guards';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# w reverts board 1, and holds its policy lock until commit.
|
|
72
|
+
session "w"
|
|
73
|
+
step "w_begin" { BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000c1'; }
|
|
74
|
+
step "w_flip" {
|
|
75
|
+
select claim_policy
|
|
76
|
+
from agent_board.set_board_claim_policy(
|
|
77
|
+
(select id from agent_board.boards where slug = 'cpr-scope-b1'), 'inherit');
|
|
78
|
+
}
|
|
79
|
+
step "w_commit" { COMMIT; }
|
|
80
|
+
|
|
81
|
+
# a1 acquires on the FLIPPED board: it must wait, then be refused by the TTL cap.
|
|
82
|
+
session "a1"
|
|
83
|
+
step "a1_begin" { BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000c1'; }
|
|
84
|
+
step "a1_acq" {
|
|
85
|
+
select resource_key
|
|
86
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-scope-key-b1', now() + interval '400 days',
|
|
87
|
+
null, '40500000-0000-4000-8000-000000000041');
|
|
88
|
+
}
|
|
89
|
+
step "a1_commit" { COMMIT; }
|
|
90
|
+
|
|
91
|
+
# a2 acquires on the OTHER board: it must not wait at all, and its opt-out still stands.
|
|
92
|
+
session "a2"
|
|
93
|
+
step "a2_begin" { BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000c1'; }
|
|
94
|
+
step "a2_acq" {
|
|
95
|
+
select resource_key
|
|
96
|
+
from agent_board.acquire_claim(null, 'file', 'cpr-scope-key-b2', now() + interval '400 days',
|
|
97
|
+
null, '40500000-0000-4000-8000-000000000042');
|
|
98
|
+
}
|
|
99
|
+
step "a2_commit" { COMMIT; }
|
|
100
|
+
|
|
101
|
+
session "chk"
|
|
102
|
+
step "chk" {
|
|
103
|
+
select resource_key, (lease_expires_at > now() + interval '31 days') as over_cap_lease
|
|
104
|
+
from agent_board.claims
|
|
105
|
+
where agent_id = '40500000-0000-4000-8000-000000000003'
|
|
106
|
+
and status = 'active' and released_at is null
|
|
107
|
+
order by resource_key;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# SAME BOARD: waits, then the revert's 'inherit' refuses the 400-day horizon.
|
|
111
|
+
permutation "w_begin" "w_flip" "a1_begin" "a1_acq" "w_commit" "a1_commit" "chk"
|
|
112
|
+
|
|
113
|
+
# DIFFERENT BOARD: no wait anywhere, and board 2's opt-out grants the 400-day lease while board 1's
|
|
114
|
+
# flip is still in flight. A constant lock key turns this into a wait and fails the golden.
|
|
115
|
+
permutation "w_begin" "w_flip" "a2_begin" "a2_acq" "w_commit" "a2_commit" "chk"
|