@consiliency/agent-board-schema 1.57.0 → 1.59.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.
Files changed (36) hide show
  1. package/fixtures/already-applied.json +8 -0
  2. package/migrations/20261008000100_agent_board_hosted_delegates.sql +3152 -0
  3. package/migrations/20261009000100_agent_board_hosted_followups.sql +665 -0
  4. package/package.json +1 -1
  5. package/schema-manifest.json +21 -3
  6. package/src/tooling.test.mjs +9 -2
  7. package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -0
  8. package/tests/agent_board_consent_emission.test.sql +4 -2
  9. package/tests/agent_board_delegate_role_class.test.sql +736 -0
  10. package/tests/agent_board_federated_identity_injective_slug.test.sql +5 -2
  11. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +71 -14
  12. package/tests/agent_board_fk_lock_edges.test.sql +9 -0
  13. package/tests/agent_board_hosted_delegates.test.sql +1278 -0
  14. package/tests/agent_board_ledger_authority.test.sql +34 -5
  15. package/tests/agent_board_signer_lifecycle.test.sql +3 -0
  16. package/tests/agent_board_write_enforcement_class.test.sql +8 -3
  17. package/tests/catalog/function_execute_privileges.json +269 -2
  18. package/tests/catalog/p_metadata_parameters.json +44 -0
  19. package/tests/isolation/expected/hosted_delegate_add_tombstone_race.out +105 -0
  20. package/tests/isolation/expected/hosted_delegate_exchange_agent_revoke_race.out +66 -0
  21. package/tests/isolation/expected/hosted_delegate_exchange_race.out +93 -0
  22. package/tests/isolation/expected/hosted_delegate_first_exchange_race.out +129 -0
  23. package/tests/isolation/expected/hosted_delegate_owner_provisioning_race.out +91 -0
  24. package/tests/isolation/expected/hosted_delegate_owner_revoke_race.out +137 -0
  25. package/tests/isolation/expected/hosted_delegate_revoke_race.out +87 -0
  26. package/tests/isolation/expected/hosted_delegate_tombstone_revoke_race.out +170 -0
  27. package/tests/isolation/expected/hosted_delegate_withdraw_evict_race.out +235 -0
  28. package/tests/isolation/specs/hosted_delegate_add_tombstone_race.spec +102 -0
  29. package/tests/isolation/specs/hosted_delegate_exchange_agent_revoke_race.spec +92 -0
  30. package/tests/isolation/specs/hosted_delegate_exchange_race.spec +64 -0
  31. package/tests/isolation/specs/hosted_delegate_first_exchange_race.spec +72 -0
  32. package/tests/isolation/specs/hosted_delegate_owner_provisioning_race.spec +73 -0
  33. package/tests/isolation/specs/hosted_delegate_owner_revoke_race.spec +93 -0
  34. package/tests/isolation/specs/hosted_delegate_revoke_race.spec +59 -0
  35. package/tests/isolation/specs/hosted_delegate_tombstone_revoke_race.spec +158 -0
  36. package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +145 -0
