@consiliency/agent-board-schema 1.57.0 → 1.58.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 (29) hide show
  1. package/fixtures/already-applied.json +4 -0
  2. package/migrations/20261008000100_agent_board_hosted_delegates.sql +3152 -0
  3. package/package.json +1 -1
  4. package/schema-manifest.json +12 -3
  5. package/src/tooling.test.mjs +5 -2
  6. package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -0
  7. package/tests/agent_board_consent_emission.test.sql +4 -2
  8. package/tests/agent_board_delegate_role_class.test.sql +736 -0
  9. package/tests/agent_board_federated_identity_injective_slug.test.sql +5 -2
  10. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +71 -14
  11. package/tests/agent_board_fk_lock_edges.test.sql +9 -0
  12. package/tests/agent_board_hosted_delegates.test.sql +1134 -0
  13. package/tests/agent_board_ledger_authority.test.sql +34 -5
  14. package/tests/agent_board_signer_lifecycle.test.sql +3 -0
  15. package/tests/agent_board_write_enforcement_class.test.sql +8 -3
  16. package/tests/catalog/function_execute_privileges.json +260 -2
  17. package/tests/catalog/p_metadata_parameters.json +40 -0
  18. package/tests/isolation/expected/hosted_delegate_exchange_race.out +93 -0
  19. package/tests/isolation/expected/hosted_delegate_first_exchange_race.out +129 -0
  20. package/tests/isolation/expected/hosted_delegate_owner_provisioning_race.out +91 -0
  21. package/tests/isolation/expected/hosted_delegate_owner_revoke_race.out +137 -0
  22. package/tests/isolation/expected/hosted_delegate_revoke_race.out +87 -0
  23. package/tests/isolation/expected/hosted_delegate_withdraw_evict_race.out +235 -0
  24. package/tests/isolation/specs/hosted_delegate_exchange_race.spec +64 -0
  25. package/tests/isolation/specs/hosted_delegate_first_exchange_race.spec +72 -0
  26. package/tests/isolation/specs/hosted_delegate_owner_provisioning_race.spec +73 -0
  27. package/tests/isolation/specs/hosted_delegate_owner_revoke_race.spec +93 -0
  28. package/tests/isolation/specs/hosted_delegate_revoke_race.spec +59 -0
  29. package/tests/isolation/specs/hosted_delegate_withdraw_evict_race.spec +145 -0
@@ -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"
@@ -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,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"