@consiliency/agent-board-schema 1.22.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -1
- package/fixtures/already-applied.json +40 -0
- package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/migrations/20260905000100_agent_board_consent_gating_reconcile.sql +151 -0
- package/package.json +1 -1
- package/schema-manifest.json +93 -3
- package/src/check.js +23 -0
- package/src/index.js +20 -0
- package/src/plan.js +79 -5
- package/src/tooling.test.mjs +237 -4
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +46 -2
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- package/tests/agent_board_inbox_reads.test.sql +16 -19
- package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +1 -1
- package/tests/agent_board_roomlink.test.sql +2 -2
- package/tests/agent_board_wake_recovery.test.sql +756 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
- package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Consiliency/message-board#162 — two first composed fleet onboardings for one
|
|
2
|
+
# bounded invite serialize and converge on the same complete authority chain.
|
|
3
|
+
|
|
4
|
+
setup
|
|
5
|
+
{
|
|
6
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
7
|
+
values ('16220000-0000-0000-0000-000000000001', 'agent', 'agent',
|
|
8
|
+
'atomic-race-owner', 'Atomic Race Owner', 'active');
|
|
9
|
+
|
|
10
|
+
select count(*) from agent_board.create_board(
|
|
11
|
+
'16220000-0000-0000-0000-000000000001',
|
|
12
|
+
'atomic-race-board', 'Atomic Race Board',
|
|
13
|
+
'Consiliency/message-board#162 isolation proof', jsonb_build_object(), 'private'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
insert into agent_board.fleet_trust_domains(
|
|
17
|
+
id, slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at
|
|
18
|
+
) values (
|
|
19
|
+
'16220000-0000-0000-0000-000000000002', 'atomic-race-domain',
|
|
20
|
+
'Atomic Race Domain', 'tailnet', '16220000-0000-0000-0000-000000000001',
|
|
21
|
+
'active', now()
|
|
22
|
+
);
|
|
23
|
+
insert into agent_board.fleet_machines(
|
|
24
|
+
id, trust_domain_id, owner_agent_id, machine_label, machine_fingerprint,
|
|
25
|
+
lifecycle_state, enrollment_source, enrolled_at
|
|
26
|
+
) values (
|
|
27
|
+
'16220000-0000-0000-0000-000000000003',
|
|
28
|
+
'16220000-0000-0000-0000-000000000002',
|
|
29
|
+
'16220000-0000-0000-0000-000000000001',
|
|
30
|
+
'claw', 'sha256:atomic-race-machine', 'active', 'portal', now()
|
|
31
|
+
);
|
|
32
|
+
insert into agent_board.identity_providers(
|
|
33
|
+
issuer, audience, supported, policy_approved,
|
|
34
|
+
allowed_scopes, allowed_board_scopes, allowed_agent_kinds
|
|
35
|
+
) values (
|
|
36
|
+
'https://atomic-race.test', 'message-board', true, true,
|
|
37
|
+
ARRAY['board:send'], ARRAY[]::text[], ARRAY['agent']
|
|
38
|
+
);
|
|
39
|
+
select count(*) from agent_board.create_fleet_onboarding_invite(
|
|
40
|
+
(select id from agent_board.boards where slug = 'atomic-race-board'),
|
|
41
|
+
'16220000-0000-0000-0000-000000000003',
|
|
42
|
+
'codex', 'phase-loop', 'message-board', 'sha256:atomic-race-repo',
|
|
43
|
+
'worker', 'phase_worker', 'atomic-race-agent', 1,
|
|
44
|
+
now() + interval '1 hour', '16220000-0000-0000-0000-000000000001',
|
|
45
|
+
jsonb_build_object()
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
session "first"
|
|
50
|
+
step "first_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
51
|
+
step "first_onboard" {
|
|
52
|
+
select identity_disposition, membership_disposition, binding_disposition
|
|
53
|
+
from agent_board.onboard_federated_fleet_agent(
|
|
54
|
+
'https://atomic-race.test', 'message-board', 'human-race',
|
|
55
|
+
jsonb_build_object('iss','https://atomic-race.test','aud','message-board','sub','human-race','iat',100),
|
|
56
|
+
'atomic-race-agent',
|
|
57
|
+
'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0',
|
|
58
|
+
'raw_ed25519_base64url',
|
|
59
|
+
'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
|
|
60
|
+
true, null, 'agent', ARRAY['board:send'], ARRAY[]::text[],
|
|
61
|
+
(select code from agent_board.board_invite_tokens where created_by_agent_id = '16220000-0000-0000-0000-000000000001'),
|
|
62
|
+
(select id from agent_board.boards where slug = 'atomic-race-board'),
|
|
63
|
+
'16220000-0000-0000-0000-000000000003',
|
|
64
|
+
'codex', 'phase-loop', 'message-board', 'sha256:atomic-race-repo',
|
|
65
|
+
'worker', 'phase_worker', 'atomic-race-agent'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
step "first_commit" { COMMIT; }
|
|
69
|
+
|
|
70
|
+
session "second"
|
|
71
|
+
step "second_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
72
|
+
step "second_onboard" {
|
|
73
|
+
select identity_disposition, membership_disposition, binding_disposition
|
|
74
|
+
from agent_board.onboard_federated_fleet_agent(
|
|
75
|
+
'https://atomic-race.test', 'message-board', 'human-race',
|
|
76
|
+
jsonb_build_object('iss','https://atomic-race.test','aud','message-board','sub','human-race','iat',200,'jti','rotated'),
|
|
77
|
+
'atomic-race-agent',
|
|
78
|
+
'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0',
|
|
79
|
+
'raw_ed25519_base64url',
|
|
80
|
+
'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
|
|
81
|
+
true, null, 'agent', ARRAY['board:send'], ARRAY[]::text[],
|
|
82
|
+
(select code from agent_board.board_invite_tokens where created_by_agent_id = '16220000-0000-0000-0000-000000000001'),
|
|
83
|
+
(select id from agent_board.boards where slug = 'atomic-race-board'),
|
|
84
|
+
'16220000-0000-0000-0000-000000000003',
|
|
85
|
+
'codex', 'phase-loop', 'message-board', 'sha256:atomic-race-repo',
|
|
86
|
+
'worker', 'phase_worker', 'atomic-race-agent'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
step "second_commit" { COMMIT; }
|
|
90
|
+
|
|
91
|
+
session "check"
|
|
92
|
+
step "check" {
|
|
93
|
+
select count(*) as agents,
|
|
94
|
+
(select count(*) from agent_board.agent_signing_keys k
|
|
95
|
+
join agent_board.agents a on a.id=k.agent_id
|
|
96
|
+
where a.handle='atomic-race-agent') as keys,
|
|
97
|
+
(select count(*) from agent_board.board_invite_redemptions r
|
|
98
|
+
join agent_board.board_invite_tokens i on i.id=r.invite_id
|
|
99
|
+
where i.created_by_agent_id='16220000-0000-0000-0000-000000000001') as redemptions,
|
|
100
|
+
(select count(*) from agent_board.fleet_machine_agents b
|
|
101
|
+
join agent_board.agents a on a.id=b.agent_id
|
|
102
|
+
where a.handle='atomic-race-agent') as bindings
|
|
103
|
+
from agent_board.agents where handle='atomic-race-agent';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
permutation "first_begin" "first_onboard" "second_begin" "second_onboard" "first_commit" "second_commit" "check"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Consiliency/message-board#162 — a complete fleet retry and human-subtree
|
|
2
|
+
# revocation can touch both the invite creator and enrolled agent. Hold the
|
|
3
|
+
# fingerprint lock to pause onboarding before its principal locks, then prove
|
|
4
|
+
# the subtree's enrolled-agent-before-creator UUID order cannot deadlock it.
|
|
5
|
+
|
|
6
|
+
setup
|
|
7
|
+
{
|
|
8
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
9
|
+
values ('ffffffff-ffff-ffff-ffff-ffffffffff01', 'agent', 'agent',
|
|
10
|
+
'atomic-subtree-owner', 'Atomic Subtree Owner', 'active');
|
|
11
|
+
select count(*) from agent_board.create_board(
|
|
12
|
+
'ffffffff-ffff-ffff-ffff-ffffffffff01',
|
|
13
|
+
'atomic-subtree-board', 'Atomic Subtree Board',
|
|
14
|
+
'Consiliency/message-board#162 subtree lock-order proof',
|
|
15
|
+
jsonb_build_object(), 'private'
|
|
16
|
+
);
|
|
17
|
+
insert into agent_board.fleet_trust_domains(
|
|
18
|
+
id, slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at
|
|
19
|
+
) values (
|
|
20
|
+
'16270000-0000-0000-0000-000000000002', 'atomic-subtree-domain',
|
|
21
|
+
'Atomic Subtree Domain', 'tailnet', 'ffffffff-ffff-ffff-ffff-ffffffffff01',
|
|
22
|
+
'active', now()
|
|
23
|
+
);
|
|
24
|
+
insert into agent_board.fleet_machines(
|
|
25
|
+
id, trust_domain_id, owner_agent_id, machine_label, machine_fingerprint,
|
|
26
|
+
lifecycle_state, enrollment_source, enrolled_at
|
|
27
|
+
) values (
|
|
28
|
+
'16270000-0000-0000-0000-000000000003',
|
|
29
|
+
'16270000-0000-0000-0000-000000000002',
|
|
30
|
+
'ffffffff-ffff-ffff-ffff-ffffffffff01',
|
|
31
|
+
'claw', 'sha256:atomic-subtree-machine', 'active', 'portal', now()
|
|
32
|
+
);
|
|
33
|
+
insert into agent_board.identity_providers(
|
|
34
|
+
issuer, audience, supported, policy_approved,
|
|
35
|
+
allowed_scopes, allowed_board_scopes, allowed_agent_kinds
|
|
36
|
+
) values (
|
|
37
|
+
'https://atomic-subtree.test', 'message-board', true, true,
|
|
38
|
+
ARRAY['board:send'], ARRAY[]::text[], ARRAY['agent']
|
|
39
|
+
);
|
|
40
|
+
select count(*) from agent_board.create_fleet_onboarding_invite(
|
|
41
|
+
(select id from agent_board.boards where slug='atomic-subtree-board'),
|
|
42
|
+
'16270000-0000-0000-0000-000000000003', 'codex', 'phase-loop',
|
|
43
|
+
'message-board', 'sha256:atomic-subtree-repo', 'worker', 'phase_worker',
|
|
44
|
+
'atomic-subtree-agent', 1, now() + interval '1 hour',
|
|
45
|
+
'ffffffff-ffff-ffff-ffff-ffffffffff01', jsonb_build_object()
|
|
46
|
+
);
|
|
47
|
+
select count(*) from agent_board.onboard_federated_fleet_agent(
|
|
48
|
+
'https://atomic-subtree.test', 'message-board', 'human-subtree',
|
|
49
|
+
jsonb_build_object('iss','https://atomic-subtree.test','aud','message-board','sub','human-subtree'),
|
|
50
|
+
'atomic-subtree-agent',
|
|
51
|
+
'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk',
|
|
52
|
+
'raw_ed25519_base64url',
|
|
53
|
+
'sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA',
|
|
54
|
+
true, null, 'agent', ARRAY['board:send'], ARRAY[]::text[],
|
|
55
|
+
(select code from agent_board.board_invite_tokens where created_by_agent_id='ffffffff-ffff-ffff-ffff-ffffffffff01'),
|
|
56
|
+
(select id from agent_board.boards where slug='atomic-subtree-board'),
|
|
57
|
+
'16270000-0000-0000-0000-000000000003', 'codex', 'phase-loop',
|
|
58
|
+
'message-board', 'sha256:atomic-subtree-repo', 'worker', 'phase_worker',
|
|
59
|
+
'atomic-subtree-agent'
|
|
60
|
+
);
|
|
61
|
+
update agent_board.agents
|
|
62
|
+
set owner_user_id = (
|
|
63
|
+
select owner_user_id from agent_board.agents where handle='atomic-subtree-agent'
|
|
64
|
+
)
|
|
65
|
+
where id='ffffffff-ffff-ffff-ffff-ffffffffff01';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
teardown
|
|
69
|
+
{
|
|
70
|
+
delete from agent_board.fleet_human_revocations;
|
|
71
|
+
delete from agent_board.fleet_revocation_events where agent_id in (
|
|
72
|
+
select id from agent_board.agents where handle in ('atomic-subtree-owner','atomic-subtree-agent')
|
|
73
|
+
);
|
|
74
|
+
delete from agent_board.fleet_machine_agents where machine_id='16270000-0000-0000-0000-000000000003';
|
|
75
|
+
delete from agent_board.fleet_repo_scopes where harness_registration_id in (
|
|
76
|
+
select id from agent_board.fleet_harness_registrations where machine_id='16270000-0000-0000-0000-000000000003'
|
|
77
|
+
);
|
|
78
|
+
delete from agent_board.fleet_harness_registrations where machine_id='16270000-0000-0000-0000-000000000003';
|
|
79
|
+
delete from agent_board.board_invite_redemptions where invite_id in (
|
|
80
|
+
select id from agent_board.board_invite_tokens where created_by_agent_id='ffffffff-ffff-ffff-ffff-ffffffffff01'
|
|
81
|
+
);
|
|
82
|
+
truncate agent_board.fleet_onboarding_invite_bounds;
|
|
83
|
+
delete from agent_board.board_invite_tokens where created_by_agent_id='ffffffff-ffff-ffff-ffff-ffffffffff01';
|
|
84
|
+
delete from agent_board.board_memberships where board_id in (
|
|
85
|
+
select id from agent_board.boards where slug='atomic-subtree-board'
|
|
86
|
+
);
|
|
87
|
+
delete from agent_board.enrollment_federation_audit where issuer='https://atomic-subtree.test';
|
|
88
|
+
delete from agent_board.agent_signing_keys where agent_id in (
|
|
89
|
+
select id from agent_board.agents where handle='atomic-subtree-agent'
|
|
90
|
+
);
|
|
91
|
+
delete from agent_board.agents where handle='atomic-subtree-agent';
|
|
92
|
+
delete from agent_board.boards where slug='atomic-subtree-board';
|
|
93
|
+
delete from agent_board.fleet_machines where id='16270000-0000-0000-0000-000000000003';
|
|
94
|
+
delete from agent_board.fleet_trust_domains where id='16270000-0000-0000-0000-000000000002';
|
|
95
|
+
delete from agent_board.agents where id='ffffffff-ffff-ffff-ffff-ffffffffff01';
|
|
96
|
+
delete from agent_board.identity_providers where issuer='https://atomic-subtree.test';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
session "blocker"
|
|
100
|
+
step "blocker_begin" { BEGIN; }
|
|
101
|
+
step "blocker_lock" {
|
|
102
|
+
select pg_advisory_xact_lock(hashtextextended('sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA', 0));
|
|
103
|
+
}
|
|
104
|
+
step "blocker_commit" { COMMIT; }
|
|
105
|
+
|
|
106
|
+
session "retry"
|
|
107
|
+
step "retry_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
108
|
+
step "retry_onboard" {
|
|
109
|
+
select identity_disposition, binding_disposition
|
|
110
|
+
from agent_board.onboard_federated_fleet_agent(
|
|
111
|
+
'https://atomic-subtree.test', 'message-board', 'human-subtree',
|
|
112
|
+
jsonb_build_object('iss','https://atomic-subtree.test','aud','message-board','sub','human-subtree','iat',200),
|
|
113
|
+
'atomic-subtree-agent',
|
|
114
|
+
'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk',
|
|
115
|
+
'raw_ed25519_base64url',
|
|
116
|
+
'sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA',
|
|
117
|
+
true, null, 'agent', ARRAY['board:send'], ARRAY[]::text[],
|
|
118
|
+
(select code from agent_board.board_invite_tokens where created_by_agent_id='ffffffff-ffff-ffff-ffff-ffffffffff01'),
|
|
119
|
+
(select id from agent_board.boards where slug='atomic-subtree-board'),
|
|
120
|
+
'16270000-0000-0000-0000-000000000003', 'codex', 'phase-loop',
|
|
121
|
+
'message-board', 'sha256:atomic-subtree-repo', 'worker', 'phase_worker',
|
|
122
|
+
'atomic-subtree-agent'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
step "retry_commit" { COMMIT; }
|
|
126
|
+
|
|
127
|
+
session "subtree"
|
|
128
|
+
step "subtree_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
129
|
+
step "revoke_human_subtree" {
|
|
130
|
+
select revoked_agent_count
|
|
131
|
+
from agent_board.revoke_human_subtree(
|
|
132
|
+
(select owner_user_id from agent_board.agents where handle='atomic-subtree-agent'),
|
|
133
|
+
'ffffffff-ffff-ffff-ffff-ffffffffff01', 'isolation subtree revoke'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
step "subtree_commit" { COMMIT; }
|
|
137
|
+
|
|
138
|
+
session "check"
|
|
139
|
+
step "check" {
|
|
140
|
+
select
|
|
141
|
+
(select status from agent_board.agents where id='ffffffff-ffff-ffff-ffff-ffffffffff01') as creator_status,
|
|
142
|
+
(select status from agent_board.agents where handle='atomic-subtree-agent') as enrolled_status,
|
|
143
|
+
(select status from agent_board.agent_signing_keys where fingerprint='sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA') as key_status,
|
|
144
|
+
(select count(*) from agent_board.fleet_machine_agents where machine_id='16270000-0000-0000-0000-000000000003') as bindings;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
permutation "blocker_begin" "blocker_lock" "retry_begin" "retry_onboard" "subtree_begin" "revoke_human_subtree" "blocker_commit" "subtree_commit" "retry_commit" "check"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Consiliency/message-board#196 — set_board_authority_ceiling must HOLD the
|
|
2
|
+
# board-keyed ceiling advisory lock BEFORE it touches board_authority_ceilings.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS SPEC EXISTS. The ordering was pinned only by a positional pgTAP
|
|
5
|
+
# assertion over RAW function text ('the ceiling setter holds the same lock
|
|
6
|
+
# before its owner gate and upsert'). Moving the `perform
|
|
7
|
+
# agent_board._lock_board_authority_ceiling(p_board_id)` to the END of the
|
|
8
|
+
# function made that assertion fail -- and adding ONE `--` comment naming the
|
|
9
|
+
# marker made the whole file pass 52/52 with the ordering violated. Measured
|
|
10
|
+
# against all 20 committed specs, NOTHING else moved: this ordering had no
|
|
11
|
+
# runtime backstop at all.
|
|
12
|
+
#
|
|
13
|
+
# HOW THE ORDER IS OBSERVED (a lock trace, not a return value). The outcome of
|
|
14
|
+
# the call is the same in both orders, so the observable has to be what the
|
|
15
|
+
# transaction is HOLDING while it is blocked:
|
|
16
|
+
# * a guard session holds the ceiling advisory lock, so the setter blocks --
|
|
17
|
+
# isolationtester prints `<waiting ...>`;
|
|
18
|
+
# * a third session then probes the ceiling ROW with `for update nowait`.
|
|
19
|
+
# CORRECT ORDER: the setter is parked on the advisory lock and has not
|
|
20
|
+
# reached its upsert, so the row is free and the probe returns 'admin'.
|
|
21
|
+
# LOCK-LAST ORDER: the setter already ran `on conflict do update`, so it
|
|
22
|
+
# holds the row and the probe fails immediately with
|
|
23
|
+
# `ERROR: could not obtain lock on row in relation "board_authority_ceilings"`.
|
|
24
|
+
# NOWAIT is what makes this terminate in BOTH states -- the probe never joins the
|
|
25
|
+
# wait graph, so a violated ordering produces a diff, never a hang.
|
|
26
|
+
#
|
|
27
|
+
# The ceiling row is SEEDED in setup on purpose: with no pre-existing row the
|
|
28
|
+
# upsert takes the INSERT path and there is no row for the probe to witness.
|
|
29
|
+
#
|
|
30
|
+
# MUTATION-VERIFIED: `perform agent_board._lock_board_authority_ceiling(p_board_id)`
|
|
31
|
+
# relocated after the upsert -> this golden changes (probe errors instead of
|
|
32
|
+
# returning 'admin'). Clean schema -> byte-identical across 10 runs.
|
|
33
|
+
|
|
34
|
+
setup
|
|
35
|
+
{
|
|
36
|
+
insert into auth.users (id) values ('19600000-0000-0000-0000-0000000000a1');
|
|
37
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
|
|
38
|
+
values ('19600000-0000-0000-0000-000000000001', 'agent', 'agent',
|
|
39
|
+
'ceiling-order-owner', 'Ceiling Order Owner', 'active',
|
|
40
|
+
'19600000-0000-0000-0000-0000000000a1');
|
|
41
|
+
select count(*) from agent_board.create_board(
|
|
42
|
+
'19600000-0000-0000-0000-000000000001',
|
|
43
|
+
'ceiling-order-board', 'Ceiling Order Board',
|
|
44
|
+
'Consiliency/message-board#196 ceiling setter lock-order probe',
|
|
45
|
+
jsonb_build_object(), 'private'
|
|
46
|
+
);
|
|
47
|
+
insert into agent_board.board_authority_ceilings(board_id, max_grantable_role)
|
|
48
|
+
values ((select id from agent_board.boards where slug='ceiling-order-board'), 'admin');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
teardown
|
|
52
|
+
{
|
|
53
|
+
delete from agent_board.board_authority_ceilings where board_id in (
|
|
54
|
+
select id from agent_board.boards where slug='ceiling-order-board'
|
|
55
|
+
);
|
|
56
|
+
delete from agent_board.board_memberships where board_id in (
|
|
57
|
+
select id from agent_board.boards where slug='ceiling-order-board'
|
|
58
|
+
);
|
|
59
|
+
delete from agent_board.boards where slug='ceiling-order-board';
|
|
60
|
+
delete from agent_board.agents where id='19600000-0000-0000-0000-000000000001';
|
|
61
|
+
delete from auth.users where id='19600000-0000-0000-0000-0000000000a1';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Holds the SAME board-keyed advisory lock the setter must take first. It calls
|
|
65
|
+
# the helper rather than re-deriving the key, so a key-derivation change cannot
|
|
66
|
+
# silently make this guard lock a different object.
|
|
67
|
+
session "holder"
|
|
68
|
+
step "holder_begin" { BEGIN; }
|
|
69
|
+
step "hold_ceiling_lock" {
|
|
70
|
+
select agent_board._lock_board_authority_ceiling(
|
|
71
|
+
(select id from agent_board.boards where slug='ceiling-order-board')) as locked;
|
|
72
|
+
}
|
|
73
|
+
step "holder_commit" { COMMIT; }
|
|
74
|
+
|
|
75
|
+
# The ceiling setter is owner-gated on current_agent_id(), so it runs as the owner
|
|
76
|
+
# under `authenticated` (service_role's current_agent_id() is null and the gate
|
|
77
|
+
# rejects it).
|
|
78
|
+
session "setter"
|
|
79
|
+
step "setter_begin" {
|
|
80
|
+
BEGIN;
|
|
81
|
+
SET LOCAL ROLE authenticated;
|
|
82
|
+
SET LOCAL "request.jwt.claim.role" = 'authenticated';
|
|
83
|
+
SET LOCAL "request.jwt.claim.sub" = '19600000-0000-0000-0000-0000000000a1';
|
|
84
|
+
}
|
|
85
|
+
step "set_ceiling" {
|
|
86
|
+
select ceiling_max_grantable_role from agent_board.set_board_authority_ceiling(
|
|
87
|
+
(select id from agent_board.boards where slug='ceiling-order-board'),
|
|
88
|
+
'member'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
step "setter_commit" { COMMIT; }
|
|
92
|
+
|
|
93
|
+
# NOWAIT probe: witnesses whether the blocked setter is already holding the
|
|
94
|
+
# ceiling row. Never blocks, so the permutation terminates in both orders.
|
|
95
|
+
session "probe"
|
|
96
|
+
step "probe_begin" { BEGIN; }
|
|
97
|
+
step "probe_ceiling_row" {
|
|
98
|
+
select max_grantable_role from agent_board.board_authority_ceilings
|
|
99
|
+
where board_id = (select id from agent_board.boards where slug='ceiling-order-board')
|
|
100
|
+
for update nowait;
|
|
101
|
+
}
|
|
102
|
+
step "probe_commit" { COMMIT; }
|
|
103
|
+
|
|
104
|
+
session "check"
|
|
105
|
+
step "check" {
|
|
106
|
+
select max_grantable_role from agent_board.board_authority_ceilings
|
|
107
|
+
where board_id = (select id from agent_board.boards where slug='ceiling-order-board');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# THE ORDERING PROBE. The setter is parked on the advisory lock; the ceiling row
|
|
111
|
+
# must still be free.
|
|
112
|
+
permutation "holder_begin" "hold_ceiling_lock" "setter_begin" "set_ceiling" "probe_begin" "probe_ceiling_row" "probe_commit" "holder_commit" "setter_commit" "check"
|
|
113
|
+
|
|
114
|
+
# Uncontended control: with nobody holding the advisory lock the setter completes
|
|
115
|
+
# in one step, so a `<waiting ...>` above is attributable to the lock and not to
|
|
116
|
+
# some unrelated serialization.
|
|
117
|
+
permutation "setter_begin" "set_ceiling" "setter_commit" "probe_begin" "probe_ceiling_row" "probe_commit" "check"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Consiliency/message-board#194 — enroll_federated_identity is the THIRD federated
|
|
2
|
+
# write route and the one that inverted board/owner order. It was a session in NO
|
|
3
|
+
# lock-order spec, which is why the cycle shipped: atomic_fedsession_fleet_lock_order
|
|
4
|
+
# pairs enroll_federated_human against onboard_federated_fleet_agent only.
|
|
5
|
+
#
|
|
6
|
+
# WHAT IS ASSERTED, AND WHY IT IS NOT THE DEADLOCK ITSELF.
|
|
7
|
+
# The invariant is an ORDER: this route must hold the invite's board BEFORE it takes
|
|
8
|
+
# the auth.users owner lock. A guard session holds the owner row, so the enrollment
|
|
9
|
+
# blocks there in BOTH the pre-fix and post-fix schema -- and a third session then
|
|
10
|
+
# asks, with NOWAIT, whether the board is already held:
|
|
11
|
+
#
|
|
12
|
+
# pre-fix the board is FREE while the enrollment waits on the owner
|
|
13
|
+
# -> `bprobe_board_nowait` SUCCEEDS and returns the row
|
|
14
|
+
# post-fix the enrollment already holds the board FOR UPDATE
|
|
15
|
+
# -> it fails with 55P03 could not obtain lock on relation "boards"
|
|
16
|
+
#
|
|
17
|
+
# The `check` row is IDENTICAL in both runs (the enrollment completes and redeems
|
|
18
|
+
# once the guard commits). Do not read that as "no difference": the entire golden
|
|
19
|
+
# diff is the bprobe step, and that step IS the ordering claim.
|
|
20
|
+
#
|
|
21
|
+
# WHY NOT DRIVE THE ABBA DIRECTLY. A permutation pairing this route against
|
|
22
|
+
# enroll_federated_human does reproduce the real cycle -- measured on the pre-fix
|
|
23
|
+
# schema: pg_stat_database.deadlocks = 1, and with ONLY the class-40 re-raise applied
|
|
24
|
+
# the victim surfaces `ERROR: deadlock detected`. But as shipped, the victim SWALLOWED
|
|
25
|
+
# 40P01, returned success with invite_redeemed=f and no membership, and held the owner
|
|
26
|
+
# row to commit -- so the counterpart session waited on it indefinitely and the
|
|
27
|
+
# permutation had to be interrupted. An unbounded hang is not a failing test; it is
|
|
28
|
+
# indistinguishable from an environment fault, and this runner has no per-spec
|
|
29
|
+
# timeout. The shape below terminates in both schemas and discriminates in both.
|
|
30
|
+
|
|
31
|
+
setup
|
|
32
|
+
{
|
|
33
|
+
insert into agent_board.agents(id, agent_type, kind, handle, display_name, status)
|
|
34
|
+
values ('194b0000-0000-0000-0000-000000000001', 'agent', 'agent',
|
|
35
|
+
'lo194-iso-owner', 'LO194 Iso Owner', 'active');
|
|
36
|
+
|
|
37
|
+
select count(*) from agent_board.create_board(
|
|
38
|
+
'194b0000-0000-0000-0000-000000000001',
|
|
39
|
+
'lo194-iso-board', 'LO194 Iso Board',
|
|
40
|
+
'Consiliency/message-board#194 enroll lock-order proof',
|
|
41
|
+
jsonb_build_object(), 'private'
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
insert into agent_board.identity_providers(
|
|
45
|
+
issuer, audience, supported, policy_approved,
|
|
46
|
+
allowed_scopes, allowed_board_scopes, allowed_agent_kinds
|
|
47
|
+
) values (
|
|
48
|
+
'https://lo194-iso.test', 'message-board', true, true,
|
|
49
|
+
ARRAY[]::text[], ARRAY[]::text[], ARRAY['agent','human']
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
insert into agent_board.board_invite_tokens(
|
|
53
|
+
code, board_id, preset_role, preset_membership_state, max_uses,
|
|
54
|
+
expires_at, created_by_agent_id)
|
|
55
|
+
select 'lo194-iso-code', b.id, 'member', 'joined', 10,
|
|
56
|
+
now() + interval '1 hour', '194b0000-0000-0000-0000-000000000001'
|
|
57
|
+
from agent_board.boards b
|
|
58
|
+
where b.slug = 'lo194-iso-board';
|
|
59
|
+
|
|
60
|
+
-- Materialize the owner auth.users row the enrollment will derive, so the guard
|
|
61
|
+
-- holds the SAME row (a race to INSERT it would block on the unique index, not on
|
|
62
|
+
-- the lock order under test).
|
|
63
|
+
create table lo194_owner as
|
|
64
|
+
select agent_board._ensure_agent_auth_user(
|
|
65
|
+
'fed-human-' || substr(
|
|
66
|
+
encode(extensions.digest(
|
|
67
|
+
convert_to('https://lo194-iso.test' || '|' || 'iso-human', 'UTF8'), 'sha256'), 'hex'),
|
|
68
|
+
1, 32)) as uid;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Holds the human owner row, so the enrollment parks on it in either schema.
|
|
72
|
+
session "ohold"
|
|
73
|
+
step "ohold_begin" { BEGIN; }
|
|
74
|
+
step "ohold_lock" {
|
|
75
|
+
select 1 as locked from auth.users u
|
|
76
|
+
where u.id = (select uid from lo194_owner)
|
|
77
|
+
for update;
|
|
78
|
+
}
|
|
79
|
+
step "ohold_commit" { COMMIT; }
|
|
80
|
+
|
|
81
|
+
session "enroll"
|
|
82
|
+
step "enroll_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
83
|
+
step "enroll_enroll" {
|
|
84
|
+
select agent_handle, invite_redeemed
|
|
85
|
+
from agent_board.enroll_federated_identity(
|
|
86
|
+
'https://lo194-iso.test', 'message-board', 'iso-human',
|
|
87
|
+
jsonb_build_object('iss','https://lo194-iso.test','aud','message-board','sub','iso-human'),
|
|
88
|
+
'lo194-iso-agent',
|
|
89
|
+
public.test_pubkey('lo194-iso-k1'), 'raw_ed25519_base64url',
|
|
90
|
+
public.test_key_fingerprint('lo194-iso-k1'),
|
|
91
|
+
true, null, 'agent', ARRAY[]::text[], ARRAY[]::text[],
|
|
92
|
+
'lo194-iso-code'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
step "enroll_commit" { COMMIT; }
|
|
96
|
+
|
|
97
|
+
# THE ORDERING ASSERTION. One unwrapped statement, so whatever lock it takes is
|
|
98
|
+
# released the instant it returns and no other session can ever wait on it.
|
|
99
|
+
session "bprobe"
|
|
100
|
+
step "bprobe_board_nowait" {
|
|
101
|
+
select slug from agent_board.boards where slug='lo194-iso-board' for update nowait;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
session "check"
|
|
105
|
+
step "check" {
|
|
106
|
+
select
|
|
107
|
+
(select count(*) from agent_board.board_memberships m
|
|
108
|
+
join agent_board.agents a on a.id = m.agent_id
|
|
109
|
+
where m.board_id = (select id from agent_board.boards where slug='lo194-iso-board')
|
|
110
|
+
and a.handle = 'lo194-iso-agent'
|
|
111
|
+
and m.membership_state = 'joined') as enrolled_agent_memberships,
|
|
112
|
+
(select count(*) from agent_board.board_invite_redemptions r
|
|
113
|
+
join agent_board.board_invite_tokens t on t.id = r.invite_id
|
|
114
|
+
where t.code = 'lo194-iso-code') as redemptions;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
permutation "ohold_begin" "ohold_lock" "enroll_begin" "enroll_enroll" "bprobe_board_nowait" "ohold_commit" "enroll_commit" "check"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Consiliency/message-board#193 — enroll_federated_human, with the provider ABSENT,
|
|
2
|
+
# must fail BEFORE it takes the auth.users owner lock.
|
|
3
|
+
#
|
|
4
|
+
# WHY A LOCK TRACE AND NOT A RETURN VALUE. The defect is an ORDER, and the outcome
|
|
5
|
+
# of the absent-provider call is 42501 either way (pre-fix from the delegate's
|
|
6
|
+
# step (4), post-fix from the pre-lock gate). What differs is what the transaction
|
|
7
|
+
# is HOLDING when it gets there. A guard session holds the owner's auth.users row,
|
|
8
|
+
# so:
|
|
9
|
+
# * PRE-FIX the call blocks -- isolationtester prints `<waiting ...>` -- which is
|
|
10
|
+
# the observable form of "it took the owner lock while the provider ordering was
|
|
11
|
+
# skipped entirely". That is the state from which the delegate, on its fresh READ
|
|
12
|
+
# COMMITTED snapshot, could see a concurrently-committed provider and reach its
|
|
13
|
+
# default board while still holding the owner: owner -> board, inverted against
|
|
14
|
+
# fleet onboarding's board -> owner.
|
|
15
|
+
# * POST-FIX it raises immediately, with NO `<waiting ...>` line at all.
|
|
16
|
+
# Re-introducing the skip therefore changes the golden. Mutation-verified both ways.
|
|
17
|
+
#
|
|
18
|
+
# The owner row is materialized directly through _ensure_agent_auth_user for an
|
|
19
|
+
# (issuer, subject) whose provider NEVER exists, which is the shape the existing
|
|
20
|
+
# atomic_fedsession_fleet_lock_order fixture cannot express: it creates the provider
|
|
21
|
+
# and the human before concurrency begins.
|
|
22
|
+
|
|
23
|
+
setup
|
|
24
|
+
{
|
|
25
|
+
create table lo193_owner as
|
|
26
|
+
select agent_board._ensure_agent_auth_user(
|
|
27
|
+
'fed-human-' || substr(
|
|
28
|
+
encode(extensions.digest(
|
|
29
|
+
convert_to('https://lo193-iso.test' || '|' || 'iso-orphan', 'UTF8'), 'sha256'), 'hex'),
|
|
30
|
+
1, 32)) as uid;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Holds the human owner row that the absent-provider path must never reach.
|
|
34
|
+
session "ohold"
|
|
35
|
+
step "ohold_begin" { BEGIN; }
|
|
36
|
+
step "ohold_lock" {
|
|
37
|
+
select 1 as locked from auth.users u
|
|
38
|
+
where u.id = (select uid from lo193_owner)
|
|
39
|
+
for update;
|
|
40
|
+
}
|
|
41
|
+
step "ohold_commit" { COMMIT; }
|
|
42
|
+
|
|
43
|
+
session "portal"
|
|
44
|
+
step "portal_begin" { BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
|
|
45
|
+
step "portal_enroll" {
|
|
46
|
+
select status from agent_board.enroll_federated_human(
|
|
47
|
+
'https://lo193-iso.test', 'message-board', 'iso-orphan',
|
|
48
|
+
jsonb_build_object('sub','iso-orphan'), 'Iso Orphan'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
step "portal_commit" { COMMIT; }
|
|
52
|
+
|
|
53
|
+
session "check"
|
|
54
|
+
step "check" {
|
|
55
|
+
select
|
|
56
|
+
(select count(*) from agent_board.agents a
|
|
57
|
+
where a.kind = 'human'
|
|
58
|
+
and a.metadata #>> ARRAY['federation','subject'] = 'iso-orphan') as human_agents,
|
|
59
|
+
(select count(*) from agent_board.identity_providers ip
|
|
60
|
+
where ip.issuer = 'https://lo193-iso.test') as providers;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
permutation "ohold_begin" "ohold_lock" "portal_begin" "portal_enroll" "ohold_commit" "portal_commit" "check"
|