@consiliency/agent-board-schema 1.24.0 → 1.27.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 +12 -0
- package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
- package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
- package/migrations/20260908000100_agent_board_signer_lifecycle_revalidation.sql +451 -0
- package/package.json +1 -1
- package/schema-manifest.json +30 -3
- package/src/tooling.test.mjs +3 -3
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +129 -112
- package/tests/agent_board_claimkey.test.sql +25 -3
- package/tests/agent_board_consent_emission.test.sql +17 -13
- package/tests/agent_board_consent_gating.test.sql +55 -11
- package/tests/agent_board_delegation_policies.test.sql +3 -1
- package/tests/agent_board_enroll_resumption.test.sql +412 -0
- package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
- package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
- package/tests/agent_board_mint_harden.test.sql +39 -4
- package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
- package/tests/agent_board_rejectsem.test.sql +1 -1
- package/tests/agent_board_rolereset.test.sql +23 -2
- package/tests/agent_board_rowid.test.sql +24 -13
- package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
- package/tests/agent_board_signed_authority_provenance.test.sql +477 -0
- package/tests/agent_board_signer_lifecycle.test.sql +709 -0
- package/tests/agent_board_source_strip_helper.test.sql +285 -0
- package/tests/agent_board_wake_recovery.test.sql +58 -40
- package/tests/catalog/p_metadata_parameters.json +693 -0
- package/tests/fixtures/test-prereqs.sql +313 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
- package/tests/isolation/expected/signed_grant_onboarding_deadlock.out +220 -0
- package/tests/isolation/expected/signer_lifecycle_revoke.out +307 -0
- package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +159 -0
- package/tests/isolation/specs/signer_lifecycle_revoke.spec +176 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Parsed test spec with 5 sessions
|
|
2
|
+
|
|
3
|
+
starting permutation: auth_begin auth_grant revoke_begin revoke_signer auth_commit revoke_commit check
|
|
4
|
+
step auth_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
5
|
+
step auth_grant:
|
|
6
|
+
select w.role as granted_role
|
|
7
|
+
from agent_board.signed_grant_board_role(
|
|
8
|
+
jsonb_build_object(
|
|
9
|
+
'p_board_id', '26950000-0000-4000-8000-000000000001',
|
|
10
|
+
'p_agent_id', '26940000-0000-4000-8000-000000000002',
|
|
11
|
+
'p_grantee_role', 'admin'),
|
|
12
|
+
jsonb_build_object(
|
|
13
|
+
'agent_id', '26940000-0000-4000-8000-000000000001',
|
|
14
|
+
'signing_key_id', '26960000-0000-4000-8000-000000000001',
|
|
15
|
+
'key_id', 'primary',
|
|
16
|
+
'key_version', 1,
|
|
17
|
+
'algorithm', 'Ed25519',
|
|
18
|
+
'verification_status', 'verified',
|
|
19
|
+
'payload_digest', 'sha256:k269',
|
|
20
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
21
|
+
|
|
22
|
+
granted_role
|
|
23
|
+
------------
|
|
24
|
+
admin
|
|
25
|
+
(1 row)
|
|
26
|
+
|
|
27
|
+
step revoke_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
28
|
+
step revoke_signer:
|
|
29
|
+
select revoked_agent_count, revoked_key_count
|
|
30
|
+
from agent_board.revoke_agent_subtree(
|
|
31
|
+
'26940000-0000-4000-8000-000000000001',
|
|
32
|
+
'26940000-0000-4000-8000-000000000003',
|
|
33
|
+
'k269 iso revoke');
|
|
34
|
+
<waiting ...>
|
|
35
|
+
step auth_commit: COMMIT;
|
|
36
|
+
step revoke_signer: <... completed>
|
|
37
|
+
revoked_agent_count|revoked_key_count
|
|
38
|
+
-------------------+-----------------
|
|
39
|
+
1| 1
|
|
40
|
+
(1 row)
|
|
41
|
+
|
|
42
|
+
step revoke_commit: COMMIT;
|
|
43
|
+
step check:
|
|
44
|
+
select
|
|
45
|
+
(select role from agent_board.board_memberships
|
|
46
|
+
where board_id = '26950000-0000-4000-8000-000000000001'
|
|
47
|
+
and agent_id = '26940000-0000-4000-8000-000000000002') as grantee_role,
|
|
48
|
+
(select status from agent_board.agents
|
|
49
|
+
where id = '26940000-0000-4000-8000-000000000001') as signer_status,
|
|
50
|
+
(select status from agent_board.agent_signing_keys
|
|
51
|
+
where id = '26960000-0000-4000-8000-000000000001') as key_status,
|
|
52
|
+
(select count(*) from agent_board.signature_evidence
|
|
53
|
+
where operation = 'grant_board_role') as evidence_rows;
|
|
54
|
+
|
|
55
|
+
grantee_role|signer_status|key_status|evidence_rows
|
|
56
|
+
------------+-------------+----------+-------------
|
|
57
|
+
admin |revoked |revoked | 1
|
|
58
|
+
(1 row)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
starting permutation: revoke_begin revoke_signer auth_begin auth_grant revoke_commit auth_commit check
|
|
62
|
+
step revoke_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
63
|
+
step revoke_signer:
|
|
64
|
+
select revoked_agent_count, revoked_key_count
|
|
65
|
+
from agent_board.revoke_agent_subtree(
|
|
66
|
+
'26940000-0000-4000-8000-000000000001',
|
|
67
|
+
'26940000-0000-4000-8000-000000000003',
|
|
68
|
+
'k269 iso revoke');
|
|
69
|
+
|
|
70
|
+
revoked_agent_count|revoked_key_count
|
|
71
|
+
-------------------+-----------------
|
|
72
|
+
1| 1
|
|
73
|
+
(1 row)
|
|
74
|
+
|
|
75
|
+
step auth_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
76
|
+
step auth_grant:
|
|
77
|
+
select w.role as granted_role
|
|
78
|
+
from agent_board.signed_grant_board_role(
|
|
79
|
+
jsonb_build_object(
|
|
80
|
+
'p_board_id', '26950000-0000-4000-8000-000000000001',
|
|
81
|
+
'p_agent_id', '26940000-0000-4000-8000-000000000002',
|
|
82
|
+
'p_grantee_role', 'admin'),
|
|
83
|
+
jsonb_build_object(
|
|
84
|
+
'agent_id', '26940000-0000-4000-8000-000000000001',
|
|
85
|
+
'signing_key_id', '26960000-0000-4000-8000-000000000001',
|
|
86
|
+
'key_id', 'primary',
|
|
87
|
+
'key_version', 1,
|
|
88
|
+
'algorithm', 'Ed25519',
|
|
89
|
+
'verification_status', 'verified',
|
|
90
|
+
'payload_digest', 'sha256:k269',
|
|
91
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
92
|
+
<waiting ...>
|
|
93
|
+
step revoke_commit: COMMIT;
|
|
94
|
+
step auth_grant: <... completed>
|
|
95
|
+
ERROR: signer_not_active: signed authority write signer is revoked, not active
|
|
96
|
+
step auth_commit: COMMIT;
|
|
97
|
+
step check:
|
|
98
|
+
select
|
|
99
|
+
(select role from agent_board.board_memberships
|
|
100
|
+
where board_id = '26950000-0000-4000-8000-000000000001'
|
|
101
|
+
and agent_id = '26940000-0000-4000-8000-000000000002') as grantee_role,
|
|
102
|
+
(select status from agent_board.agents
|
|
103
|
+
where id = '26940000-0000-4000-8000-000000000001') as signer_status,
|
|
104
|
+
(select status from agent_board.agent_signing_keys
|
|
105
|
+
where id = '26960000-0000-4000-8000-000000000001') as key_status,
|
|
106
|
+
(select count(*) from agent_board.signature_evidence
|
|
107
|
+
where operation = 'grant_board_role') as evidence_rows;
|
|
108
|
+
|
|
109
|
+
grantee_role|signer_status|key_status|evidence_rows
|
|
110
|
+
------------+-------------+----------+-------------
|
|
111
|
+
member |revoked |revoked | 0
|
|
112
|
+
(1 row)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
starting permutation: suspend_begin suspend_signer auth_begin auth_grant suspend_commit auth_commit check
|
|
116
|
+
step suspend_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
117
|
+
step suspend_signer:
|
|
118
|
+
update agent_board.agents set status = 'suspended', updated_at = now()
|
|
119
|
+
where id = '26940000-0000-4000-8000-000000000001';
|
|
120
|
+
|
|
121
|
+
step auth_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
122
|
+
step auth_grant:
|
|
123
|
+
select w.role as granted_role
|
|
124
|
+
from agent_board.signed_grant_board_role(
|
|
125
|
+
jsonb_build_object(
|
|
126
|
+
'p_board_id', '26950000-0000-4000-8000-000000000001',
|
|
127
|
+
'p_agent_id', '26940000-0000-4000-8000-000000000002',
|
|
128
|
+
'p_grantee_role', 'admin'),
|
|
129
|
+
jsonb_build_object(
|
|
130
|
+
'agent_id', '26940000-0000-4000-8000-000000000001',
|
|
131
|
+
'signing_key_id', '26960000-0000-4000-8000-000000000001',
|
|
132
|
+
'key_id', 'primary',
|
|
133
|
+
'key_version', 1,
|
|
134
|
+
'algorithm', 'Ed25519',
|
|
135
|
+
'verification_status', 'verified',
|
|
136
|
+
'payload_digest', 'sha256:k269',
|
|
137
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
138
|
+
<waiting ...>
|
|
139
|
+
step suspend_commit: COMMIT;
|
|
140
|
+
step auth_grant: <... completed>
|
|
141
|
+
ERROR: signer_not_active: signed authority write signer is suspended, not active
|
|
142
|
+
step auth_commit: COMMIT;
|
|
143
|
+
step check:
|
|
144
|
+
select
|
|
145
|
+
(select role from agent_board.board_memberships
|
|
146
|
+
where board_id = '26950000-0000-4000-8000-000000000001'
|
|
147
|
+
and agent_id = '26940000-0000-4000-8000-000000000002') as grantee_role,
|
|
148
|
+
(select status from agent_board.agents
|
|
149
|
+
where id = '26940000-0000-4000-8000-000000000001') as signer_status,
|
|
150
|
+
(select status from agent_board.agent_signing_keys
|
|
151
|
+
where id = '26960000-0000-4000-8000-000000000001') as key_status,
|
|
152
|
+
(select count(*) from agent_board.signature_evidence
|
|
153
|
+
where operation = 'grant_board_role') as evidence_rows;
|
|
154
|
+
|
|
155
|
+
grantee_role|signer_status|key_status|evidence_rows
|
|
156
|
+
------------+-------------+----------+-------------
|
|
157
|
+
member |suspended |active | 0
|
|
158
|
+
(1 row)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
starting permutation: auth_begin auth_grant suspend_begin suspend_signer auth_commit suspend_commit check
|
|
162
|
+
step auth_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
163
|
+
step auth_grant:
|
|
164
|
+
select w.role as granted_role
|
|
165
|
+
from agent_board.signed_grant_board_role(
|
|
166
|
+
jsonb_build_object(
|
|
167
|
+
'p_board_id', '26950000-0000-4000-8000-000000000001',
|
|
168
|
+
'p_agent_id', '26940000-0000-4000-8000-000000000002',
|
|
169
|
+
'p_grantee_role', 'admin'),
|
|
170
|
+
jsonb_build_object(
|
|
171
|
+
'agent_id', '26940000-0000-4000-8000-000000000001',
|
|
172
|
+
'signing_key_id', '26960000-0000-4000-8000-000000000001',
|
|
173
|
+
'key_id', 'primary',
|
|
174
|
+
'key_version', 1,
|
|
175
|
+
'algorithm', 'Ed25519',
|
|
176
|
+
'verification_status', 'verified',
|
|
177
|
+
'payload_digest', 'sha256:k269',
|
|
178
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
179
|
+
|
|
180
|
+
granted_role
|
|
181
|
+
------------
|
|
182
|
+
admin
|
|
183
|
+
(1 row)
|
|
184
|
+
|
|
185
|
+
step suspend_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
186
|
+
step suspend_signer:
|
|
187
|
+
update agent_board.agents set status = 'suspended', updated_at = now()
|
|
188
|
+
where id = '26940000-0000-4000-8000-000000000001';
|
|
189
|
+
<waiting ...>
|
|
190
|
+
step auth_commit: COMMIT;
|
|
191
|
+
step suspend_signer: <... completed>
|
|
192
|
+
step suspend_commit: COMMIT;
|
|
193
|
+
step check:
|
|
194
|
+
select
|
|
195
|
+
(select role from agent_board.board_memberships
|
|
196
|
+
where board_id = '26950000-0000-4000-8000-000000000001'
|
|
197
|
+
and agent_id = '26940000-0000-4000-8000-000000000002') as grantee_role,
|
|
198
|
+
(select status from agent_board.agents
|
|
199
|
+
where id = '26940000-0000-4000-8000-000000000001') as signer_status,
|
|
200
|
+
(select status from agent_board.agent_signing_keys
|
|
201
|
+
where id = '26960000-0000-4000-8000-000000000001') as key_status,
|
|
202
|
+
(select count(*) from agent_board.signature_evidence
|
|
203
|
+
where operation = 'grant_board_role') as evidence_rows;
|
|
204
|
+
|
|
205
|
+
grantee_role|signer_status|key_status|evidence_rows
|
|
206
|
+
------------+-------------+----------+-------------
|
|
207
|
+
admin |suspended |active | 1
|
|
208
|
+
(1 row)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
starting permutation: auth_begin auth_grant keyrevoke_begin keyrevoke_key auth_commit keyrevoke_commit check
|
|
212
|
+
step auth_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
213
|
+
step auth_grant:
|
|
214
|
+
select w.role as granted_role
|
|
215
|
+
from agent_board.signed_grant_board_role(
|
|
216
|
+
jsonb_build_object(
|
|
217
|
+
'p_board_id', '26950000-0000-4000-8000-000000000001',
|
|
218
|
+
'p_agent_id', '26940000-0000-4000-8000-000000000002',
|
|
219
|
+
'p_grantee_role', 'admin'),
|
|
220
|
+
jsonb_build_object(
|
|
221
|
+
'agent_id', '26940000-0000-4000-8000-000000000001',
|
|
222
|
+
'signing_key_id', '26960000-0000-4000-8000-000000000001',
|
|
223
|
+
'key_id', 'primary',
|
|
224
|
+
'key_version', 1,
|
|
225
|
+
'algorithm', 'Ed25519',
|
|
226
|
+
'verification_status', 'verified',
|
|
227
|
+
'payload_digest', 'sha256:k269',
|
|
228
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
229
|
+
|
|
230
|
+
granted_role
|
|
231
|
+
------------
|
|
232
|
+
admin
|
|
233
|
+
(1 row)
|
|
234
|
+
|
|
235
|
+
step keyrevoke_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
236
|
+
step keyrevoke_key:
|
|
237
|
+
update agent_board.agent_signing_keys
|
|
238
|
+
set status = 'revoked', proof_state = 'revoked', revoked_at = now(), updated_at = now()
|
|
239
|
+
where id = '26960000-0000-4000-8000-000000000001';
|
|
240
|
+
<waiting ...>
|
|
241
|
+
step auth_commit: COMMIT;
|
|
242
|
+
step keyrevoke_key: <... completed>
|
|
243
|
+
step keyrevoke_commit: COMMIT;
|
|
244
|
+
step check:
|
|
245
|
+
select
|
|
246
|
+
(select role from agent_board.board_memberships
|
|
247
|
+
where board_id = '26950000-0000-4000-8000-000000000001'
|
|
248
|
+
and agent_id = '26940000-0000-4000-8000-000000000002') as grantee_role,
|
|
249
|
+
(select status from agent_board.agents
|
|
250
|
+
where id = '26940000-0000-4000-8000-000000000001') as signer_status,
|
|
251
|
+
(select status from agent_board.agent_signing_keys
|
|
252
|
+
where id = '26960000-0000-4000-8000-000000000001') as key_status,
|
|
253
|
+
(select count(*) from agent_board.signature_evidence
|
|
254
|
+
where operation = 'grant_board_role') as evidence_rows;
|
|
255
|
+
|
|
256
|
+
grantee_role|signer_status|key_status|evidence_rows
|
|
257
|
+
------------+-------------+----------+-------------
|
|
258
|
+
admin |active |revoked | 1
|
|
259
|
+
(1 row)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
starting permutation: keyrevoke_begin keyrevoke_key auth_begin auth_grant keyrevoke_commit auth_commit check
|
|
263
|
+
step keyrevoke_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
264
|
+
step keyrevoke_key:
|
|
265
|
+
update agent_board.agent_signing_keys
|
|
266
|
+
set status = 'revoked', proof_state = 'revoked', revoked_at = now(), updated_at = now()
|
|
267
|
+
where id = '26960000-0000-4000-8000-000000000001';
|
|
268
|
+
|
|
269
|
+
step auth_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
|
|
270
|
+
step auth_grant:
|
|
271
|
+
select w.role as granted_role
|
|
272
|
+
from agent_board.signed_grant_board_role(
|
|
273
|
+
jsonb_build_object(
|
|
274
|
+
'p_board_id', '26950000-0000-4000-8000-000000000001',
|
|
275
|
+
'p_agent_id', '26940000-0000-4000-8000-000000000002',
|
|
276
|
+
'p_grantee_role', 'admin'),
|
|
277
|
+
jsonb_build_object(
|
|
278
|
+
'agent_id', '26940000-0000-4000-8000-000000000001',
|
|
279
|
+
'signing_key_id', '26960000-0000-4000-8000-000000000001',
|
|
280
|
+
'key_id', 'primary',
|
|
281
|
+
'key_version', 1,
|
|
282
|
+
'algorithm', 'Ed25519',
|
|
283
|
+
'verification_status', 'verified',
|
|
284
|
+
'payload_digest', 'sha256:k269',
|
|
285
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
286
|
+
<waiting ...>
|
|
287
|
+
step keyrevoke_commit: COMMIT;
|
|
288
|
+
step auth_grant: <... completed>
|
|
289
|
+
ERROR: signer_key_not_live: signed authority write signing key is revoked, not active
|
|
290
|
+
step auth_commit: COMMIT;
|
|
291
|
+
step check:
|
|
292
|
+
select
|
|
293
|
+
(select role from agent_board.board_memberships
|
|
294
|
+
where board_id = '26950000-0000-4000-8000-000000000001'
|
|
295
|
+
and agent_id = '26940000-0000-4000-8000-000000000002') as grantee_role,
|
|
296
|
+
(select status from agent_board.agents
|
|
297
|
+
where id = '26940000-0000-4000-8000-000000000001') as signer_status,
|
|
298
|
+
(select status from agent_board.agent_signing_keys
|
|
299
|
+
where id = '26960000-0000-4000-8000-000000000001') as key_status,
|
|
300
|
+
(select count(*) from agent_board.signature_evidence
|
|
301
|
+
where operation = 'grant_board_role') as evidence_rows;
|
|
302
|
+
|
|
303
|
+
grantee_role|signer_status|key_status|evidence_rows
|
|
304
|
+
------------+-------------+----------+-------------
|
|
305
|
+
member |active |revoked | 0
|
|
306
|
+
(1 row)
|
|
307
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Consiliency/message-board#269 (F4) — signed_grant_board_role must be ACYCLIC against
|
|
2
|
+
# onboard_federated_fleet_agent.
|
|
3
|
+
#
|
|
4
|
+
# THE DEFECT. The grant wrapper's lock order was
|
|
5
|
+
# board_memberships(grantee, via the inner grant's UPDATE) -> agents(signer, FOR SHARE)
|
|
6
|
+
# while onboard_federated_fleet_agent takes
|
|
7
|
+
# agents(20260901000110:349, FOR UPDATE) -> board_memberships(:397, FOR UPDATE)
|
|
8
|
+
# — the opposite order, with no advisory lock on the grant side to serialize them. That is an
|
|
9
|
+
# ABBA cycle between two HONEST callers: a board owner who created a fleet invite while
|
|
10
|
+
# concurrently granting the enrollee a role. Both operands are routine on one board (the
|
|
11
|
+
# collision needs enrolling agent == grantee and invite creator == signer). Reproduced before
|
|
12
|
+
# the fix as a live `ERROR: deadlock detected ... while locking tuple in relation
|
|
13
|
+
# "board_memberships"`, with the real wrapper on one side.
|
|
14
|
+
#
|
|
15
|
+
# THE FIX this spec pins: the wrapper takes the board-keyed _lock_board_authority_ceiling
|
|
16
|
+
# advisory lock FIRST. Both authority wrappers now take it before anything else, and onboarding
|
|
17
|
+
# takes it at :290 holding only board_invite_tokens and fleet_onboarding_invite_bounds — rows no
|
|
18
|
+
# authority wrapper ever touches — so the transactions are serialized per board rather than
|
|
19
|
+
# merely ordered. Mutual exclusion survives a new lock being added downstream; an ordering
|
|
20
|
+
# argument would have to be re-derived each time.
|
|
21
|
+
#
|
|
22
|
+
# THE "onboard" SESSION IS A LOCK-EQUIVALENT STAND-IN, and here is the equivalence, stated the
|
|
23
|
+
# way this suite requires rather than assumed.
|
|
24
|
+
# * It runs onboard_federated_fleet_agent's three cycle-relevant acquisitions, in its order
|
|
25
|
+
# and its modes: _lock_board_authority_ceiling(board) (:290), then `agents ... for update`
|
|
26
|
+
# (:349), then `board_memberships ... for update` (:397).
|
|
27
|
+
# * It omits that function's other locks — board_invite_tokens, fleet_onboarding_invite_bounds,
|
|
28
|
+
# boards, fleet_machines, fleet_trust_domains, the fingerprint-keyed advisory, auth.users,
|
|
29
|
+
# board_authority_ceilings, board_invite_redemptions, and the enrolled agent's own agents/
|
|
30
|
+
# agent_signing_keys rows. Every one of those is on a relation the grant wrapper never
|
|
31
|
+
# touches, so none can participate in a cycle with it; including them would change nothing
|
|
32
|
+
# observable and would require provisioning a full fleet invite, machine, trust domain and
|
|
33
|
+
# PoP-verified identity inside an isolation setup block.
|
|
34
|
+
# * A full onboarding replay was rejected for that reason alone, not because it would fail.
|
|
35
|
+
# The claim under test is a LOCK ORDER, and these are the locks.
|
|
36
|
+
# THE ADVISORY ACQUISITION IN THIS SESSION IS LOAD-BEARING, not decoration. Drop it and this
|
|
37
|
+
# spec deadlocks even against the FIXED wrapper — the stand-in would hold agents(signer) while
|
|
38
|
+
# taking no advisory lock, so the wrapper would sail through its own advisory acquisition and
|
|
39
|
+
# into the same cycle. A reader who "simplifies" this session by deleting the advisory step is
|
|
40
|
+
# not testing onboarding; they are testing a caller that does not exist, and they will read the
|
|
41
|
+
# resulting deadlock as a failed fix.
|
|
42
|
+
#
|
|
43
|
+
# WHY THE EXISTING signer_lifecycle_revoke.spec DOES NOT COVER THIS. Its three adversaries are
|
|
44
|
+
# revoke_agent_subtree, a bare suspend, and a key-only update. NONE of them takes a
|
|
45
|
+
# board_memberships lock, and board_memberships is one of the two operands in this cycle, so its
|
|
46
|
+
# green golden is silent here. That is why this is a separate spec rather than a permutation
|
|
47
|
+
# added there.
|
|
48
|
+
|
|
49
|
+
setup
|
|
50
|
+
{
|
|
51
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
52
|
+
values
|
|
53
|
+
('f4940000-0000-4000-8000-000000000001','agent','agent','f4iso-signer','F4 ISO Signer/Creator','active'),
|
|
54
|
+
('f4940000-0000-4000-8000-000000000002','agent','agent','f4iso-grantee','F4 ISO Grantee/Enrollee','active');
|
|
55
|
+
|
|
56
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
57
|
+
values ('f4950000-0000-4000-8000-000000000001','f4-iso-board','F4 ISO Board',
|
|
58
|
+
'f4940000-0000-4000-8000-000000000001','private','active');
|
|
59
|
+
|
|
60
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
61
|
+
values
|
|
62
|
+
('f4950000-0000-4000-8000-000000000001','f4940000-0000-4000-8000-000000000001','owner','joined','f4940000-0000-4000-8000-000000000001'),
|
|
63
|
+
('f4950000-0000-4000-8000-000000000001','f4940000-0000-4000-8000-000000000002','member','joined','f4940000-0000-4000-8000-000000000001');
|
|
64
|
+
|
|
65
|
+
insert into agent_board.agent_signing_keys(
|
|
66
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
67
|
+
values ('f4960000-0000-4000-8000-000000000001','f4940000-0000-4000-8000-000000000001',
|
|
68
|
+
'primary', 1, public.test_pubkey('f4-iso'), 'raw_ed25519_base64url',
|
|
69
|
+
public.test_key_fingerprint('f4-iso'), 'active', 'proven');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
teardown
|
|
73
|
+
{
|
|
74
|
+
delete from agent_board.signature_evidence
|
|
75
|
+
where agent_id in (select id from agent_board.agents where handle like 'f4iso-%');
|
|
76
|
+
delete from agent_board.board_authority_ceilings where board_id='f4950000-0000-4000-8000-000000000001';
|
|
77
|
+
delete from agent_board.board_memberships where board_id='f4950000-0000-4000-8000-000000000001';
|
|
78
|
+
delete from agent_board.boards where id='f4950000-0000-4000-8000-000000000001';
|
|
79
|
+
delete from agent_board.agent_signing_keys
|
|
80
|
+
where agent_id in (select id from agent_board.agents where handle like 'f4iso-%');
|
|
81
|
+
delete from agent_board.agents where handle like 'f4iso-%';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# The real signed authority write, unmodified. Only the deterministic column is selected: the
|
|
85
|
+
# evidence id is a fresh uuid and must never reach a golden.
|
|
86
|
+
session "auth"
|
|
87
|
+
step "auth_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
88
|
+
step "auth_grant" {
|
|
89
|
+
select w.role as granted_role
|
|
90
|
+
from agent_board.signed_grant_board_role(
|
|
91
|
+
jsonb_build_object(
|
|
92
|
+
'p_board_id', 'f4950000-0000-4000-8000-000000000001',
|
|
93
|
+
'p_agent_id', 'f4940000-0000-4000-8000-000000000002',
|
|
94
|
+
'p_grantee_role', 'admin'),
|
|
95
|
+
jsonb_build_object(
|
|
96
|
+
'agent_id', 'f4940000-0000-4000-8000-000000000001',
|
|
97
|
+
'signing_key_id', 'f4960000-0000-4000-8000-000000000001',
|
|
98
|
+
'key_id', 'primary',
|
|
99
|
+
'key_version', 1,
|
|
100
|
+
'algorithm', 'Ed25519',
|
|
101
|
+
'verification_status', 'verified',
|
|
102
|
+
'payload_digest', 'sha256:f4iso',
|
|
103
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
104
|
+
}
|
|
105
|
+
step "auth_commit" { COMMIT; }
|
|
106
|
+
|
|
107
|
+
# onboard_federated_fleet_agent's cycle-relevant locks, in its order and modes. See the header
|
|
108
|
+
# for the equivalence argument; the advisory step is load-bearing.
|
|
109
|
+
session "onboard"
|
|
110
|
+
step "onboard_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
111
|
+
step "onboard_advisory" {
|
|
112
|
+
select agent_board._lock_board_authority_ceiling('f4950000-0000-4000-8000-000000000001') is null as advisory_taken;
|
|
113
|
+
}
|
|
114
|
+
step "onboard_agents" {
|
|
115
|
+
select count(*)::int as locked_agents from (
|
|
116
|
+
select 1 from agent_board.agents a
|
|
117
|
+
where a.id = 'f4940000-0000-4000-8000-000000000001' for update) s;
|
|
118
|
+
}
|
|
119
|
+
step "onboard_membership" {
|
|
120
|
+
select count(*)::int as locked_memberships from (
|
|
121
|
+
select 1 from agent_board.board_memberships bm
|
|
122
|
+
where bm.board_id = 'f4950000-0000-4000-8000-000000000001'
|
|
123
|
+
and bm.agent_id = 'f4940000-0000-4000-8000-000000000002'
|
|
124
|
+
order by bm.id for update) s;
|
|
125
|
+
}
|
|
126
|
+
step "onboard_commit" { COMMIT; }
|
|
127
|
+
|
|
128
|
+
session "check"
|
|
129
|
+
step "check" {
|
|
130
|
+
select
|
|
131
|
+
(select role from agent_board.board_memberships
|
|
132
|
+
where board_id = 'f4950000-0000-4000-8000-000000000001'
|
|
133
|
+
and agent_id = 'f4940000-0000-4000-8000-000000000002') as grantee_role,
|
|
134
|
+
(select count(*)::int from agent_board.signature_evidence
|
|
135
|
+
where operation = 'grant_board_role'
|
|
136
|
+
and agent_id = 'f4940000-0000-4000-8000-000000000001') as evidence_rows;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# 1. THE CYCLE, in the order that produced the deadlock. onboard takes the advisory lock and
|
|
140
|
+
# then the signer's agents row; the wrapper then runs. UNFIXED, the wrapper acquires the
|
|
141
|
+
# grantee membership row (its inner UPDATE) and blocks wanting agents FOR SHARE, onboard
|
|
142
|
+
# then wants that same membership row, and the server aborts one of them with `deadlock
|
|
143
|
+
# detected`. FIXED, the wrapper blocks at its FIRST statement — the advisory lock onboard
|
|
144
|
+
# already holds — so it holds nothing, onboard completes, and the grant proceeds after.
|
|
145
|
+
# The difference between those two worlds is the whole golden.
|
|
146
|
+
permutation "onboard_begin" "onboard_advisory" "onboard_agents" "auth_begin" "auth_grant" "onboard_membership" "onboard_commit" "auth_commit" "check"
|
|
147
|
+
|
|
148
|
+
# 2. The same operands with the wrapper first. It holds the advisory lock, so onboard waits at
|
|
149
|
+
# its OWN first step and never reaches the rows; once the wrapper commits, onboard proceeds.
|
|
150
|
+
# auth_commit is scheduled while onboard is still waiting on purpose — with it ordered after
|
|
151
|
+
# onboard's row steps the wait is unresolvable and isolationtester cancels the step, which
|
|
152
|
+
# records a `canceling statement due to user request` in the golden rather than the
|
|
153
|
+
# serialization this permutation exists to show.
|
|
154
|
+
permutation "auth_begin" "auth_grant" "onboard_begin" "onboard_advisory" "auth_commit" "onboard_agents" "onboard_membership" "onboard_commit" "check"
|
|
155
|
+
|
|
156
|
+
# 3. onboard holds the advisory lock alone, with no row locks yet. The wrapper must still block
|
|
157
|
+
# on it — this is what proves the acquisition is FIRST in the wrapper rather than merely
|
|
158
|
+
# present somewhere in it.
|
|
159
|
+
permutation "onboard_begin" "onboard_advisory" "auth_begin" "auth_grant" "onboard_agents" "onboard_membership" "onboard_commit" "auth_commit" "check"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Consiliency/message-board#269 — a signed AUTHORITY write revalidates its signer inside
|
|
2
|
+
# its OWN transaction, and the lifecycle rows it reads are share-locked so a concurrent
|
|
3
|
+
# revoke/suspend serializes against the mutation instead of racing it.
|
|
4
|
+
#
|
|
5
|
+
# WHY THIS CANNOT BE A pgTAP FILE. The defect is a window BETWEEN two committed
|
|
6
|
+
# transactions: Portal verifies the signature and reads lifecycle in one PostgREST call,
|
|
7
|
+
# a revocation commits, and the wrapper runs in a second call. A pgTAP file is one
|
|
8
|
+
# rolled-back transaction and can only reach the predicate ("a suspended signer is
|
|
9
|
+
# refused"), never the interleaving or the blocking. Both are the point here, and both are
|
|
10
|
+
# observable in the trace: isolationtester prints `<waiting ...>` / `<... completed>`
|
|
11
|
+
# whenever a step blocks, so the lock is a byte in the golden rather than a claim.
|
|
12
|
+
#
|
|
13
|
+
# THE THREE ADVERSARIES, and why each is the real thing and not a stand-in.
|
|
14
|
+
# revoke_agent_subtree — the shipped cascade. Its primitive _revoke_agent_principal
|
|
15
|
+
# (20260901000100:1277, the NEWEST definition) locks the agents
|
|
16
|
+
# row FOR UPDATE and then the signing keys, which is why this
|
|
17
|
+
# helper takes agents first: matching the transaction the gate
|
|
18
|
+
# exists to serialize against is what keeps the order acyclic.
|
|
19
|
+
# suspend — a bare `update agent_board.agents set status='suspended'`. This
|
|
20
|
+
# is lock-identical to the platform-admin RPC revoke_agent
|
|
21
|
+
# (20260512000000:569), whose suspension is likewise a plain
|
|
22
|
+
# UPDATE of a non-key column taking FOR NO KEY UPDATE. It is used
|
|
23
|
+
# directly only to avoid provisioning a platform-admin JWT inside
|
|
24
|
+
# an isolation session; the lock interaction under test is the
|
|
25
|
+
# same byte for byte.
|
|
26
|
+
# keyrevoke — a key-only `update agent_board.agent_signing_keys set
|
|
27
|
+
# status='revoked'...`, which is the UPDATE statement
|
|
28
|
+
# revoke_fleet_subject's signing_key branch runs, minus the agent
|
|
29
|
+
# row lock that function takes first. Every shipped key-revoking
|
|
30
|
+
# path locks the agent first, so only a key-ONLY writer can pin
|
|
31
|
+
# the agent_signing_keys FOR SHARE at all -- and it must be a
|
|
32
|
+
# PLAIN update. issue_signing_key_challenge was tried here and
|
|
33
|
+
# rejected: it takes FOR UPDATE on the key, which also conflicts
|
|
34
|
+
# with the FOR KEY SHARE the evidence row's foreign key already
|
|
35
|
+
# takes, so it blocks whether or not this change exists and
|
|
36
|
+
# distinguishes nothing. A plain UPDATE takes FOR NO KEY UPDATE,
|
|
37
|
+
# which the FK's KEY SHARE does NOT block and FOR SHARE does --
|
|
38
|
+
# so this session, and only this session, makes the key lock and
|
|
39
|
+
# its MODE observable. The table's UPDATE privilege is the
|
|
40
|
+
# owner's alone (service_role has SELECT), so this models
|
|
41
|
+
# owner-level incident DML and any future key-only writer.
|
|
42
|
+
|
|
43
|
+
setup
|
|
44
|
+
{
|
|
45
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
46
|
+
values
|
|
47
|
+
('26940000-0000-4000-8000-000000000001','agent','agent','k269iso-signer','K269 ISO Signer','active'),
|
|
48
|
+
('26940000-0000-4000-8000-000000000002','agent','agent','k269iso-grantee','K269 ISO Grantee','active'),
|
|
49
|
+
('26940000-0000-4000-8000-000000000003','agent','agent','k269iso-revoker','K269 ISO Revoker','active');
|
|
50
|
+
|
|
51
|
+
insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
|
|
52
|
+
values ('26950000-0000-4000-8000-000000000001','k269-iso-board','K269 ISO Board',
|
|
53
|
+
'26940000-0000-4000-8000-000000000001','private','active');
|
|
54
|
+
|
|
55
|
+
insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
|
|
56
|
+
values
|
|
57
|
+
('26950000-0000-4000-8000-000000000001','26940000-0000-4000-8000-000000000001','owner','joined','26940000-0000-4000-8000-000000000001'),
|
|
58
|
+
('26950000-0000-4000-8000-000000000001','26940000-0000-4000-8000-000000000002','member','joined','26940000-0000-4000-8000-000000000001');
|
|
59
|
+
|
|
60
|
+
insert into agent_board.agent_signing_keys(
|
|
61
|
+
id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
|
|
62
|
+
values ('26960000-0000-4000-8000-000000000001','26940000-0000-4000-8000-000000000001',
|
|
63
|
+
'primary', 1, public.test_pubkey('k269-iso'), 'raw_ed25519_base64url',
|
|
64
|
+
public.test_key_fingerprint('k269-iso'), 'active', 'proven');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
teardown
|
|
68
|
+
{
|
|
69
|
+
delete from agent_board.signature_evidence
|
|
70
|
+
where agent_id in (select id from agent_board.agents where handle like 'k269iso-%');
|
|
71
|
+
delete from agent_board.agent_signing_key_challenges
|
|
72
|
+
where agent_id in (select id from agent_board.agents where handle like 'k269iso-%');
|
|
73
|
+
delete from agent_board.fleet_revocation_events
|
|
74
|
+
where agent_id in (select id from agent_board.agents where handle like 'k269iso-%')
|
|
75
|
+
or revoked_by_agent_id in (select id from agent_board.agents where handle like 'k269iso-%');
|
|
76
|
+
delete from agent_board.board_authority_ceilings where board_id='26950000-0000-4000-8000-000000000001';
|
|
77
|
+
delete from agent_board.board_memberships where board_id='26950000-0000-4000-8000-000000000001';
|
|
78
|
+
delete from agent_board.boards where id='26950000-0000-4000-8000-000000000001';
|
|
79
|
+
delete from agent_board.agent_signing_keys
|
|
80
|
+
where agent_id in (select id from agent_board.agents where handle like 'k269iso-%');
|
|
81
|
+
delete from agent_board.agents where handle like 'k269iso-%';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# The signed authority write. The signature_evidence_id it returns is a fresh uuid, so only
|
|
85
|
+
# the deterministic column is selected — a golden must not contain a random value.
|
|
86
|
+
session "auth"
|
|
87
|
+
step "auth_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
88
|
+
step "auth_grant" {
|
|
89
|
+
select w.role as granted_role
|
|
90
|
+
from agent_board.signed_grant_board_role(
|
|
91
|
+
jsonb_build_object(
|
|
92
|
+
'p_board_id', '26950000-0000-4000-8000-000000000001',
|
|
93
|
+
'p_agent_id', '26940000-0000-4000-8000-000000000002',
|
|
94
|
+
'p_grantee_role', 'admin'),
|
|
95
|
+
jsonb_build_object(
|
|
96
|
+
'agent_id', '26940000-0000-4000-8000-000000000001',
|
|
97
|
+
'signing_key_id', '26960000-0000-4000-8000-000000000001',
|
|
98
|
+
'key_id', 'primary',
|
|
99
|
+
'key_version', 1,
|
|
100
|
+
'algorithm', 'Ed25519',
|
|
101
|
+
'verification_status', 'verified',
|
|
102
|
+
'payload_digest', 'sha256:k269',
|
|
103
|
+
'enforcement_mode', 'require_for_agent_writes')) w;
|
|
104
|
+
}
|
|
105
|
+
step "auth_commit" { COMMIT; }
|
|
106
|
+
|
|
107
|
+
session "revoke"
|
|
108
|
+
step "revoke_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
109
|
+
step "revoke_signer" {
|
|
110
|
+
select revoked_agent_count, revoked_key_count
|
|
111
|
+
from agent_board.revoke_agent_subtree(
|
|
112
|
+
'26940000-0000-4000-8000-000000000001',
|
|
113
|
+
'26940000-0000-4000-8000-000000000003',
|
|
114
|
+
'k269 iso revoke');
|
|
115
|
+
}
|
|
116
|
+
step "revoke_commit" { COMMIT; }
|
|
117
|
+
|
|
118
|
+
session "suspend"
|
|
119
|
+
step "suspend_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
120
|
+
step "suspend_signer" {
|
|
121
|
+
update agent_board.agents set status = 'suspended', updated_at = now()
|
|
122
|
+
where id = '26940000-0000-4000-8000-000000000001';
|
|
123
|
+
}
|
|
124
|
+
step "suspend_commit" { COMMIT; }
|
|
125
|
+
|
|
126
|
+
# Key-only writer: a PLAIN update of the signing key, touching no agents row.
|
|
127
|
+
session "keyrevoke"
|
|
128
|
+
step "keyrevoke_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
129
|
+
step "keyrevoke_key" {
|
|
130
|
+
update agent_board.agent_signing_keys
|
|
131
|
+
set status = 'revoked', proof_state = 'revoked', revoked_at = now(), updated_at = now()
|
|
132
|
+
where id = '26960000-0000-4000-8000-000000000001';
|
|
133
|
+
}
|
|
134
|
+
step "keyrevoke_commit" { COMMIT; }
|
|
135
|
+
|
|
136
|
+
session "check"
|
|
137
|
+
step "check" {
|
|
138
|
+
select
|
|
139
|
+
(select role from agent_board.board_memberships
|
|
140
|
+
where board_id = '26950000-0000-4000-8000-000000000001'
|
|
141
|
+
and agent_id = '26940000-0000-4000-8000-000000000002') as grantee_role,
|
|
142
|
+
(select status from agent_board.agents
|
|
143
|
+
where id = '26940000-0000-4000-8000-000000000001') as signer_status,
|
|
144
|
+
(select status from agent_board.agent_signing_keys
|
|
145
|
+
where id = '26960000-0000-4000-8000-000000000001') as key_status,
|
|
146
|
+
(select count(*) from agent_board.signature_evidence
|
|
147
|
+
where operation = 'grant_board_role') as evidence_rows;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# 1. Write first: the revocation must WAIT on the share-locked lifecycle rows rather than
|
|
151
|
+
# committing underneath a grant that has already been authorized. The grant stands and
|
|
152
|
+
# the revocation lands after it.
|
|
153
|
+
permutation "auth_begin" "auth_grant" "revoke_begin" "revoke_signer" "auth_commit" "revoke_commit" "check"
|
|
154
|
+
|
|
155
|
+
# 2. The defect's own interleaving, in the order that exposes it. The revocation is in
|
|
156
|
+
# flight when the wrapper runs; the wrapper blocks on the agents row, then observes the
|
|
157
|
+
# committed revocation and fails closed. Before #269 this permutation completed the
|
|
158
|
+
# grant: the wrapper read no lifecycle at all, so there was nothing to block on.
|
|
159
|
+
permutation "revoke_begin" "revoke_signer" "auth_begin" "auth_grant" "revoke_commit" "auth_commit" "check"
|
|
160
|
+
|
|
161
|
+
# 3. Suspension, which touches the agents row and NOT the key — so it isolates the agents
|
|
162
|
+
# FOR SHARE from the key FOR SHARE.
|
|
163
|
+
permutation "suspend_begin" "suspend_signer" "auth_begin" "auth_grant" "suspend_commit" "auth_commit" "check"
|
|
164
|
+
|
|
165
|
+
# 4. Suspension the other way round: the write holds the agents row and the suspension waits.
|
|
166
|
+
permutation "auth_begin" "auth_grant" "suspend_begin" "suspend_signer" "auth_commit" "suspend_commit" "check"
|
|
167
|
+
|
|
168
|
+
# 5. The key half, isolated. Nothing here touches the agents row, so the wait is produced by
|
|
169
|
+
# the agent_signing_keys FOR SHARE and by nothing else -- and by its MODE: FOR KEY SHARE
|
|
170
|
+
# would not conflict with this plain UPDATE's FOR NO KEY UPDATE.
|
|
171
|
+
permutation "auth_begin" "auth_grant" "keyrevoke_begin" "keyrevoke_key" "auth_commit" "keyrevoke_commit" "check"
|
|
172
|
+
|
|
173
|
+
# 6. And the key revocation's own race: revoked in flight, the authority write blocks on the
|
|
174
|
+
# key row, then observes the committed revocation and fails closed -- the key-side twin of
|
|
175
|
+
# permutation 2.
|
|
176
|
+
permutation "keyrevoke_begin" "keyrevoke_key" "auth_begin" "auth_grant" "keyrevoke_commit" "auth_commit" "check"
|