@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,72 @@
1
+ # HOSTED (v11) SL-2, IF-0-HOSTED-7: concurrent FIRST exchanges of one instance id.
2
+ #
3
+ # Same owner (permutations 1, 2). The exchange takes an owner-keyed advisory lock before it
4
+ # provisions the owner's auth user, so the second waits, then sees the first's committed owner
5
+ # and instance, locks the row, finds the first's agent and takes the refresh path. One
6
+ # instance, one agent, one auth user, two live credentials. Without the owner lock the second
7
+ # provisioned a SECOND auth user for the same owner and was refused owner_mismatch -- the
8
+ # defect this spec found (recorded in plans/evidence/hosted/mutation-receipts.md, SL-2).
9
+ #
10
+ # Different owners presenting the same instance id (permutation 3). The owner locks differ, so
11
+ # here the INSERT-THEN-LOCK is what serialises: the second insert blocks on the primary key
12
+ # until the first commits, then the second locks the committed row and is refused
13
+ # owner_mismatch. With a bare `select ... for update` the second finds no row (the first is
14
+ # uncommitted), tries to create the instance itself, and fails on the primary key instead --
15
+ # the trace changes (the mutation receipt).
16
+
17
+ teardown
18
+ {
19
+ delete from agent_board.agent_credentials
20
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_%');
21
+ delete from agent_board.delegate_instances where instance_id like 'iso\_%';
22
+ delete from agent_board.agents where handle like 'delegate-%';
23
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
24
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
25
+ }
26
+
27
+ session "a"
28
+ step "a_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
29
+ step "a_exchange" {
30
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
31
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-owner', 'slot_resource', null,
32
+ 'agent_board_delegate_instance', jsonb_build_object(
33
+ 'instance_id', 'iso_first_exchange_01', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
34
+ 'owner_sub', 'iso-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
35
+ }
36
+ step "a_commit" { COMMIT; }
37
+
38
+ session "b"
39
+ step "b_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
40
+ step "b_exchange" {
41
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
42
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-owner', 'slot_resource', null,
43
+ 'agent_board_delegate_instance', jsonb_build_object(
44
+ 'instance_id', 'iso_first_exchange_01', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
45
+ 'owner_sub', 'iso-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
46
+ }
47
+ step "b_commit" { COMMIT; }
48
+
49
+ session "intruder"
50
+ step "i_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
51
+ step "i_exchange" {
52
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
53
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-other-owner', 'slot_resource', null,
54
+ 'agent_board_delegate_instance', jsonb_build_object(
55
+ 'instance_id', 'iso_first_exchange_01', 'grant_id', 'iso_grant_0000000009', 'generation', 1,
56
+ 'owner_sub', 'iso-other-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
57
+ }
58
+ step "i_commit" { COMMIT; }
59
+
60
+ session "check"
61
+ step "check" {
62
+ select (select count(*) from agent_board.delegate_instances where instance_id = 'iso_first_exchange_01') as instances,
63
+ (select count(*) from agent_board.agents where handle like 'delegate-%') as delegate_agents,
64
+ (select count(*) from auth.users where email like 'agent+delegate-%') as delegate_auth_users,
65
+ (select count(*) from agent_board.agent_credentials ac
66
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
67
+ where di.instance_id = 'iso_first_exchange_01' and ac.revoked_at is null) as live_credentials;
68
+ }
69
+
70
+ permutation "a_begin" "a_exchange" "b_begin" "b_exchange" "a_commit" "b_commit" "check"
71
+ permutation "b_begin" "b_exchange" "a_begin" "a_exchange" "b_commit" "a_commit" "check"
72
+ permutation "a_begin" "a_exchange" "i_begin" "i_exchange" "a_commit" "i_commit" "check"
@@ -0,0 +1,73 @@
1
+ # HOSTED (v11) fix lane, PR #590 (Codex P2): a FIRST human enroll racing a FIRST delegate
2
+ # exchange for the same (iss, sub), an owner the board has never seen.
3
+ #
4
+ # Both provision the owner's auth user through _ensure_agent_auth_user(_federated_identity_slug
5
+ # (iss, sub)). The exchange's owner advisory lock does not cover enroll_federated_human, so before
6
+ # this fix each transaction looked the slug up, found nothing, and inserted its own auth user: two
7
+ # owners for one human, and the delegate's owner_user_id was not the human's. The fix serialises
8
+ # the miss path inside _ensure_agent_auth_user (a slug-keyed advisory lock, then the lookup again),
9
+ # so whichever provisions second waits, then finds the first's committed user.
10
+ #
11
+ # Both orders: exactly one fed-human auth user, the human row's auth_user_id equals the delegate
12
+ # instance's owner_user_id, and both calls succeed.
13
+ #
14
+ # The "two auth users" observable depends on the isolation substrate's auth.users, which has no
15
+ # unique index on email. A real GoTrue schema may carry one; there the unfixed race ends in a
16
+ # 23505 on the second inserter instead. The lock removes both.
17
+
18
+ setup
19
+ {
20
+ insert into agent_board.identity_providers(
21
+ issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds
22
+ ) values (
23
+ 'https://iso-prov.test', 'message-board', true, true, ARRAY[]::text[], ARRAY[]::text[], ARRAY['human']
24
+ );
25
+ }
26
+
27
+ teardown
28
+ {
29
+ delete from agent_board.agent_credentials
30
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_prov\_%');
31
+ delete from agent_board.delegate_instances where instance_id like 'iso\_prov\_%';
32
+ delete from agent_board.enrollment_federation_audit where issuer = 'https://iso-prov.test';
33
+ delete from agent_board.agents where handle like 'delegate-%' or handle like 'fed-human-%';
34
+ delete from agent_board.identity_providers where issuer = 'https://iso-prov.test';
35
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
36
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
37
+ }
38
+
39
+ session "human"
40
+ step "h_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
41
+ step "h_enroll" {
42
+ select status from agent_board.enroll_federated_human(
43
+ 'https://iso-prov.test', 'message-board', 'iso-prov-owner',
44
+ jsonb_build_object('sub', 'iso-prov-owner'), 'Iso Prov Owner');
45
+ }
46
+ step "h_commit" { COMMIT; }
47
+
48
+ session "exchange"
49
+ step "x_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
50
+ step "x_exchange" {
51
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
52
+ 'iss', 'https://iso-prov.test', 'sub', 'iso-prov-owner', 'slot_resource', null,
53
+ 'agent_board_delegate_instance', jsonb_build_object(
54
+ 'instance_id', 'iso_prov_instance_0001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
55
+ 'owner_sub', 'iso-prov-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
56
+ }
57
+ step "x_commit" { COMMIT; }
58
+
59
+ session "check"
60
+ step "check" {
61
+ select (select count(*) from auth.users
62
+ where email = 'agent+' || agent_board._federated_identity_slug('https://iso-prov.test', 'iso-prov-owner')
63
+ || '@agent-board.local') as owner_auth_users,
64
+ (select count(*) from agent_board.agents
65
+ where handle = agent_board._federated_identity_slug('https://iso-prov.test', 'iso-prov-owner')) as human_rows,
66
+ (select h.auth_user_id = di.owner_user_id
67
+ from agent_board.agents h, agent_board.delegate_instances di
68
+ where h.handle = agent_board._federated_identity_slug('https://iso-prov.test', 'iso-prov-owner')
69
+ and di.instance_id = 'iso_prov_instance_0001') as delegate_owned_by_human;
70
+ }
71
+
72
+ permutation "h_begin" "h_enroll" "x_begin" "x_exchange" "h_commit" "x_commit" "check"
73
+ permutation "x_begin" "x_exchange" "h_begin" "h_enroll" "x_commit" "h_commit" "check"
@@ -0,0 +1,93 @@
1
+ # HOSTED (v11) fix lane, PR #590 (N2): an exchange racing revoke_human_subtree of its owner.
2
+ #
3
+ # revoke_human_subtree takes the owner's auth.users row FOR UPDATE, writes the durable marker
4
+ # (fleet_human_revocations), then revokes every agent it lists as owned by the human -- a list
5
+ # read once, at its loop. The exchange refuses owner_inactive on the marker or a non-active human
6
+ # row, and reads both under FOR KEY SHARE on that same auth.users row, so the two serialise:
7
+ #
8
+ # P1 exchange first (a NEW instance): the revocation waits, then lists and revokes the new
9
+ # delegate too. No active delegate and no live credential survive.
10
+ # P2 revocation first, NEW instance: the exchange waits, then sees the marker and is refused
11
+ # owner_inactive. No instance is written.
12
+ # P3 revocation first, EXISTING instance: the same, and no credential is minted for the
13
+ # delegate the revocation just revoked.
14
+ #
15
+ # Without the row lock the exchange reads the marker and the human row before the revocation
16
+ # commits (neither visible), and only blocks later, on a foreign-key lock: P2 then commits an
17
+ # active delegate the revocation never listed, and P3 a live credential for a revoked agent.
18
+
19
+ setup
20
+ {
21
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
22
+ values ('16310000-0000-0000-0000-000000000001', 'agent', 'agent', 'iso-rev-revoker', 'Iso Rev Revoker', 'active');
23
+ insert into agent_board.identity_providers(
24
+ issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds
25
+ ) values (
26
+ 'https://iso-rev.test', 'message-board', true, true, ARRAY[]::text[], ARRAY[]::text[], ARRAY['human']
27
+ );
28
+ select count(*) from agent_board.enroll_federated_human(
29
+ 'https://iso-rev.test', 'message-board', 'iso-rev-owner',
30
+ jsonb_build_object('sub', 'iso-rev-owner'), 'Iso Rev Owner');
31
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
32
+ 'iss', 'https://iso-rev.test', 'sub', 'iso-rev-owner', 'slot_resource', null,
33
+ 'agent_board_delegate_instance', jsonb_build_object(
34
+ 'instance_id', 'iso_rev_existing_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
35
+ 'owner_sub', 'iso-rev-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
36
+ }
37
+
38
+ teardown
39
+ {
40
+ delete from agent_board.fleet_revocation_events where revoked_by_agent_id = '16310000-0000-0000-0000-000000000001';
41
+ delete from agent_board.fleet_human_revocations where issuer = 'https://iso-rev.test';
42
+ delete from agent_board.agent_credentials
43
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_rev\_%');
44
+ delete from agent_board.delegate_instances where instance_id like 'iso\_rev\_%';
45
+ delete from agent_board.enrollment_federation_audit where issuer = 'https://iso-rev.test';
46
+ delete from agent_board.agents
47
+ where handle like 'delegate-%' or handle like 'fed-human-%' or id = '16310000-0000-0000-0000-000000000001';
48
+ delete from agent_board.identity_providers where issuer = 'https://iso-rev.test';
49
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
50
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
51
+ }
52
+
53
+ session "exchange"
54
+ step "x_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
55
+ step "x_new" {
56
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
57
+ 'iss', 'https://iso-rev.test', 'sub', 'iso-rev-owner', 'slot_resource', null,
58
+ 'agent_board_delegate_instance', jsonb_build_object(
59
+ 'instance_id', 'iso_rev_new_000000001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
60
+ 'owner_sub', 'iso-rev-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
61
+ }
62
+ step "x_existing" {
63
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
64
+ 'iss', 'https://iso-rev.test', 'sub', 'iso-rev-owner', 'slot_resource', null,
65
+ 'agent_board_delegate_instance', jsonb_build_object(
66
+ 'instance_id', 'iso_rev_existing_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
67
+ 'owner_sub', 'iso-rev-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
68
+ }
69
+ step "x_commit" { COMMIT; }
70
+
71
+ session "revoke"
72
+ step "r_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
73
+ step "r_revoke" {
74
+ select revoked_agent_count from agent_board.revoke_human_subtree(
75
+ (select auth_user_id from agent_board.agents
76
+ where handle = agent_board._federated_identity_slug('https://iso-rev.test', 'iso-rev-owner')),
77
+ '16310000-0000-0000-0000-000000000001', 'iso owner revoke race');
78
+ }
79
+ step "r_commit" { COMMIT; }
80
+
81
+ session "check"
82
+ step "check" {
83
+ select (select count(*) from agent_board.fleet_human_revocations where issuer = 'https://iso-rev.test') as markers,
84
+ (select count(*) from agent_board.delegate_instances where instance_id like 'iso\_rev\_%') as instances,
85
+ (select count(*) from agent_board.agents where handle like 'delegate-%' and status = 'active') as active_delegates,
86
+ (select count(*) from agent_board.agent_credentials ac
87
+ join agent_board.delegate_instances di on di.agent_id = ac.agent_id
88
+ where di.instance_id like 'iso\_rev\_%' and ac.revoked_at is null) as live_delegate_credentials;
89
+ }
90
+
91
+ permutation "x_begin" "x_new" "r_begin" "r_revoke" "x_commit" "r_commit" "check"
92
+ permutation "r_begin" "r_revoke" "x_begin" "x_new" "r_commit" "x_commit" "check"
93
+ permutation "r_begin" "r_revoke" "x_begin" "x_existing" "r_commit" "x_commit" "check"
@@ -0,0 +1,59 @@
1
+ # HOSTED (v11) SL-2, IF-0-HOSTED-7/8: an exchange racing a Portal revocation (tombstone) of
2
+ # the same instance. Both take the instance row FOR UPDATE first, so they serialise:
3
+ #
4
+ # exchange first: the generation-2 credential is issued; the tombstone, blocked on the row,
5
+ # then revokes it with every other credential and revokes the agent.
6
+ # revoke first: the tombstone commits; the exchange, blocked, then reads the tombstone and
7
+ # is refused instance_revoked.
8
+ #
9
+ # Either way: tombstoned, no live credential, the agent revoked.
10
+
11
+ setup
12
+ {
13
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
14
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-owner', 'slot_resource', null,
15
+ 'agent_board_delegate_instance', jsonb_build_object(
16
+ 'instance_id', 'iso_revoke_race_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
17
+ 'owner_sub', 'iso-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
18
+ }
19
+
20
+ teardown
21
+ {
22
+ delete from agent_board.agent_credentials
23
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_%');
24
+ delete from agent_board.delegate_instances where instance_id like 'iso\_%';
25
+ delete from agent_board.agents where handle like 'delegate-%';
26
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
27
+ delete from auth.users where email like 'agent+delegate-%' or email like 'agent+fed-human-%';
28
+ }
29
+
30
+ session "exchange"
31
+ step "ex_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
32
+ step "ex_exchange" {
33
+ select count(*) as issued from agent_board.exchange_delegate_credential(jsonb_build_object(
34
+ 'iss', 'https://iso-portal.test', 'sub', 'iso-owner', 'slot_resource', null,
35
+ 'agent_board_delegate_instance', jsonb_build_object(
36
+ 'instance_id', 'iso_revoke_race_00001', 'grant_id', 'iso_grant_0000000002', 'generation', 2,
37
+ 'owner_sub', 'iso-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
38
+ }
39
+ step "ex_commit" { COMMIT; }
40
+
41
+ session "revoke"
42
+ step "rv_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
43
+ step "rv_tombstone" {
44
+ select first from agent_board.tombstone_delegate_instance(
45
+ 'iso_revoke_race_00001', 'https://iso-portal.test', 'iso-owner', 'owner_revoked');
46
+ }
47
+ step "rv_commit" { COMMIT; }
48
+
49
+ session "check"
50
+ step "check" {
51
+ select (di.tombstoned_at is not null) as tombstoned,
52
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = di.agent_id and ac.revoked_at is null) as live_credentials,
53
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id = di.agent_id) as all_credentials,
54
+ (select a.status from agent_board.agents a where a.id = di.agent_id) as agent_status
55
+ from agent_board.delegate_instances di where di.instance_id = 'iso_revoke_race_00001';
56
+ }
57
+
58
+ permutation "ex_begin" "ex_exchange" "rv_begin" "rv_tombstone" "ex_commit" "rv_commit" "check"
59
+ permutation "rv_begin" "rv_tombstone" "ex_begin" "ex_exchange" "rv_commit" "ex_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"
@@ -0,0 +1,145 @@
1
+ # HOSTED (v11) fix lane, PR #590 (Codex P1): an owner's withdrawal of its delegate (D7) racing
2
+ # a board admin's eviction of that delegate, and the pre-existing same-shape self-leave race.
3
+ #
4
+ # update_board_membership decides the transition from the target's CURRENT state, then writes
5
+ # through the sink. The D7 branch refuses `removed -> left` so an eviction is never laundered
6
+ # into a departure the owner can undo (R-LINK-7). That read was unlocked: with the admin's
7
+ # `removed` uncommitted, the owner read `joined`, passed, blocked on the sink's row lock, and
8
+ # after the admin committed wrote `left` over `removed` -- then re-added the evicted delegate.
9
+ # The read is now FOR UPDATE, so the decision is made on the state the write replaces.
10
+ #
11
+ # P1 admin evicts first: the owner's withdrawal waits, then sees `removed` and is refused; the
12
+ # delegate stays removed and the owner's re-add is refused.
13
+ # P2 owner withdraws first: the admin's eviction waits, then moves `left` to `removed`; same
14
+ # final state, same refusal.
15
+ # P3 the same race for an ordinary member's self-leave (the branch that shares the read): the
16
+ # self-leave waits, then is refused; the member stays removed.
17
+
18
+ setup
19
+ {
20
+ insert into auth.users(id, instance_id, aud, role, email) values
21
+ ('16320000-0000-4000-8000-000000000001', '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'iso-wd-admin@t.test'),
22
+ ('16320000-0000-4000-8000-000000000002', '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated', 'iso-wd-member@t.test');
23
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id) values
24
+ ('16321000-0000-4000-8000-000000000001', 'agent', 'agent', 'iso-wd-admin', 'Iso Wd Admin', 'active', '16320000-0000-4000-8000-000000000001'),
25
+ ('16321000-0000-4000-8000-000000000002', 'agent', 'agent', 'iso-wd-member', 'Iso Wd Member', 'active', '16320000-0000-4000-8000-000000000002');
26
+ insert into agent_board.identity_providers(
27
+ issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds
28
+ ) values (
29
+ 'https://iso-wd.test', 'message-board', true, true, ARRAY[]::text[], ARRAY[]::text[], ARRAY['human']
30
+ );
31
+ select count(*) from agent_board.enroll_federated_human(
32
+ 'https://iso-wd.test', 'message-board', 'iso-wd-owner',
33
+ jsonb_build_object('sub', 'iso-wd-owner'), 'Iso Wd Owner');
34
+ select count(*) from agent_board.exchange_delegate_credential(jsonb_build_object(
35
+ 'iss', 'https://iso-wd.test', 'sub', 'iso-wd-owner', 'slot_resource', null,
36
+ 'agent_board_delegate_instance', jsonb_build_object(
37
+ 'instance_id', 'iso_wd_instance_00001', 'grant_id', 'iso_grant_0000000001', 'generation', 1,
38
+ 'owner_sub', 'iso-wd-owner', 'client_id', 'iso-client', 'status', 'active', 'client_change', null)));
39
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
40
+ values ('16322000-0000-4000-8000-000000000001', 'iso-wd-board', 'Iso Wd Board',
41
+ '16321000-0000-4000-8000-000000000001', 'private', 'active');
42
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
43
+ select '16322000-0000-4000-8000-000000000001'::uuid, a.id, r.role, 'joined', '16321000-0000-4000-8000-000000000001'::uuid
44
+ from (values ('iso-wd-admin', 'owner'), ('iso-wd-member', 'member')) r(handle, role)
45
+ join agent_board.agents a on a.handle = r.handle
46
+ union all
47
+ select '16322000-0000-4000-8000-000000000001'::uuid, a.id, 'member', 'joined', '16321000-0000-4000-8000-000000000001'::uuid
48
+ from agent_board.agents a where a.handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner');
49
+ do $$
50
+ begin
51
+ perform set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
52
+ (select auth_user_id from agent_board.agents
53
+ where handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner')))::text, true);
54
+ perform agent_board.add_delegate_to_board(
55
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
56
+ '16322000-0000-4000-8000-000000000001');
57
+ end $$;
58
+ }
59
+
60
+ teardown
61
+ {
62
+ delete from agent_board.board_membership_events where board_id = '16322000-0000-4000-8000-000000000001';
63
+ delete from agent_board.board_memberships where board_id = '16322000-0000-4000-8000-000000000001';
64
+ delete from agent_board.boards where id = '16322000-0000-4000-8000-000000000001';
65
+ delete from agent_board.agent_credentials
66
+ where agent_id in (select agent_id from agent_board.delegate_instances where instance_id like 'iso\_wd\_%');
67
+ delete from agent_board.delegate_instances where instance_id like 'iso\_wd\_%';
68
+ delete from agent_board.enrollment_federation_audit where issuer = 'https://iso-wd.test';
69
+ delete from agent_board.agents
70
+ where handle like 'delegate-%' or handle like 'fed-human-%' or handle in ('iso-wd-admin', 'iso-wd-member');
71
+ delete from agent_board.identity_providers where issuer = 'https://iso-wd.test';
72
+ delete from auth.identities where provider_id like 'delegate-%' or provider_id like 'fed-human-%';
73
+ delete from auth.users
74
+ where email like 'agent+delegate-%' or email like 'agent+fed-human-%'
75
+ or id in ('16320000-0000-4000-8000-000000000001', '16320000-0000-4000-8000-000000000002');
76
+ }
77
+
78
+ session "admin"
79
+ step "a_begin" {
80
+ BEGIN;
81
+ select set_config('request.jwt.claims',
82
+ json_build_object('role', 'authenticated', 'sub', '16320000-0000-4000-8000-000000000001')::text, true) is not null as admin_session;
83
+ }
84
+ step "a_evict" {
85
+ select membership_state from agent_board.update_board_membership(
86
+ '16322000-0000-4000-8000-000000000001',
87
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
88
+ 'removed');
89
+ }
90
+ step "a_evict_member" {
91
+ select membership_state from agent_board.update_board_membership(
92
+ '16322000-0000-4000-8000-000000000001', '16321000-0000-4000-8000-000000000002', 'removed');
93
+ }
94
+ step "a_commit" { COMMIT; }
95
+
96
+ session "owner"
97
+ step "o_begin" {
98
+ BEGIN;
99
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
100
+ (select auth_user_id from agent_board.agents
101
+ where handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner')))::text, true) is not null as owner_session;
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
+ step "o_commit" { COMMIT; }
110
+ step "o_readd" {
111
+ select set_config('request.jwt.claims', json_build_object('role', 'authenticated', 'sub',
112
+ (select auth_user_id from agent_board.agents
113
+ where handle = agent_board._federated_identity_slug('https://iso-wd.test', 'iso-wd-owner')))::text, false) is not null as owner_session;
114
+ select membership_state from agent_board.add_delegate_to_board(
115
+ (select agent_id from agent_board.delegate_instances where instance_id = 'iso_wd_instance_00001'),
116
+ '16322000-0000-4000-8000-000000000001');
117
+ }
118
+
119
+ session "member"
120
+ step "m_begin" {
121
+ BEGIN;
122
+ select set_config('request.jwt.claims',
123
+ json_build_object('role', 'authenticated', 'sub', '16320000-0000-4000-8000-000000000002')::text, true) is not null as member_session;
124
+ }
125
+ step "m_leave" {
126
+ select membership_state from agent_board.update_board_membership(
127
+ '16322000-0000-4000-8000-000000000001', '16321000-0000-4000-8000-000000000002', 'left');
128
+ }
129
+ step "m_commit" { COMMIT; }
130
+
131
+ session "check"
132
+ step "check" {
133
+ select (select bm.membership_state from agent_board.board_memberships bm
134
+ join agent_board.delegate_instances di on di.agent_id = bm.agent_id
135
+ where bm.board_id = '16322000-0000-4000-8000-000000000001' and di.instance_id = 'iso_wd_instance_00001'
136
+ order by bm.created_at desc, bm.id desc limit 1) as delegate_state,
137
+ (select bm.membership_state from agent_board.board_memberships bm
138
+ where bm.board_id = '16322000-0000-4000-8000-000000000001'
139
+ and bm.agent_id = '16321000-0000-4000-8000-000000000002'
140
+ order by bm.created_at desc, bm.id desc limit 1) as member_state;
141
+ }
142
+
143
+ permutation "a_begin" "a_evict" "o_begin" "o_withdraw" "a_commit" "o_commit" "check" "o_readd" "check"
144
+ permutation "o_begin" "o_withdraw" "a_begin" "a_evict" "o_commit" "a_commit" "check" "o_readd" "check"
145
+ permutation "a_begin" "a_evict_member" "m_begin" "m_leave" "a_commit" "m_commit" "check"