@@ -0,0 +1,170 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: h_begin t_begin t_tomb r_begin r_human h_commit t_commit r_commit check
4
+ bare_owner
5
+ ----------
6
+ t
7
+ (1 row)
8
+
9
+ step h_begin:
10
+ BEGIN;
11
+ select (bm.id is not null) as held from agent_board.board_memberships bm
12
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
13
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state = 'joined'
14
+ for update of bm;
15
+
16
+ held
17
+ ----
18
+ t
19
+ (1 row)
20
+
21
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
22
+ step t_tomb:
23
+ select first from agent_board.tombstone_delegate_instance(
24
+ 'iso_tr_instance_00001', 'https://iso-tr.test', 'iso-tr-owner', 'owner_revoked');
25
+ <waiting ...>
26
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
27
+ step r_human:
28
+ select revoked_agent_count from agent_board.revoke_human_subtree(
29
+ (select auth_user_id from agent_board.agents
30
+ where handle = agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-owner')),
31
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
32
+ <waiting ...>
33
+ step h_commit: COMMIT;
34
+ step t_tomb: <... completed>
35
+ first
36
+ -----
37
+ t
38
+ (1 row)
39
+
40
+ step t_commit: COMMIT;
41
+ step r_human: <... completed>
42
+ revoked_agent_count
43
+ -------------------
44
+ 2
45
+ (1 row)
46
+
47
+ step r_commit: COMMIT;
48
+ step check:
49
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
50
+ where di.instance_id = 'iso_tr_instance_00001') as tombstoned,
51
+ (select a.status from agent_board.agents a
52
+ join agent_board.delegate_instances di on di.agent_id = a.id
53
+ where di.instance_id = 'iso_tr_instance_00001') as delegate_status,
54
+ (select count(*) from agent_board.agent_credentials ac
55
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
56
+ where di.instance_id = 'iso_tr_instance_00001' and ac.revoked_at is null) as live_credentials,
57
+ (select count(*) from agent_board.board_memberships bm
58
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
59
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state in ('joined', 'subscribed')) as active_memberships;
60
+
61
+ tombstoned|delegate_status|live_credentials|active_memberships
62
+ ----------+---------------+----------------+------------------
63
+ true |revoked | 0| 0
64
+ (1 row)
65
+
66
+
67
+ starting permutation: h_begin t_begin t_tomb r_begin r_agent h_commit t_commit r_commit check
68
+ bare_owner
69
+ ----------
70
+ t
71
+ (1 row)
72
+
73
+ step h_begin:
74
+ BEGIN;
75
+ select (bm.id is not null) as held from agent_board.board_memberships bm
76
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
77
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state = 'joined'
78
+ for update of bm;
79
+
80
+ held
81
+ ----
82
+ t
83
+ (1 row)
84
+
85
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
86
+ step t_tomb:
87
+ select first from agent_board.tombstone_delegate_instance(
88
+ 'iso_tr_instance_00001', 'https://iso-tr.test', 'iso-tr-owner', 'owner_revoked');
89
+ <waiting ...>
90
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
91
+ step r_agent:
92
+ select revoked_agent_count from agent_board.revoke_agent_subtree(
93
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_tr_instance_00001'),
94
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
95
+ <waiting ...>
96
+ step h_commit: COMMIT;
97
+ step t_tomb: <... completed>
98
+ first
99
+ -----
100
+ t
101
+ (1 row)
102
+
103
+ step t_commit: COMMIT;
104
+ step r_agent: <... completed>
105
+ revoked_agent_count
106
+ -------------------
107
+ 1
108
+ (1 row)
109
+
110
+ step r_commit: COMMIT;
111
+ step check:
112
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
113
+ where di.instance_id = 'iso_tr_instance_00001') as tombstoned,
114
+ (select a.status from agent_board.agents a
115
+ join agent_board.delegate_instances di on di.agent_id = a.id
116
+ where di.instance_id = 'iso_tr_instance_00001') as delegate_status,
117
+ (select count(*) from agent_board.agent_credentials ac
118
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
119
+ where di.instance_id = 'iso_tr_instance_00001' and ac.revoked_at is null) as live_credentials,
120
+ (select count(*) from agent_board.board_memberships bm
121
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
122
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state in ('joined', 'subscribed')) as active_memberships;
123
+
124
+ tombstoned|delegate_status|live_credentials|active_memberships
125
+ ----------+---------------+----------------+------------------
126
+ true |revoked | 0| 0
127
+ (1 row)
128
+
129
+
130
+ starting permutation: t_begin t_tomb_bare r_begin r_human_bare t_commit r_commit check_bare
131
+ bare_owner
132
+ ----------
133
+ t
134
+ (1 row)
135
+
136
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
137
+ step t_tomb_bare:
138
+ select first from agent_board.tombstone_delegate_instance(
139
+ 'iso_tr_bare_instance_01', 'https://iso-tr.test', 'iso-tr-bare', 'owner_revoked');
140
+
141
+ first
142
+ -----
143
+ t
144
+ (1 row)
145
+
146
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
147
+ step r_human_bare:
148
+ select revoked_agent_count from agent_board.revoke_human_subtree(
149
+ (select id from auth.users
150
+ where email = 'agent+' || agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-bare') || '@agent-board.local'),
151
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
152
+
153
+ revoked_agent_count
154
+ -------------------
155
+ 0
156
+ (1 row)
157
+
158
+ step t_commit: COMMIT;
159
+ step r_commit: COMMIT;
160
+ step check_bare:
161
+ select (select count(*) from agent_board.delegate_instances where instance_id = 'iso_tr_bare_instance_01'
162
+ and tombstoned_at is not null and owner_subject = 'iso-tr-bare') as tombstoned_recorded,
163
+ (select count(*) from agent_board.fleet_human_revocations
164
+ where issuer = 'https://iso-tr.test' and subject = 'iso-tr-bare') as markers;
165
+
166
+ tombstoned_recorded|markers
167
+ -------------------+-------
168
+ 1| 0
169
+ (1 row)
170
+
@@ -0,0 +1,235 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: a_begin a_evict o_begin o_withdraw a_commit o_commit check o_readd check
4
+ step a_begin:
5
+ BEGIN;
6
+ select set_config('request.jwt.claims',
7
+ json_build_object('role', 'authenticated', 'sub', '16320000-0000-4000-8000-000000000001')::text, true) is not null as admin_session;
8
+
9
+ admin_session
10
+ -------------
11
+ t
12
+ (1 row)
13
+
14
+ step a_evict:
15
+ select membership_state from agent_board.update_board_membership(
16
+ '16322000-0000-4000-8000-000000000001',
17
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
18
+ 'removed');
19
+
20
+ membership_state
21
+ ----------------
22
+ removed
23
+ (1 row)
24
+
25
+ step o_begin:
26
+ BEGIN;
27
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
28
+ (select auth_user_id from agent_board.agents
29
+ where handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner')))::text, true) is not null as owner_session;
30
+
31
+ owner_session
32
+ -------------
33
+ t
34
+ (1 row)
35
+
36
+ step o_withdraw:
37
+ select membership_state from agent_board.update_board_membership(
38
+ '16322000-0000-4000-8000-000000000001',
39
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
40
+ 'left');
41
+ <waiting ...>
42
+ step a_commit: COMMIT;
43
+ step o_withdraw: <... completed>
44
+ ERROR: a principal may withdraw its delegate only from an active or pending membership
45
+ step o_commit: COMMIT;
46
+ step check:
47
+ select (select bm.membership_state from agent_board.board_memberships bm
48
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
49
+ where bm.board_id = '16322000-0000-4000-8000-000000000001' and di.instance_id = 'iso_wd_instance_00001'
50
+ order by bm.created_at desc, bm.id desc limit 1) as delegate_state,
51
+ (select bm.membership_state from agent_board.board_memberships bm
52
+ where bm.board_id = '16322000-0000-4000-8000-000000000001'
53
+ and bm.agent_id = '16321000-0000-4000-8000-000000000002'
54
+ order by bm.created_at desc, bm.id desc limit 1) as member_state;
55
+
56
+ delegate_state|member_state
57
+ --------------+------------
58
+ removed |joined
59
+ (1 row)
60
+
61
+ step o_readd:
62
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
63
+ (select auth_user_id from agent_board.agents
64
+ where handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner')))::text, false) is not null as owner_session;
65
+ select membership_state from agent_board.add_delegate_to_board(
66
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
67
+ '16322000-0000-4000-8000-000000000001');
68
+
69
+ owner_session
70
+ -------------
71
+ t
72
+ (1 row)
73
+
74
+ ERROR: a removed delegate stays removed; a board admin must lift the removal
75
+ step check:
76
+ select (select bm.membership_state from agent_board.board_memberships bm
77
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
78
+ where bm.board_id = '16322000-0000-4000-8000-000000000001' and di.instance_id = 'iso_wd_instance_00001'
79
+ order by bm.created_at desc, bm.id desc limit 1) as delegate_state,
80
+ (select bm.membership_state from agent_board.board_memberships bm
81
+ where bm.board_id = '16322000-0000-4000-8000-000000000001'
82
+ and bm.agent_id = '16321000-0000-4000-8000-000000000002'
83
+ order by bm.created_at desc, bm.id desc limit 1) as member_state;
84
+
85
+ delegate_state|member_state
86
+ --------------+------------
87
+ removed |joined
88
+ (1 row)
89
+
90
+
91
+ starting permutation: o_begin o_withdraw a_begin a_evict o_commit a_commit check o_readd check
92
+ step o_begin:
93
+ BEGIN;
94
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
95
+ (select auth_user_id from agent_board.agents
96
+ where handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner')))::text, true) is not null as owner_session;
97
+
98
+ owner_session
99
+ -------------
100
+ t
101
+ (1 row)
102
+
103
+ step o_withdraw:
104
+ select membership_state from agent_board.update_board_membership(
105
+ '16322000-0000-4000-8000-000000000001',
106
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
107
+ 'left');
108
+
109
+ membership_state
110
+ ----------------
111
+ left
112
+ (1 row)
113
+
114
+ step a_begin:
115
+ BEGIN;
116
+ select set_config('request.jwt.claims',
117
+ json_build_object('role', 'authenticated', 'sub', '16320000-0000-4000-8000-000000000001')::text, true) is not null as admin_session;
118
+
119
+ admin_session
120
+ -------------
121
+ t
122
+ (1 row)
123
+
124
+ step a_evict:
125
+ select membership_state from agent_board.update_board_membership(
126
+ '16322000-0000-4000-8000-000000000001',
127
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
128
+ 'removed');
129
+ <waiting ...>
130
+ step o_commit: COMMIT;
131
+ step a_evict: <... completed>
132
+ membership_state
133
+ ----------------
134
+ removed
135
+ (1 row)
136
+
137
+ step a_commit: COMMIT;
138
+ step check:
139
+ select (select bm.membership_state from agent_board.board_memberships bm
140
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
141
+ where bm.board_id = '16322000-0000-4000-8000-000000000001' and di.instance_id = 'iso_wd_instance_00001'
142
+ order by bm.created_at desc, bm.id desc limit 1) as delegate_state,
143
+ (select bm.membership_state from agent_board.board_memberships bm
144
+ where bm.board_id = '16322000-0000-4000-8000-000000000001'
145
+ and bm.agent_id = '16321000-0000-4000-8000-000000000002'
146
+ order by bm.created_at desc, bm.id desc limit 1) as member_state;
147
+
148
+ delegate_state|member_state
149
+ --------------+------------
150
+ removed |joined
151
+ (1 row)
152
+
153
+ step o_readd:
154
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
155
+ (select auth_user_id from agent_board.agents
156
+ where handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner')))::text, false) is not null as owner_session;
157
+ select membership_state from agent_board.add_delegate_to_board(
158
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
159
+ '16322000-0000-4000-8000-000000000001');
160
+
161
+ owner_session
162
+ -------------
163
+ t
164
+ (1 row)
165
+
166
+ ERROR: a removed delegate stays removed; a board admin must lift the removal
167
+ step check:
168
+ select (select bm.membership_state from agent_board.board_memberships bm
169
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
170
+ where bm.board_id = '16322000-0000-4000-8000-000000000001' and di.instance_id = 'iso_wd_instance_00001'
171
+ order by bm.created_at desc, bm.id desc limit 1) as delegate_state,
172
+ (select bm.membership_state from agent_board.board_memberships bm
173
+ where bm.board_id = '16322000-0000-4000-8000-000000000001'
174
+ and bm.agent_id = '16321000-0000-4000-8000-000000000002'
175
+ order by bm.created_at desc, bm.id desc limit 1) as member_state;
176
+
177
+ delegate_state|member_state
178
+ --------------+------------
179
+ removed |joined
180
+ (1 row)
181
+
182
+
183
+ starting permutation: a_begin a_evict_member m_begin m_leave a_commit m_commit check
184
+ step a_begin:
185
+ BEGIN;
186
+ select set_config('request.jwt.claims',
187
+ json_build_object('role', 'authenticated', 'sub', '16320000-0000-4000-8000-000000000001')::text, true) is not null as admin_session;
188
+
189
+ admin_session
190
+ -------------
191
+ t
192
+ (1 row)
193
+
194
+ step a_evict_member:
195
+ select membership_state from agent_board.update_board_membership(
196
+ '16322000-0000-4000-8000-000000000001', '16321000-0000-4000-8000-000000000002', 'removed');
197
+
198
+ membership_state
199
+ ----------------
200
+ removed
201
+ (1 row)
202
+
203
+ step m_begin:
204
+ BEGIN;
205
+ select set_config('request.jwt.claims',
206
+ json_build_object('role', 'authenticated', 'sub', '16320000-0000-4000-8000-000000000002')::text, true) is not null as member_session;
207
+
208
+ member_session
209
+ --------------
210
+ t
211
+ (1 row)
212
+
213
+ step m_leave:
214
+ select membership_state from agent_board.update_board_membership(
215
+ '16322000-0000-4000-8000-000000000001', '16321000-0000-4000-8000-000000000002', 'left');
216
+ <waiting ...>
217
+ step a_commit: COMMIT;
218
+ step m_leave: <... completed>
219
+ ERROR: self membership change not permitted; use the join/approval path
220
+ step m_commit: COMMIT;
221
+ step check:
222
+ select (select bm.membership_state from agent_board.board_memberships bm
223
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
224
+ where bm.board_id = '16322000-0000-4000-8000-000000000001' and di.instance_id = 'iso_wd_instance_00001'
225
+ order by bm.created_at desc, bm.id desc limit 1) as delegate_state,
226
+ (select bm.membership_state from agent_board.board_memberships bm
227
+ where bm.board_id = '16322000-0000-4000-8000-000000000001'
228
+ and bm.agent_id = '16321000-0000-4000-8000-000000000002'
229
+ order by bm.created_at desc, bm.id desc limit 1) as member_state;
230
+
231
+ delegate_state|member_state
232
+ --------------+------------
233
+ joined |removed
234
+ (1 row)
235
+
@@ -0,0 +1,102 @@
1
+ # HOSTED follow-ups (Consiliency/message-board#591, item 3b): add_delegate_to_board racing
2
+ # tombstone_delegate_instance of the same instance.
3
+ #
4
+ # add_delegate_to_board reads the instance row FOR SHARE (after the board advisory lock and the
5
+ # owner's agents row) and refuses a tombstoned one; the tombstone holds that row FOR UPDATE while
6
+ # it tombstones the instance and ends every active membership it then lists. The instance row is
7
+ # the one lock the two contend on, so they serialise, and a tombstoned instance is never left with
8
+ # an active membership:
9
+ #
10
+ # P1 the owner's add first: the tombstone waits on the instance row, then lists the new
11
+ # membership and ends it (`left`). Tombstoned, revoked, no active membership.
12
+ # P2 the tombstone first: the add waits on the instance row, then reads the tombstone and is
13
+ # refused. Tombstoned, revoked, no membership at all.
14
+ #
15
+ # Without the FOR SHARE (measured, mutation T4 in plans/evidence/hosted/mutation-receipts.md) both
16
+ # orders leave a tombstoned instance joined to the board: in P1 the tombstone no longer waits and
17
+ # lists no membership to end; in P2 the add reads the instance before the tombstone commits and
18
+ # writes its membership without waiting (the sink's foreign-key FOR KEY SHARE on the delegate's
19
+ # agents row does not conflict with the tombstone's FOR NO KEY UPDATE). Removing the tombstone's
20
+ # own FOR UPDATE alone is NOT caught (T5): its UPDATE of the instance row takes the same lock
21
+ # before it lists memberships.
22
+
23
+ setup
24
+ {
25
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
26
+ values ('16591100-0000-4000-8000-000000000001', 'agent', 'agent', 'iso-at-admin', 'Iso At Admin', 'active');
27
+ insert into agent_board.identity_providers(
28
+ issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds
29
+ ) values (
30
+ 'https://iso-at.test', 'message-board', true, true, ARRAY[]::text[], ARRAY[]::text[], ARRAY['human']
31
+ );
32
+ select count(*) from agent_board.enroll_federated_human(
33
+ 'https://iso-at.test', 'message-board', 'iso-at-owner',
34
+ jsonb_build_object('sub', 'iso-at-owner'), 'Iso At Owner');
35
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
36
+ 'iss', 'https://iso-at.test', 'sub', 'iso-at-owner', 'slot_resource', null,
37
+ 'agent_board_delegate_instance', jsonb_build_object(
38
+ 'instance_id', 'iso_at_instance_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
39
+ 'owner_sub', 'iso-at-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
40
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
41
+ values ('16591300-0000-4000-8000-000000000001', 'iso-at-board', 'Iso At Board',
42
+ '16591100-0000-4000-8000-000000000001', 'private', 'active');
43
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
44
+ select '16591300-0000-4000-8000-000000000001'::uuid, a.id, 'member', 'joined', '16591100-0000-4000-8000-000000000001'::uuid
45
+ from agent_board.agents a where a.handle = agent_board._federated_identity_slug('https://iso-at.test', 'iso-at-owner');
46
+ }
47
+
48
+ teardown
49
+ {
50
+ delete from agent_board.board_membership_events where board_id = '16591300-0000-4000-8000-000000000001';
51
+ delete from agent_board.board_memberships where board_id = '16591300-0000-4000-8000-000000000001';
52
+ delete from agent_board.boards where id = '16591300-0000-4000-8000-000000000001';
53
+ delete from agent_board.agent_credentials
54
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_at\_%');
55
+ delete from agent_board.delegate_instances where instance_id like 'iso\_at\_%';
56
+ delete from agent_board.enrollment_federation_audit where issuer = 'https://iso-at.test';
57
+ delete from agent_board.agents
58
+ where handle like 'delegate-%' or handle like 'fed-human-%' or id = '16591100-0000-4000-8000-000000000001';
59
+ delete from agent_board.identity_providers where issuer = 'https://iso-at.test';
60
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
61
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
62
+ }
63
+
64
+ session "owner"
65
+ step "o_begin" {
66
+ BEGIN;
67
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
68
+ (select auth_user_id from agent_board.agents
69
+ where handle = agent_board._federated_identity_slug('https://iso-at.test', 'iso-at-owner')))::text, true) is not null as owner_session;
70
+ }
71
+ step "o_add" {
72
+ select membership_state from agent_board.add_delegate_to_board(
73
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_at_instance_00001'),
74
+ '16591300-0000-4000-8000-000000000001');
75
+ }
76
+ step "o_commit" { COMMIT; }
77
+
78
+ session "tomb"
79
+ step "t_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
80
+ step "t_tomb" {
81
+ select first from agent_board.tombstone_delegate_instance(
82
+ 'iso_at_instance_00001', 'https://iso-at.test', 'iso-at-owner', 'owner_revoked');
83
+ }
84
+ step "t_commit" { COMMIT; }
85
+
86
+ session "check"
87
+ step "check" {
88
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
89
+ where di.instance_id = 'iso_at_instance_00001') as tombstoned,
90
+ (select a.status from agent_board.agents a
91
+ join agent_board.delegate_instances di on di.agent_id = a.id
92
+ where di.instance_id = 'iso_at_instance_00001') as delegate_status,
93
+ (select string_agg(bm.membership_state, ',' order by bm.created_at, bm.id) from agent_board.board_memberships bm
94
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
95
+ where di.instance_id = 'iso_at_instance_00001') as delegate_memberships,
96
+ (select count(*) from agent_board.board_memberships bm
97
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
98
+ where di.tombstoned_at is not null and bm.membership_state in ('joined', 'subscribed')) as tombstoned_and_active;
99
+ }
100
+
101
+ permutation "o_begin" "o_add" "t_begin" "t_tomb" "o_commit" "t_commit" "check"
102
+ permutation "t_begin" "t_tomb" "o_begin" "o_add" "t_commit" "o_commit" "check"
@@ -0,0 +1,92 @@
1
+ # HOSTED follow-ups (Consiliency/message-board#591, the class of item 8): an exchange that
2
+ # supersedes a generation racing revoke_agent_subtree of the same delegate.
3
+ #
4
+ # The exchange revokes every lower-generation credential (an UPDATE that locks those rows) and
5
+ # then inserts the new credential, whose foreign key takes FOR KEY SHARE on the delegate's agents
6
+ # row. _revoke_agent_principal takes that agents row FOR UPDATE and then the credentials: ABBA,
7
+ # the same shape as the tombstone's (hosted_delegate_tombstone_revoke_race P2). The exchange now
8
+ # takes the FOR KEY SHARE when it reads the agent's status, before any credential.
9
+ #
10
+ # The "hold" session parks the exchange mid-UPDATE deterministically: it holds the delegate's
11
+ # newest credential FOR UPDATE, so the exchange's superseding UPDATE locks the older one and waits.
12
+ #
13
+ # P1 Unfixed: the revocation takes the agents row, revokes the agent, and waits on the
14
+ # credential the exchange holds; released, the exchange's insert waits on the agents row:
15
+ # deadlock, the exchange is the victim (its deadlock_timeout is the shorter). Fixed: the
16
+ # revocation waits on the agents row; the exchange commits its generation-2 credential, and
17
+ # the revocation then revokes the agent and every credential, that one included.
18
+
19
+ setup
20
+ {
21
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
22
+ values ('16591400-0000-4000-8000-000000000001', 'agent', 'agent', 'iso-xa-revoker', 'Iso Xa Revoker', 'active');
23
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
24
+ 'iss', 'https://iso-xa.test', 'sub', 'iso-xa-owner', 'slot_resource', null,
25
+ 'agent_board_delegate_instance', jsonb_build_object(
26
+ 'instance_id', 'iso_xa_instance_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
27
+ 'owner_sub', 'iso-xa-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
28
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
29
+ 'iss', 'https://iso-xa.test', 'sub', 'iso-xa-owner', 'slot_resource', null,
30
+ 'agent_board_delegate_instance', jsonb_build_object(
31
+ 'instance_id', 'iso_xa_instance_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
32
+ 'owner_sub', 'iso-xa-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
33
+ }
34
+
35
+ teardown
36
+ {
37
+ delete from agent_board.fleet_revocation_events where revoked_by_agent_id = '16591400-0000-4000-8000-000000000001';
38
+ delete from agent_board.agent_credentials
39
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_xa\_%');
40
+ delete from agent_board.delegate_instances where instance_id like 'iso\_xa\_%';
41
+ delete from agent_board.agents where handle like 'delegate-%' or id = '16591400-0000-4000-8000-000000000001';
42
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
43
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
44
+ }
45
+
46
+ session "hold"
47
+ step "h_begin" {
48
+ BEGIN;
49
+ select (ac.id is not null) as held
50
+ from agent_board.agent_credentials ac
51
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
52
+ where di.instance_id = 'iso_xa_instance_00001'
53
+ order by ac.created_at desc, ac.id desc
54
+ limit 1
55
+ for update of ac;
56
+ }
57
+ step "h_commit" { COMMIT; }
58
+
59
+ session "exch"
60
+ setup { SET deadlock_timeout = '100ms'; }
61
+ step "x_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
62
+ step "x_gen2" {
63
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
64
+ 'iss', 'https://iso-xa.test', 'sub', 'iso-xa-owner', 'slot_resource', null,
65
+ 'agent_board_delegate_instance', jsonb_build_object(
66
+ 'instance_id', 'iso_xa_instance_00001', 'grant_id', 'iso_grant_0000000002', 'generation', 2,
67
+ 'owner_sub', 'iso-xa-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
68
+ }
69
+ step "x_commit" { COMMIT; }
70
+
71
+ session "revoke"
72
+ setup { SET deadlock_timeout = '60s'; }
73
+ step "r_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
74
+ step "r_agent" {
75
+ select revoked_agent_count from agent_board.revoke_agent_subtree(
76
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_xa_instance_00001'),
77
+ '16591400-0000-4000-8000-000000000001', 'iso exchange revoke race');
78
+ }
79
+ step "r_commit" { COMMIT; }
80
+
81
+ session "check"
82
+ step "check" {
83
+ select a.status as delegate_status,
84
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id) as credentials,
85
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id and ac.generation = 2) as generation_2,
86
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id and ac.revoked_at is null) as live
87
+ from agent_board.agents a
88
+ join agent_board.delegate_instances di on di.agent_id = a.id
89
+ where di.instance_id = 'iso_xa_instance_00001';
90
+ }
91
+
92
+ permutation "h_begin" "x_begin" "x_gen2" "r_begin" "r_agent" "h_commit" "x_commit" "r_commit" "check"
@@ -0,0 +1,64 @@
1
+ # HOSTED (v11) SL-2, IF-0-HOSTED-7: two concurrent exchanges of ONE instance at different
2
+ # generations serialise on the instance row lock (`select ... for update`), in both orders.
3
+ #
4
+ # new first: the generation-3 exchange commits; the generation-2 exchange, blocked on the
5
+ # row, then sees r = 3 and is refused generation_superseded.
6
+ # old first: the generation-2 exchange commits; the generation-3 exchange, blocked, then
7
+ # sees r = 2, records 3 and revokes every lower-generation credential.
8
+ #
9
+ # Either way the recorded generation is 3 and the only live credential is generation 3.
10
+ # Without the row lock the generation-2 exchange does not wait for the read, decides against
11
+ # r = 1, and the trace and the final generation both change (the mutation receipt).
12
+
13
+ setup
14
+ {
15
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
16
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-owner', 'slot_resource', null,
17
+ 'agent_board_delegate_instance', jsonb_build_object(
18
+ 'instance_id', 'iso_exchange_race_0001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
19
+ 'owner_sub', 'iso-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
20
+ }
21
+
22
+ teardown
23
+ {
24
+ delete from agent_board.agent_credentials
25
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_%');
26
+ delete from agent_board.delegate_instances where instance_id like 'iso\_%';
27
+ delete from agent_board.agents where handle like 'delegate-%';
28
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
29
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
30
+ }
31
+
32
+ session "new"
33
+ step "new_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
34
+ step "new_exchange" {
35
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
36
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-owner', 'slot_resource', null,
37
+ 'agent_board_delegate_instance', jsonb_build_object(
38
+ 'instance_id', 'iso_exchange_race_0001', 'grant_id', 'iso_grant_0000000003', 'generation', 3,
39
+ 'owner_sub', 'iso-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
40
+ }
41
+ step "new_commit" { COMMIT; }
42
+
43
+ session "old"
44
+ step "old_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
45
+ step "old_exchange" {
46
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
47
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-owner', 'slot_resource', null,
48
+ 'agent_board_delegate_instance', jsonb_build_object(
49
+ 'instance_id', 'iso_exchange_race_0001', 'grant_id', 'iso_grant_0000000002', 'generation', 2,
50
+ 'owner_sub', 'iso-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
51
+ }
52
+ step "old_commit" { COMMIT; }
53
+
54
+ session "check"
55
+ step "check" {
56
+ select di.generation as recorded_generation, di.grant_id as recorded_grant,
57
+ (select string_agg(ac.generation::text, ',' order by ac.generation)
58
+ from agent_board.agent_credentials ac where ac.agent_id = di.agent_id and ac.revoked_at is null) as live_generations,
59
+ (select count(*) from agent_board.agents a where a.handle like 'delegate-%') as delegate_agents
60
+ from agent_board.delegate_instances di where di.instance_id = 'iso_exchange_race_0001';
61
+ }
62
+
63
+ permutation "new_begin" "new_exchange" "old_begin" "old_exchange" "new_commit" "old_commit" "check"
64
+ permutation "old_begin" "old_exchange" "new_begin" "new_exchange" "old_commit" "new_commit" "check"