@consiliency/agent-board-schema 1.27.0 → 1.28.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 +4 -0
- package/migrations/20260909000100_agent_board_ledger_revoked_authority.sql +1036 -0
- package/package.json +1 -1
- package/schema-manifest.json +12 -3
- package/src/tooling.test.mjs +2 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +17 -1
- package/tests/agent_board_ledger_authority.test.sql +1010 -0
- package/tests/agent_board_signer_lifecycle.test.sql +6 -0
- package/tests/catalog/p_metadata_parameters.json +8 -0
- package/tests/isolation/expected/ledger_authority_revoke.out +483 -0
- package/tests/isolation/expected/signer_lifecycle_revoke.out +1 -1
- package/tests/isolation/specs/ledger_authority_revoke.spec +290 -0
- package/tests/isolation/specs/signer_lifecycle_revoke.spec +10 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Consiliency/message-board#276 — a board AUTHORITY write reads the fleet revocation ledger
|
|
2
|
+
# under a share lock on the actor's agents row, so a concurrent revoke_fleet_subject('agent')
|
|
3
|
+
# serializes against the write instead of racing it.
|
|
4
|
+
#
|
|
5
|
+
# WHY THIS CANNOT BE A pgTAP FILE. The predicate ("a ledger-revoked grantor is refused") is
|
|
6
|
+
# provable in one transaction and is proved in agent_board_ledger_authority.test.sql. The
|
|
7
|
+
# property THIS file exists for is not the predicate: it is that the read is LOCKED. An
|
|
8
|
+
# unlocked `exists (select 1 from fleet_revocation_events ...)` passes every pgTAP assertion in
|
|
9
|
+
# that file and still leaves the TOCTOU wide open — the authority write reads "not revoked", a
|
|
10
|
+
# revocation commits, and the grant lands anyway. A single rolled-back transaction cannot
|
|
11
|
+
# produce that interleaving. isolationtester prints `<waiting ...>` / `<... completed>` whenever
|
|
12
|
+
# a step blocks, so the lock is a byte in the golden rather than a claim in a comment.
|
|
13
|
+
#
|
|
14
|
+
# THE ADVERSARY IS THE SHIPPED FUNCTION, not a stand-in. `revoke_fleet_subject('agent', X)`
|
|
15
|
+
# (20260901000100:1062) is the exact call the issue is about: it locks the machines X is bound
|
|
16
|
+
# to, then the agents row FOR UPDATE, then the bindings, then writes the agent-subject ledger
|
|
17
|
+
# row. That `agents` FOR UPDATE is what the gate's FOR SHARE conflicts with, and it is the sole
|
|
18
|
+
# reason the two orders below produce different outcomes.
|
|
19
|
+
#
|
|
20
|
+
# BOTH ROUTES ARE DRIVEN, because the whole point of siting the check inside grant_board_role
|
|
21
|
+
# was that the wide `authenticated` door and the narrow signed one could not diverge:
|
|
22
|
+
# ugrant — grant_board_role called directly by the authenticated owner. No gateway.
|
|
23
|
+
# sgrant — signed_grant_board_role called by the service_role gateway, with a decision
|
|
24
|
+
# naming a live, owned, verified key so a refusal can only be #276's gate and
|
|
25
|
+
# never #269's signer revalidation.
|
|
26
|
+
#
|
|
27
|
+
# The check session also reads agents.status: it must stay 'active' throughout. If a future
|
|
28
|
+
# change made fleet revocation flip the status, these permutations would still pass — but for
|
|
29
|
+
# a different reason — and that column in the golden is what makes the substitution visible.
|
|
30
|
+
#
|
|
31
|
+
# MUTATION-VERIFIED: dropping `for share` from _assert_actor_not_fleet_revoked (leaving the
|
|
32
|
+
# ledger read unlocked) changes permutations 2 and 4 — the authority write no longer waits.
|
|
33
|
+
# Clean schema -> byte-identical across 10 runs.
|
|
34
|
+
|
|
35
|
+
setup
|
|
36
|
+
{
|
|
37
|
+
insert into auth.users (id) values
|
|
38
|
+
('27700000-0000-0000-0000-0000000000a1'),
|
|
39
|
+
('27700000-0000-0000-0000-0000000000a2'),
|
|
40
|
+
('27700000-0000-0000-0000-0000000000a3');
|
|
41
|
+
|
|
42
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
|
|
43
|
+
values
|
|
44
|
+
('27710000-0000-4000-8000-000000000001','agent','agent','k276iso-owner','K276 ISO Owner','active','27700000-0000-0000-0000-0000000000a1'),
|
|
45
|
+
('27710000-0000-4000-8000-000000000002','agent','agent','k276iso-grantee','K276 ISO Grantee','active','27700000-0000-0000-0000-0000000000a2'),
|
|
46
|
+
('27710000-0000-4000-8000-000000000003','agent','agent','k276iso-revoker','K276 ISO Revoker','active','27700000-0000-0000-0000-0000000000a3');
|
|
47
|
+
|
|
48
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
49
|
+
values ('27720000-0000-4000-8000-000000000001','k276-iso-board','K276 ISO Board',
|
|
50
|
+
'27710000-0000-4000-8000-000000000001','private','active');
|
|
51
|
+
|
|
52
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
53
|
+
values
|
|
54
|
+
('27720000-0000-4000-8000-000000000001','27710000-0000-4000-8000-000000000001','owner','joined','27710000-0000-4000-8000-000000000001'),
|
|
55
|
+
('27720000-0000-4000-8000-000000000001','27710000-0000-4000-8000-000000000002','member','joined','27710000-0000-4000-8000-000000000001');
|
|
56
|
+
|
|
57
|
+
insert into agent_board.agent_signing_keys(
|
|
58
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
59
|
+
values ('27730000-0000-4000-8000-000000000001','27710000-0000-4000-8000-000000000001',
|
|
60
|
+
'primary', 1, public.test_pubkey('k276-iso'), 'raw_ed25519_base64url',
|
|
61
|
+
public.test_key_fingerprint('k276-iso'), 'active', 'proven');
|
|
62
|
+
|
|
63
|
+
-- A real fleet binding, so revoke_fleet_subject('agent') walks its full path (machine locks,
|
|
64
|
+
-- agents lock, binding revocation, ledger write) rather than a degenerate one.
|
|
65
|
+
insert into agent_board.fleet_trust_domains(id, slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
66
|
+
values ('27740000-0000-4000-8000-000000000001','k276-iso-domain','K276 ISO Domain','tailnet',
|
|
67
|
+
'27710000-0000-4000-8000-000000000003','active', now());
|
|
68
|
+
insert into agent_board.fleet_machines(id, trust_domain_id, owner_agent_id, machine_label, machine_fingerprint, lifecycle_state, enrolled_at)
|
|
69
|
+
values ('27750000-0000-4000-8000-000000000001','27740000-0000-4000-8000-000000000001',
|
|
70
|
+
'27710000-0000-4000-8000-000000000003','k276-iso-machine','fp-k276-iso','active', now());
|
|
71
|
+
insert into agent_board.fleet_machine_agents(id, machine_id, agent_id, agent_role, lifecycle_state)
|
|
72
|
+
values ('27760000-0000-4000-8000-000000000001','27750000-0000-4000-8000-000000000001',
|
|
73
|
+
'27710000-0000-4000-8000-000000000001','phase_worker','active');
|
|
74
|
+
|
|
75
|
+
-- An OUTSTANDING admin link, modelled as a pre-existing row because that is what it is: a
|
|
76
|
+
-- link minted before its creator was revoked, which the mint gate structurally cannot reach.
|
|
77
|
+
-- Seeded rather than minted through create_invite_link for two reasons, both mechanical: the
|
|
78
|
+
-- generated code is random (a golden must not contain one), and board_invite_tokens carries
|
|
79
|
+
-- a CREATOR-ONLY read policy (board_invite_tokens_creator_read), so a redeem session running
|
|
80
|
+
-- as `authenticated` cannot look the code up -- it holds the code out of band, as a real
|
|
81
|
+
-- redeemer does. An earlier revision of this spec did look it up and every redemption failed
|
|
82
|
+
-- 'invite is not usable' on a null code, which the uniform error made indistinguishable from
|
|
83
|
+
-- the gate firing. The code is a literal here for exactly that reason.
|
|
84
|
+
insert into agent_board.board_invite_tokens(
|
|
85
|
+
code, board_id, preset_role, preset_membership_state, max_uses, expires_at, created_by_agent_id)
|
|
86
|
+
values ('k276isolink', '27720000-0000-4000-8000-000000000001', 'admin', 'joined', 5,
|
|
87
|
+
now() + interval '2 days', '27710000-0000-4000-8000-000000000001');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
teardown
|
|
91
|
+
{
|
|
92
|
+
delete from agent_board.signature_evidence
|
|
93
|
+
where agent_id in (select id from agent_board.agents where handle like 'k276iso-%');
|
|
94
|
+
delete from agent_board.board_invite_redemptions
|
|
95
|
+
where invite_id in (select id from agent_board.board_invite_tokens
|
|
96
|
+
where board_id = '27720000-0000-4000-8000-000000000001');
|
|
97
|
+
delete from agent_board.board_invite_tokens
|
|
98
|
+
where board_id = '27720000-0000-4000-8000-000000000001';
|
|
99
|
+
delete from agent_board.board_membership_events
|
|
100
|
+
where board_id = '27720000-0000-4000-8000-000000000001';
|
|
101
|
+
delete from agent_board.fleet_revocation_events
|
|
102
|
+
where agent_id in (select id from agent_board.agents where handle like 'k276iso-%')
|
|
103
|
+
or subject_id in (select id from agent_board.agents where handle like 'k276iso-%')
|
|
104
|
+
or revoked_by_agent_id in (select id from agent_board.agents where handle like 'k276iso-%');
|
|
105
|
+
delete from agent_board.fleet_machine_agents where id='27760000-0000-4000-8000-000000000001';
|
|
106
|
+
delete from agent_board.fleet_machines where id='27750000-0000-4000-8000-000000000001';
|
|
107
|
+
delete from agent_board.fleet_trust_domains where id='27740000-0000-4000-8000-000000000001';
|
|
108
|
+
delete from agent_board.agent_signing_keys
|
|
109
|
+
where agent_id in (select id from agent_board.agents where handle like 'k276iso-%');
|
|
110
|
+
delete from agent_board.board_authority_ceilings where board_id='27720000-0000-4000-8000-000000000001';
|
|
111
|
+
delete from agent_board.board_memberships where board_id='27720000-0000-4000-8000-000000000001';
|
|
112
|
+
delete from agent_board.boards where id='27720000-0000-4000-8000-000000000001';
|
|
113
|
+
delete from agent_board.agents where handle like 'k276iso-%';
|
|
114
|
+
delete from auth.users where id in (
|
|
115
|
+
'27700000-0000-0000-0000-0000000000a1',
|
|
116
|
+
'27700000-0000-0000-0000-0000000000a2',
|
|
117
|
+
'27700000-0000-0000-0000-0000000000a3');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# The WIDE door: the authenticated owner calling the chokepoint directly, no gateway involved.
|
|
121
|
+
session "ugrant"
|
|
122
|
+
step "ugrant_begin" {
|
|
123
|
+
BEGIN;
|
|
124
|
+
SET LOCAL ROLE authenticated;
|
|
125
|
+
SET LOCAL "request.jwt.claim.role" = 'authenticated';
|
|
126
|
+
SET LOCAL "request.jwt.claim.sub" = '27700000-0000-0000-0000-0000000000a1';
|
|
127
|
+
}
|
|
128
|
+
step "ugrant_grant" {
|
|
129
|
+
select g.role as granted_role
|
|
130
|
+
from agent_board.grant_board_role(
|
|
131
|
+
'27720000-0000-4000-8000-000000000001',
|
|
132
|
+
'27710000-0000-4000-8000-000000000002',
|
|
133
|
+
'admin') g;
|
|
134
|
+
}
|
|
135
|
+
step "ugrant_commit" { COMMIT; }
|
|
136
|
+
|
|
137
|
+
# The NARROW door. Only the deterministic column is selected -- signature_evidence_id is a
|
|
138
|
+
# fresh uuid and a golden must not contain a random value.
|
|
139
|
+
session "sgrant"
|
|
140
|
+
step "sgrant_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
141
|
+
step "sgrant_grant" {
|
|
142
|
+
select w.role as granted_role
|
|
143
|
+
from agent_board.signed_grant_board_role(
|
|
144
|
+
jsonb_build_object(
|
|
145
|
+
'p_board_id', '27720000-0000-4000-8000-000000000001',
|
|
146
|
+
'p_agent_id', '27710000-0000-4000-8000-000000000002',
|
|
147
|
+
'p_grantee_role', 'admin'),
|
|
148
|
+
jsonb_build_object(
|
|
149
|
+
'agent_id', '27710000-0000-4000-8000-000000000001',
|
|
150
|
+
'signing_key_id', '27730000-0000-4000-8000-000000000001',
|
|
151
|
+
'key_id', 'primary',
|
|
152
|
+
'key_version', 1,
|
|
153
|
+
'algorithm', 'Ed25519',
|
|
154
|
+
'verification_status', 'verified',
|
|
155
|
+
'payload_digest', 'sha256:k276',
|
|
156
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
157
|
+
}
|
|
158
|
+
step "sgrant_commit" { COMMIT; }
|
|
159
|
+
|
|
160
|
+
session "revoke"
|
|
161
|
+
step "revoke_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
162
|
+
step "revoke_agent" {
|
|
163
|
+
select subject_kind
|
|
164
|
+
from agent_board.revoke_fleet_subject(
|
|
165
|
+
'agent',
|
|
166
|
+
'27710000-0000-4000-8000-000000000001',
|
|
167
|
+
'27710000-0000-4000-8000-000000000003',
|
|
168
|
+
'k276 iso fleet revocation');
|
|
169
|
+
}
|
|
170
|
+
step "revoke_commit" { COMMIT; }
|
|
171
|
+
|
|
172
|
+
# The invite MINT half (#276 G3). Locked like the three chokepoints: the actor IS the principal
|
|
173
|
+
# whose authority is being spent, since an admin-preset link is a deferred grant of admin.
|
|
174
|
+
session "mint"
|
|
175
|
+
step "mint_begin" {
|
|
176
|
+
BEGIN;
|
|
177
|
+
SET LOCAL ROLE authenticated;
|
|
178
|
+
SET LOCAL "request.jwt.claim.role" = 'authenticated';
|
|
179
|
+
SET LOCAL "request.jwt.claim.sub" = '27700000-0000-0000-0000-0000000000a1';
|
|
180
|
+
}
|
|
181
|
+
step "mint_link" {
|
|
182
|
+
select m.preset_role
|
|
183
|
+
from agent_board.create_invite_link(
|
|
184
|
+
'27720000-0000-4000-8000-000000000001', 'admin', 'joined', 5,
|
|
185
|
+
now() + interval '2 days') m;
|
|
186
|
+
}
|
|
187
|
+
step "mint_commit" { COMMIT; }
|
|
188
|
+
|
|
189
|
+
# The invite REDEEM half. DELIBERATELY UNLOCKED -- every way of row-locking the creator here
|
|
190
|
+
# closes a cycle (enroll_federated_identity reaches this function holding a boards lock; see the
|
|
191
|
+
# migration header). This session exists to record the ACCEPTED window as bytes rather than
|
|
192
|
+
# prose: with the revocation in flight and uncommitted, the redemption does NOT block and
|
|
193
|
+
# succeeds on its own snapshot. Permutation 6 then shows the very next redemption refused.
|
|
194
|
+
session "redeem"
|
|
195
|
+
step "redeem_begin" {
|
|
196
|
+
BEGIN;
|
|
197
|
+
SET LOCAL ROLE authenticated;
|
|
198
|
+
SET LOCAL "request.jwt.claim.role" = 'authenticated';
|
|
199
|
+
SET LOCAL "request.jwt.claim.sub" = '27700000-0000-0000-0000-0000000000a2';
|
|
200
|
+
}
|
|
201
|
+
step "redeem_link" {
|
|
202
|
+
select r.role as redeemed_role
|
|
203
|
+
from agent_board.redeem_invite_link('k276isolink') r;
|
|
204
|
+
}
|
|
205
|
+
step "redeem_commit" { COMMIT; }
|
|
206
|
+
|
|
207
|
+
# A SECOND redeemer of the SAME token. Exists to bound the accepted window: two redemptions of
|
|
208
|
+
# one token cannot both sit between their ledger read and their commit, because the token
|
|
209
|
+
# FOR UPDATE is the redeem path's first statement. Permutation 8 drives exactly that.
|
|
210
|
+
session "redeem2"
|
|
211
|
+
step "redeem2_begin" {
|
|
212
|
+
BEGIN;
|
|
213
|
+
SET LOCAL ROLE authenticated;
|
|
214
|
+
SET LOCAL "request.jwt.claim.role" = 'authenticated';
|
|
215
|
+
SET LOCAL "request.jwt.claim.sub" = '27700000-0000-0000-0000-0000000000a3';
|
|
216
|
+
}
|
|
217
|
+
step "redeem2_link" {
|
|
218
|
+
select r.role as redeemed_role
|
|
219
|
+
from agent_board.redeem_invite_link('k276isolink') r;
|
|
220
|
+
}
|
|
221
|
+
step "redeem2_commit" { COMMIT; }
|
|
222
|
+
|
|
223
|
+
session "check"
|
|
224
|
+
step "check" {
|
|
225
|
+
select
|
|
226
|
+
(select role from agent_board.board_memberships
|
|
227
|
+
where board_id = '27720000-0000-4000-8000-000000000001'
|
|
228
|
+
and agent_id = '27710000-0000-4000-8000-000000000002') as grantee_role,
|
|
229
|
+
(select status from agent_board.agents
|
|
230
|
+
where id = '27710000-0000-4000-8000-000000000001') as owner_status,
|
|
231
|
+
(select count(*) from agent_board.fleet_revocation_events
|
|
232
|
+
where subject_kind = 'agent'
|
|
233
|
+
and subject_id = '27710000-0000-4000-8000-000000000001') as agent_ledger_rows,
|
|
234
|
+
(select count(*) from agent_board.fleet_machine_agents
|
|
235
|
+
where agent_id = '27710000-0000-4000-8000-000000000001'
|
|
236
|
+
and lifecycle_state = 'active') as active_bindings,
|
|
237
|
+
(select count(*) from agent_board.board_invite_tokens
|
|
238
|
+
where board_id = '27720000-0000-4000-8000-000000000001'
|
|
239
|
+
and code <> 'k276isolink') as minted_tokens,
|
|
240
|
+
(select uses_count from agent_board.board_invite_tokens
|
|
241
|
+
where code = 'k276isolink') as outstanding_link_uses;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# 1. Write first (UNSIGNED). The revocation must WAIT on the share-locked agents row rather
|
|
245
|
+
# than committing underneath a grant that has already been authorized. The grant stands and
|
|
246
|
+
# the revocation lands after it -- a grant that beat the revocation, which is the same
|
|
247
|
+
# outcome as a grant one millisecond earlier.
|
|
248
|
+
permutation "ugrant_begin" "ugrant_grant" "revoke_begin" "revoke_agent" "ugrant_commit" "revoke_commit" "check"
|
|
249
|
+
|
|
250
|
+
# 2. THE DEFECT'S OWN INTERLEAVING, unsigned route. The revocation is in flight when the
|
|
251
|
+
# authority write runs; the write blocks on the agents row, then observes the committed
|
|
252
|
+
# revocation and fails closed. Before #276 this permutation COMPLETED the grant: the
|
|
253
|
+
# chokepoint read no ledger at all, so there was nothing to block on and nothing to see.
|
|
254
|
+
permutation "revoke_begin" "revoke_agent" "ugrant_begin" "ugrant_grant" "revoke_commit" "ugrant_commit" "check"
|
|
255
|
+
|
|
256
|
+
# 3. Write first, SIGNED route. Same property through the gateway wrapper, which reaches the
|
|
257
|
+
# gate only because the check lives inside the inner chokepoint.
|
|
258
|
+
permutation "sgrant_begin" "sgrant_grant" "revoke_begin" "revoke_agent" "sgrant_commit" "revoke_commit" "check"
|
|
259
|
+
|
|
260
|
+
# 4. The defect's interleaving through the SIGNED route. #269 already made this wrapper block
|
|
261
|
+
# on the agents row for a STATUS change; this permutation is what proves it also fails
|
|
262
|
+
# closed on a LEDGER revocation, which leaves agents.status untouched.
|
|
263
|
+
permutation "revoke_begin" "revoke_agent" "sgrant_begin" "sgrant_grant" "revoke_commit" "sgrant_commit" "check"
|
|
264
|
+
|
|
265
|
+
# 5. The MINT half, revoke-in-flight. The mint blocks on the creator's agents row, then observes
|
|
266
|
+
# the committed revocation and fails closed -- and leaves no token, so no code escapes. This
|
|
267
|
+
# is the permutation the FOR SHARE in _assert_actor_not_fleet_revoked exists for on this path.
|
|
268
|
+
permutation "revoke_begin" "revoke_agent" "mint_begin" "mint_link" "revoke_commit" "mint_commit" "check"
|
|
269
|
+
|
|
270
|
+
# 6. THE ACCEPTED WINDOW, recorded rather than described. The outstanding link exists, the
|
|
271
|
+
# revocation is IN FLIGHT and uncommitted, and the redemption does NOT block -- it reads its
|
|
272
|
+
# own snapshot and succeeds. That is the TOCTOU this migration deliberately accepts on the
|
|
273
|
+
# redeem path, because every row lock that would close it closes a deadlock cycle instead
|
|
274
|
+
# (see the header). The check row shows the use consumed; the pgTAP file shows the NEXT
|
|
275
|
+
# redemption, after the revocation commits, refused.
|
|
276
|
+
permutation "revoke_begin" "revoke_agent" "redeem_begin" "redeem_link" "redeem_commit" "revoke_commit" "check"
|
|
277
|
+
|
|
278
|
+
# 7. ...and the same interleaving with the revocation COMMITTED first: now the redemption is
|
|
279
|
+
# refused. The pair is what makes permutation 6 a bounded window rather than an open door.
|
|
280
|
+
permutation "revoke_begin" "revoke_agent" "revoke_commit" "redeem_begin" "redeem_link" "redeem_commit" "check"
|
|
281
|
+
|
|
282
|
+
# 8. THE RESIDUAL BOUND, driven rather than argued. Two redeemers of the SAME token with the
|
|
283
|
+
# revocation in flight: the first reads the ledger before the revocation commits and slips
|
|
284
|
+
# through; the second BLOCKS on the token row -- board_invite_tokens FOR UPDATE is the redeem
|
|
285
|
+
# path's first statement -- and by the time it proceeds the revocation has committed, so its
|
|
286
|
+
# own ledger read refuses it. That is why the disclosure says "at most one redemption per
|
|
287
|
+
# outstanding token" and not "one redemption": tokens serialise redemptions of themselves,
|
|
288
|
+
# but a creator holding K links has K independent windows. outstanding_link_uses = 1 in the
|
|
289
|
+
# check row is the byte that says exactly one slipped.
|
|
290
|
+
permutation "revoke_begin" "revoke_agent" "redeem_begin" "redeem_link" "redeem2_begin" "redeem2_link" "revoke_commit" "redeem_commit" "redeem2_commit" "check"
|
|
@@ -39,6 +39,16 @@
|
|
|
39
39
|
# its MODE observable. The table's UPDATE privilege is the
|
|
40
40
|
# owner's alone (service_role has SELECT), so this models
|
|
41
41
|
# owner-level incident DML and any future key-only writer.
|
|
42
|
+
#
|
|
43
|
+
# GOLDEN CHANGE IN #276, recorded so it is not read as a #269 regression. Permutation 2's
|
|
44
|
+
# refusal line moved from `signer_not_active` to `fleet_revoked`. `revoke_agent_subtree`'s
|
|
45
|
+
# primitive produces BOTH conditions at once — it flips agents.status AND writes an
|
|
46
|
+
# agent-subject fleet_revocation_events row — and #276's gate, which lives inside
|
|
47
|
+
# grant_board_role, now runs before _assert_signed_authority_signer_live and wins the race to
|
|
48
|
+
# raise. The blocking behaviour this permutation exists for is unchanged (`<waiting ...>` /
|
|
49
|
+
# `<... completed>` are byte-identical), and `signer_not_active` is still exercised
|
|
50
|
+
# EXCLUSIVELY by permutations 3 and 4, whose `suspend` session touches only agents.status and
|
|
51
|
+
# writes no ledger row. Nothing here stopped being pinned.
|
|
42
52
|
|
|
43
53
|
setup
|
|
44
54
|
{
|