@consiliency/agent-board-schema 1.58.0 → 1.60.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.
@@ -0,0 +1,66 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: h_begin x_begin x_gen2 r_begin r_agent h_commit x_commit r_commit check
4
+ count
5
+ -----
6
+ 1
7
+ (1 row)
8
+
9
+ step h_begin:
10
+ BEGIN;
11
+ select (ac.id is not null) as held
12
+ from agent_board.agent_credentials ac
13
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
14
+ where di.instance_id = 'iso_xa_instance_00001'
15
+ order by ac.created_at desc, ac.id desc
16
+ limit 1
17
+ for update of ac;
18
+
19
+ held
20
+ ----
21
+ t
22
+ (1 row)
23
+
24
+ step x_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
25
+ step x_gen2:
26
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
27
+ 'iss', 'https://iso-xa.test', 'sub', 'iso-xa-owner', 'slot_resource', null,
28
+ 'agent_board_delegate_instance', jsonb_build_object(
29
+ 'instance_id', 'iso_xa_instance_00001', 'grant_id', 'iso_grant_0000000002', 'generation', 2,
30
+ 'owner_sub', 'iso-xa-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
31
+ <waiting ...>
32
+ step r_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
33
+ step r_agent:
34
+ select revoked_agent_count from agent_board.revoke_agent_subtree(
35
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_xa_instance_00001'),
36
+ '16591400-0000-4000-8000-000000000001', 'iso exchange revoke race');
37
+ <waiting ...>
38
+ step h_commit: COMMIT;
39
+ step x_gen2: <... completed>
40
+ issued
41
+ ------
42
+ 1
43
+ (1 row)
44
+
45
+ step x_commit: COMMIT;
46
+ step r_agent: <... completed>
47
+ revoked_agent_count
48
+ -------------------
49
+ 1
50
+ (1 row)
51
+
52
+ step r_commit: COMMIT;
53
+ step check:
54
+ select a.status as delegate_status,
55
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id) as credentials,
56
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id and ac.generation = 2) as generation_2,
57
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = a.id and ac.revoked_at is null) as live
58
+ from agent_board.agents a
59
+ join agent_board.delegate_instances di on di.agent_id = a.id
60
+ where di.instance_id = 'iso_xa_instance_00001';
61
+
62
+ delegate_status|credentials|generation_2|live
63
+ ---------------+-----------+------------+----
64
+ revoked | 3| 1| 0
65
+ (1 row)
66
+
@@ -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,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,158 @@
1
+ # HOSTED follow-ups (Consiliency/message-board#591, items 8 and 4): tombstone_delegate_instance
2
+ # racing the revokers of the same delegate.
3
+ #
4
+ # The tombstone used to take the delegate's credentials first (its revoke UPDATE) and the
5
+ # delegate's agents row last (the membership event's foreign key, then its status UPDATE).
6
+ # _revoke_agent_principal -- the per-agent step of revoke_human_subtree and revoke_agent_subtree --
7
+ # takes the agents row FOR UPDATE first and the credentials after: ABBA. The tombstone now does its
8
+ # status UPDATE of the agents row (FOR NO KEY UPDATE) right after the instance row, before any
9
+ # credential. It takes NO lock on the owner's auth.users row: it reaches boards, and a board+owner
10
+ # route must take the board first (agent_board_federated_lock_order_class EC-3).
11
+ #
12
+ # P1 and P2 park the tombstone mid-function deterministically: the "hold" session holds the
13
+ # delegate's board_memberships row FOR UPDATE, which update_board_membership (called by the
14
+ # tombstone after it has revoked the credentials) waits for. The revoker is started while the
15
+ # tombstone is parked, then the hold is released.
16
+ #
17
+ # P1 revoke_human_subtree of the owner. Unfixed: the revocation takes the delegate's agents row
18
+ # and waits on the credentials the tombstone holds; released, the tombstone waits on that
19
+ # agents row: deadlock, the tombstone is the victim (its deadlock_timeout is the shorter) and
20
+ # rolls back. Fixed: the revocation waits on the agents row, and both commit: the instance
21
+ # is tombstoned AND the delegate revoked.
22
+ # P2 revoke_agent_subtree of the delegate itself. The same deadlock unfixed; the same wait fixed.
23
+ # P3 RECORDS A STATED RESIDUAL (AB15), it does not pin a fix: a pre-exchange tombstone (a NEW
24
+ # instance) racing revoke_human_subtree of an owner with no other evidence. The revocation
25
+ # does not wait for the tombstone, reads delegate_instances before the insert commits, and
26
+ # writes no marker (markers = 0). Reachable only for an owner whose auth user exists with no
27
+ # audit row, no human agents row and no recorded pair -- i.e. pre-1.59.0 evidence only -- so
28
+ # setup provisions the auth user directly. Closing it needs an owner-row lock in the
29
+ # tombstone, which EC-3 forbids (it would deadlock against enroll_federated_human). If a
30
+ # change makes this read 1, the residual closed: update AB15 and this golden together.
31
+ #
32
+ # What each permutation discriminates (measured, plans/evidence/hosted/mutation-receipts.md,
33
+ # section "#591 follow-ups"): P1 and P2 deadlock with the status UPDATE moved back after the
34
+ # memberships.
35
+
36
+ setup
37
+ {
38
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
39
+ values ('16591000-0000-4000-8000-000000000001', 'agent', 'agent', 'iso-tr-revoker', 'Iso Tr Revoker', 'active');
40
+ insert into agent_board.identity_providers(
41
+ issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds
42
+ ) values (
43
+ 'https://iso-tr.test', 'message-board', true, true, ARRAY[]::text[], ARRAY[]::text[], ARRAY['human']
44
+ );
45
+ select count(*) from agent_board.enroll_federated_human(
46
+ 'https://iso-tr.test', 'message-board', 'iso-tr-owner',
47
+ jsonb_build_object('sub', 'iso-tr-owner'), 'Iso Tr Owner');
48
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
49
+ 'iss', 'https://iso-tr.test', 'sub', 'iso-tr-owner', 'slot_resource', null,
50
+ 'agent_board_delegate_instance', jsonb_build_object(
51
+ 'instance_id', 'iso_tr_instance_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
52
+ 'owner_sub', 'iso-tr-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
53
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
54
+ values ('16591200-0000-4000-8000-000000000001', 'iso-tr-board', 'Iso Tr Board',
55
+ '16591000-0000-4000-8000-000000000001', 'private', 'active');
56
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
57
+ select '16591200-0000-4000-8000-000000000001'::uuid, a.id, 'member', 'joined', '16591000-0000-4000-8000-000000000001'::uuid
58
+ from agent_board.agents a where a.handle = agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-owner');
59
+ do $$
60
+ begin
61
+ perform set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
62
+ (select auth_user_id from agent_board.agents
63
+ where handle = agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-owner')))::text, true);
64
+ perform agent_board.add_delegate_to_board(
65
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_tr_instance_00001'),
66
+ '16591200-0000-4000-8000-000000000001');
67
+ end $$;
68
+ -- P3's owner: an auth user and nothing else (see the header).
69
+ select agent_board._ensure_agent_auth_user(agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-bare')) is not null as bare_owner;
70
+ }
71
+
72
+ teardown
73
+ {
74
+ delete from agent_board.fleet_revocation_events where revoked_by_agent_id = '16591000-0000-4000-8000-000000000001';
75
+ delete from agent_board.fleet_human_revocations where issuer = 'https://iso-tr.test';
76
+ delete from agent_board.board_membership_events where board_id = '16591200-0000-4000-8000-000000000001';
77
+ delete from agent_board.board_memberships where board_id = '16591200-0000-4000-8000-000000000001';
78
+ delete from agent_board.boards where id = '16591200-0000-4000-8000-000000000001';
79
+ delete from agent_board.agent_credentials
80
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_tr\_%');
81
+ delete from agent_board.delegate_instances where instance_id like 'iso\_tr\_%';
82
+ delete from agent_board.enrollment_federation_audit where issuer = 'https://iso-tr.test';
83
+ delete from agent_board.agents
84
+ where handle like 'delegate-%' or handle like 'fed-human-%' or id = '16591000-0000-4000-8000-000000000001';
85
+ delete from agent_board.identity_providers where issuer = 'https://iso-tr.test';
86
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
87
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
88
+ }
89
+
90
+ session "hold"
91
+ step "h_begin" {
92
+ BEGIN;
93
+ select (bm.id is not null) as held 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_tr_instance_00001' and bm.membership_state = 'joined'
96
+ for update of bm;
97
+ }
98
+ step "h_commit" { COMMIT; }
99
+
100
+ session "tomb"
101
+ setup { SET deadlock_timeout = '100ms'; }
102
+ step "t_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
103
+ step "t_tomb" {
104
+ select first from agent_board.tombstone_delegate_instance(
105
+ 'iso_tr_instance_00001', 'https://iso-tr.test', 'iso-tr-owner', 'owner_revoked');
106
+ }
107
+ step "t_tomb_bare" {
108
+ select first from agent_board.tombstone_delegate_instance(
109
+ 'iso_tr_bare_instance_01', 'https://iso-tr.test', 'iso-tr-bare', 'owner_revoked');
110
+ }
111
+ step "t_commit" { COMMIT; }
112
+
113
+ session "revoke"
114
+ setup { SET deadlock_timeout = '60s'; }
115
+ step "r_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
116
+ step "r_human" {
117
+ select revoked_agent_count from agent_board.revoke_human_subtree(
118
+ (select auth_user_id from agent_board.agents
119
+ where handle = agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-owner')),
120
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
121
+ }
122
+ step "r_agent" {
123
+ select revoked_agent_count from agent_board.revoke_agent_subtree(
124
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_tr_instance_00001'),
125
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
126
+ }
127
+ step "r_human_bare" {
128
+ select revoked_agent_count from agent_board.revoke_human_subtree(
129
+ (select id from auth.users
130
+ where email = 'agent+' || agent_board._federated_identity_slug('https://iso-tr.test', 'iso-tr-bare') || '@agent-board.local'),
131
+ '16591000-0000-4000-8000-000000000001', 'iso tombstone revoke race');
132
+ }
133
+ step "r_commit" { COMMIT; }
134
+
135
+ session "check"
136
+ step "check" {
137
+ select (select (di.tombstoned_at is not null)::text from agent_board.delegate_instances di
138
+ where di.instance_id = 'iso_tr_instance_00001') as tombstoned,
139
+ (select a.status from agent_board.agents a
140
+ join agent_board.delegate_instances di on di.agent_id = a.id
141
+ where di.instance_id = 'iso_tr_instance_00001') as delegate_status,
142
+ (select count(*) from agent_board.agent_credentials ac
143
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
144
+ where di.instance_id = 'iso_tr_instance_00001' and ac.revoked_at is null) as live_credentials,
145
+ (select count(*) from agent_board.board_memberships bm
146
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
147
+ where di.instance_id = 'iso_tr_instance_00001' and bm.membership_state in ('joined', 'subscribed')) as active_memberships;
148
+ }
149
+ step "check_bare" {
150
+ select (select count(*) from agent_board.delegate_instances where instance_id = 'iso_tr_bare_instance_01'
151
+ and tombstoned_at is not null and owner_subject = 'iso-tr-bare') as tombstoned_recorded,
152
+ (select count(*) from agent_board.fleet_human_revocations
153
+ where issuer = 'https://iso-tr.test' and subject = 'iso-tr-bare') as markers;
154
+ }
155
+
156
+ permutation "h_begin" "t_begin" "t_tomb" "r_begin" "r_human" "h_commit" "t_commit" "r_commit" "check"
157
+ permutation "h_begin" "t_begin" "t_tomb" "r_begin" "r_agent" "h_commit" "t_commit" "r_commit" "check"
158
+ permutation "t_begin" "t_tomb_bare" "r_begin" "r_human_bare" "t_commit" "r_commit" "check_bare"