@consiliency/agent-board-schema 1.60.0 → 1.63.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 (68) hide show
  1. package/README.md +80 -0
  2. package/fixtures/already-applied.json +12 -0
  3. package/migrations/20261011000100_agent_board_session_identities.sql +1964 -0
  4. package/migrations/20261012000100_agent_board_sidowner.sql +209 -0
  5. package/migrations/20261014000100_agent_board_grant_target_role.sql +218 -0
  6. package/package.json +1 -1
  7. package/schema-manifest.json +30 -3
  8. package/src/tooling.test.mjs +6 -2
  9. package/tests/agent_board_atomic_fleet_onboarding.test.sql +13 -4
  10. package/tests/agent_board_consent_emission.test.sql +30 -11
  11. package/tests/agent_board_consent_gating.test.sql +16 -2
  12. package/tests/agent_board_delegate_role_class.test.sql +1 -1
  13. package/tests/agent_board_delegation_cap_ceiling.test.sql +26 -15
  14. package/tests/agent_board_delegation_enforcement.test.sql +1 -1
  15. package/tests/agent_board_enroll_resumption.test.sql +22 -4
  16. package/tests/agent_board_enroll_token_single_use.test.sql +18 -3
  17. package/tests/agent_board_federated_lock_order_class.test.sql +2 -2
  18. package/tests/agent_board_fk_lock_edges.test.sql +74 -14
  19. package/tests/agent_board_grant_target_role.test.sql +1118 -0
  20. package/tests/agent_board_invite_no_downgrade.test.sql +10 -8
  21. package/tests/agent_board_ledger_authority.test.sql +70 -12
  22. package/tests/agent_board_membership_current_row_class.test.sql +9 -1
  23. package/tests/agent_board_mint_fleet_agent_session.test.sql +2 -2
  24. package/tests/agent_board_mint_harden.test.sql +38 -27
  25. package/tests/agent_board_rolereset.test.sql +8 -2
  26. package/tests/agent_board_roomdeleg_lane_b.test.sql +6 -6
  27. package/tests/agent_board_rowid.test.sql +3 -1
  28. package/tests/agent_board_session_identity_approval_revocation.test.sql +334 -0
  29. package/tests/agent_board_session_identity_backfill.test.sql +82 -0
  30. package/tests/agent_board_session_identity_backfill_refusal.test.sql +14 -0
  31. package/tests/agent_board_session_identity_behavior_classes.test.sql +176 -0
  32. package/tests/agent_board_session_identity_class.test.sql +118 -0
  33. package/tests/agent_board_session_identity_fingerprint.test.sql +271 -0
  34. package/tests/agent_board_session_identity_labels_limits.test.sql +205 -0
  35. package/tests/agent_board_session_identity_lease.test.sql +235 -0
  36. package/tests/agent_board_session_identity_lifetime.test.sql +230 -0
  37. package/tests/agent_board_session_identity_limits.test.sql +208 -0
  38. package/tests/agent_board_session_identity_lineage.test.sql +64 -0
  39. package/tests/agent_board_session_identity_link.test.sql +201 -0
  40. package/tests/agent_board_session_identity_mint.test.sql +233 -0
  41. package/tests/agent_board_session_identity_panel_r1.test.sql +194 -0
  42. package/tests/agent_board_session_identity_panel_r2.test.sql +174 -0
  43. package/tests/agent_board_session_identity_protected_actions.test.sql +156 -0
  44. package/tests/agent_board_session_identity_revoke_closure.test.sql +131 -0
  45. package/tests/agent_board_sidowner_leases.test.sql +107 -0
  46. package/tests/agent_board_sidowner_owner_required.test.sql +97 -0
  47. package/tests/agent_board_sidowner_read_class.test.sql +171 -0
  48. package/tests/agent_board_sidowner_reads.test.sql +136 -0
  49. package/tests/agent_board_signer_lifecycle.test.sql +26 -4
  50. package/tests/agent_board_signing_challenge_pending_cap.test.sql +1 -1
  51. package/tests/agent_board_write_enforcement_class.test.sql +74 -15
  52. package/tests/catalog/function_execute_privileges.json +218 -2
  53. package/tests/catalog/p_metadata_parameters.json +98 -2
  54. package/tests/isolation/expected/grant_target_role_race.out +348 -0
  55. package/tests/isolation/expected/session_identity_limits.out +165 -0
  56. package/tests/isolation/expected/session_identity_links.out +235 -0
  57. package/tests/isolation/expected/session_identity_reclaim.out +253 -0
  58. package/tests/isolation/expected/session_identity_renew_revoke.out +115 -0
  59. package/tests/isolation/expected/session_identity_renew_revoke_key_gate.out +138 -0
  60. package/tests/isolation/expected/session_identity_tombstone_onboarding.out +247 -0
  61. package/tests/isolation/specs/grant_target_role_race.spec +207 -0
  62. package/tests/isolation/specs/session_identity_limits.spec +81 -0
  63. package/tests/isolation/specs/session_identity_links.spec +80 -0
  64. package/tests/isolation/specs/session_identity_reclaim.spec +125 -0
  65. package/tests/isolation/specs/session_identity_renew_revoke.spec +84 -0
  66. package/tests/isolation/specs/session_identity_renew_revoke_key_gate.spec +94 -0
  67. package/tests/isolation/specs/session_identity_tombstone_onboarding.spec +108 -0
  68. package/tests/seeds/20261011000100_agent_board_session_identities.sql.pre.sql +47 -0
@@ -0,0 +1,138 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: key_begin key_hold revoke_begin revoke renew_begin renew key_commit revoke_commit renew_commit check
4
+ step key_begin: BEGIN;
5
+ step key_hold:
6
+ select 1 as held from agent_board.agent_signing_keys
7
+ where agent_id = (select id from agent_board.agents where handle = 'sid-renew-child') for update;
8
+
9
+ held
10
+ ----
11
+ 1
12
+ (1 row)
13
+
14
+ step revoke_begin:
15
+ BEGIN;
16
+ SET LOCAL ROLE authenticated;
17
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
18
+ SET LOCAL "request.jwt.claim.sub" = '81000000-0000-4000-8000-000000000001';
19
+
20
+ step revoke:
21
+ select agent_board.revoke_session_identity((select id from agent_board.agents where handle = 'sid-renew-child'));
22
+ <waiting ...>
23
+ step renew_begin:
24
+ BEGIN;
25
+ select set_config('request.jwt.claim.sub', (select auth_user_id::text from agent_board.agents where handle = 'sid-renew-child'), true) is not null as holder_forwarded;
26
+ SET LOCAL ROLE service_role;
27
+ SET LOCAL "request.jwt.claim.role" = 'service_role';
28
+ SET LOCAL "request.jwt.claim.identity_lease_epoch" = '0';
29
+
30
+ holder_forwarded
31
+ ----------------
32
+ t
33
+ (1 row)
34
+
35
+ step renew:
36
+ select agent_board.renew_session_identity((select id from agent_board.agents where handle = 'sid-renew-child'), 'approved');
37
+ <waiting ...>
38
+ step key_commit: COMMIT;
39
+ step revoke: <... completed>
40
+ revoke_session_identity
41
+ -----------------------
42
+
43
+ (1 row)
44
+
45
+ step revoke_commit: COMMIT;
46
+ step renew: <... completed>
47
+ ERROR: identity_tombstoned
48
+ step renew_commit: COMMIT;
49
+ step check:
50
+ select si.tombstone_reason, si.credential_epoch, a.status, g.revoked_at is not null as grant_revoked,
51
+ g.last_renewed_at is not null as renewal_preceded_revoke,
52
+ (select count(*) from agent_board.fleet_revocation_events e where e.subject_id = a.id) as permanent_revocations,
53
+ (select count(*) from agent_board.agent_signing_keys k where k.agent_id = a.id and k.status <> 'revoked') as active_keys
54
+ from agent_board.agents a join agent_board.session_identities si on si.agent_id = a.id
55
+ join agent_board.delegation_grants g on g.child_agent_id = a.id where a.handle = 'sid-renew-child';
56
+ select bool_and(expires_at <= issued_at + interval '900 seconds') as bounded_pre_revoke_tokens
57
+ from agent_board.nonfleet_sessions where agent_id = (select id from agent_board.agents where handle = 'sid-renew-child');
58
+
59
+ tombstone_reason|credential_epoch|status |grant_revoked|renewal_preceded_revoke|permanent_revocations|active_keys
60
+ ----------------+----------------+-------+-------------+-----------------------+---------------------+-----------
61
+ revoked | 1|revoked|t |f | 1| 0
62
+ (1 row)
63
+
64
+ bounded_pre_revoke_tokens
65
+ -------------------------
66
+ t
67
+ (1 row)
68
+
69
+
70
+ starting permutation: key_begin key_hold renew_begin renew revoke_begin revoke key_commit renew_commit revoke_commit check
71
+ step key_begin: BEGIN;
72
+ step key_hold:
73
+ select 1 as held from agent_board.agent_signing_keys
74
+ where agent_id = (select id from agent_board.agents where handle = 'sid-renew-child') for update;
75
+
76
+ held
77
+ ----
78
+ 1
79
+ (1 row)
80
+
81
+ step renew_begin:
82
+ BEGIN;
83
+ select set_config('request.jwt.claim.sub', (select auth_user_id::text from agent_board.agents where handle = 'sid-renew-child'), true) is not null as holder_forwarded;
84
+ SET LOCAL ROLE service_role;
85
+ SET LOCAL "request.jwt.claim.role" = 'service_role';
86
+ SET LOCAL "request.jwt.claim.identity_lease_epoch" = '0';
87
+
88
+ holder_forwarded
89
+ ----------------
90
+ t
91
+ (1 row)
92
+
93
+ step renew:
94
+ select agent_board.renew_session_identity((select id from agent_board.agents where handle = 'sid-renew-child'), 'approved');
95
+
96
+ renew_session_identity
97
+ ----------------------
98
+
99
+ (1 row)
100
+
101
+ step revoke_begin:
102
+ BEGIN;
103
+ SET LOCAL ROLE authenticated;
104
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
105
+ SET LOCAL "request.jwt.claim.sub" = '81000000-0000-4000-8000-000000000001';
106
+
107
+ step revoke:
108
+ select agent_board.revoke_session_identity((select id from agent_board.agents where handle = 'sid-renew-child'));
109
+ <waiting ...>
110
+ step key_commit: COMMIT;
111
+ step renew_commit: COMMIT;
112
+ step revoke: <... completed>
113
+ revoke_session_identity
114
+ -----------------------
115
+
116
+ (1 row)
117
+
118
+ step revoke_commit: COMMIT;
119
+ step check:
120
+ select si.tombstone_reason, si.credential_epoch, a.status, g.revoked_at is not null as grant_revoked,
121
+ g.last_renewed_at is not null as renewal_preceded_revoke,
122
+ (select count(*) from agent_board.fleet_revocation_events e where e.subject_id = a.id) as permanent_revocations,
123
+ (select count(*) from agent_board.agent_signing_keys k where k.agent_id = a.id and k.status <> 'revoked') as active_keys
124
+ from agent_board.agents a join agent_board.session_identities si on si.agent_id = a.id
125
+ join agent_board.delegation_grants g on g.child_agent_id = a.id where a.handle = 'sid-renew-child';
126
+ select bool_and(expires_at <= issued_at + interval '900 seconds') as bounded_pre_revoke_tokens
127
+ from agent_board.nonfleet_sessions where agent_id = (select id from agent_board.agents where handle = 'sid-renew-child');
128
+
129
+ tombstone_reason|credential_epoch|status |grant_revoked|renewal_preceded_revoke|permanent_revocations|active_keys
130
+ ----------------+----------------+-------+-------------+-----------------------+---------------------+-----------
131
+ revoked | 1|revoked|t |t | 1| 0
132
+ (1 row)
133
+
134
+ bounded_pre_revoke_tokens
135
+ -------------------------
136
+ t
137
+ (1 row)
138
+
@@ -0,0 +1,247 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: o_begin o_advisory o_agents t_begin t_tomb o_membership o_commit t_commit check
4
+ step o_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
5
+ step o_advisory:
6
+ select agent_board._lock_board_authority_ceiling('85000000-0000-4000-8000-000000000002') is null as advisory_taken;
7
+
8
+ advisory_taken
9
+ --------------
10
+ f
11
+ (1 row)
12
+
13
+ step o_agents:
14
+ select count(*)::int as locked_agents from (
15
+ select 1 from agent_board.agents where id='85000000-0000-4000-8000-000000000001' for update) s;
16
+
17
+ locked_agents
18
+ -------------
19
+ 1
20
+ (1 row)
21
+
22
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
23
+ step t_tomb:
24
+ select first from agent_board.tombstone_delegate_instance(
25
+ 'sid_ti_instance_00001','urn:sid-ti:issuer','sid-ti-owner','owner_revoked');
26
+ <waiting ...>
27
+ step o_membership:
28
+ select count(*)::int as locked_memberships from (
29
+ select 1 from agent_board.board_memberships
30
+ where board_id='85000000-0000-4000-8000-000000000002'
31
+ and agent_id='85000000-0000-4000-8000-000000000001' for update) s;
32
+
33
+ locked_memberships
34
+ ------------------
35
+ 1
36
+ (1 row)
37
+
38
+ step o_commit: COMMIT;
39
+ step t_tomb: <... completed>
40
+ first
41
+ -----
42
+ t
43
+ (1 row)
44
+
45
+ step t_commit: COMMIT;
46
+ step check:
47
+ select tombstoned_at is not null as instance_tombstoned from agent_board.delegate_instances
48
+ where instance_id='sid_ti_instance_00001';
49
+ select status from agent_board.agents where id='85000000-0000-4000-8000-000000000001';
50
+ select membership_state from agent_board.board_memberships
51
+ where board_id='85000000-0000-4000-8000-000000000002'
52
+ and agent_id='85000000-0000-4000-8000-000000000001';
53
+ select si.tombstoned_at is not null as child_tombstoned,si.credential_epoch,
54
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id=si.agent_id and ac.revoked_at is null) as live_credentials
55
+ from agent_board.session_identities si join agent_board.agents a on a.id=si.agent_id where a.handle='sid-ti-child';
56
+ select count(*) as withdrawal_events from agent_board.board_membership_events
57
+ where board_id='85000000-0000-4000-8000-000000000002' and event_kind='operator_removal';
58
+
59
+ instance_tombstoned
60
+ -------------------
61
+ t
62
+ (1 row)
63
+
64
+ status
65
+ -------
66
+ revoked
67
+ (1 row)
68
+
69
+ membership_state
70
+ ----------------
71
+ left
72
+ (1 row)
73
+
74
+ child_tombstoned|credential_epoch|live_credentials
75
+ ----------------+----------------+----------------
76
+ t | 1| 0
77
+ (1 row)
78
+
79
+ withdrawal_events
80
+ -----------------
81
+ 1
82
+ (1 row)
83
+
84
+
85
+ starting permutation: t_begin t_tomb o_begin o_advisory t_commit o_agents o_membership o_commit check
86
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
87
+ step t_tomb:
88
+ select first from agent_board.tombstone_delegate_instance(
89
+ 'sid_ti_instance_00001','urn:sid-ti:issuer','sid-ti-owner','owner_revoked');
90
+
91
+ first
92
+ -----
93
+ t
94
+ (1 row)
95
+
96
+ step o_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
97
+ step o_advisory:
98
+ select agent_board._lock_board_authority_ceiling('85000000-0000-4000-8000-000000000002') is null as advisory_taken;
99
+ <waiting ...>
100
+ step t_commit: COMMIT;
101
+ step o_advisory: <... completed>
102
+ advisory_taken
103
+ --------------
104
+ f
105
+ (1 row)
106
+
107
+ step o_agents:
108
+ select count(*)::int as locked_agents from (
109
+ select 1 from agent_board.agents where id='85000000-0000-4000-8000-000000000001' for update) s;
110
+
111
+ locked_agents
112
+ -------------
113
+ 1
114
+ (1 row)
115
+
116
+ step o_membership:
117
+ select count(*)::int as locked_memberships from (
118
+ select 1 from agent_board.board_memberships
119
+ where board_id='85000000-0000-4000-8000-000000000002'
120
+ and agent_id='85000000-0000-4000-8000-000000000001' for update) s;
121
+
122
+ locked_memberships
123
+ ------------------
124
+ 1
125
+ (1 row)
126
+
127
+ step o_commit: COMMIT;
128
+ step check:
129
+ select tombstoned_at is not null as instance_tombstoned from agent_board.delegate_instances
130
+ where instance_id='sid_ti_instance_00001';
131
+ select status from agent_board.agents where id='85000000-0000-4000-8000-000000000001';
132
+ select membership_state from agent_board.board_memberships
133
+ where board_id='85000000-0000-4000-8000-000000000002'
134
+ and agent_id='85000000-0000-4000-8000-000000000001';
135
+ select si.tombstoned_at is not null as child_tombstoned,si.credential_epoch,
136
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id=si.agent_id and ac.revoked_at is null) as live_credentials
137
+ from agent_board.session_identities si join agent_board.agents a on a.id=si.agent_id where a.handle='sid-ti-child';
138
+ select count(*) as withdrawal_events from agent_board.board_membership_events
139
+ where board_id='85000000-0000-4000-8000-000000000002' and event_kind='operator_removal';
140
+
141
+ instance_tombstoned
142
+ -------------------
143
+ t
144
+ (1 row)
145
+
146
+ status
147
+ -------
148
+ revoked
149
+ (1 row)
150
+
151
+ membership_state
152
+ ----------------
153
+ left
154
+ (1 row)
155
+
156
+ child_tombstoned|credential_epoch|live_credentials
157
+ ----------------+----------------+----------------
158
+ t | 1| 0
159
+ (1 row)
160
+
161
+ withdrawal_events
162
+ -----------------
163
+ 1
164
+ (1 row)
165
+
166
+
167
+ starting permutation: o_begin o_advisory t_begin t_tomb o_agents o_membership o_commit t_commit check
168
+ step o_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
169
+ step o_advisory:
170
+ select agent_board._lock_board_authority_ceiling('85000000-0000-4000-8000-000000000002') is null as advisory_taken;
171
+
172
+ advisory_taken
173
+ --------------
174
+ f
175
+ (1 row)
176
+
177
+ step t_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
178
+ step t_tomb:
179
+ select first from agent_board.tombstone_delegate_instance(
180
+ 'sid_ti_instance_00001','urn:sid-ti:issuer','sid-ti-owner','owner_revoked');
181
+ <waiting ...>
182
+ step o_agents:
183
+ select count(*)::int as locked_agents from (
184
+ select 1 from agent_board.agents where id='85000000-0000-4000-8000-000000000001' for update) s;
185
+
186
+ locked_agents
187
+ -------------
188
+ 1
189
+ (1 row)
190
+
191
+ step o_membership:
192
+ select count(*)::int as locked_memberships from (
193
+ select 1 from agent_board.board_memberships
194
+ where board_id='85000000-0000-4000-8000-000000000002'
195
+ and agent_id='85000000-0000-4000-8000-000000000001' for update) s;
196
+
197
+ locked_memberships
198
+ ------------------
199
+ 1
200
+ (1 row)
201
+
202
+ step o_commit: COMMIT;
203
+ step t_tomb: <... completed>
204
+ first
205
+ -----
206
+ t
207
+ (1 row)
208
+
209
+ step t_commit: COMMIT;
210
+ step check:
211
+ select tombstoned_at is not null as instance_tombstoned from agent_board.delegate_instances
212
+ where instance_id='sid_ti_instance_00001';
213
+ select status from agent_board.agents where id='85000000-0000-4000-8000-000000000001';
214
+ select membership_state from agent_board.board_memberships
215
+ where board_id='85000000-0000-4000-8000-000000000002'
216
+ and agent_id='85000000-0000-4000-8000-000000000001';
217
+ select si.tombstoned_at is not null as child_tombstoned,si.credential_epoch,
218
+ (select count(*) from agent_board.agent_credentials ac where ac.agent_id=si.agent_id and ac.revoked_at is null) as live_credentials
219
+ from agent_board.session_identities si join agent_board.agents a on a.id=si.agent_id where a.handle='sid-ti-child';
220
+ select count(*) as withdrawal_events from agent_board.board_membership_events
221
+ where board_id='85000000-0000-4000-8000-000000000002' and event_kind='operator_removal';
222
+
223
+ instance_tombstoned
224
+ -------------------
225
+ t
226
+ (1 row)
227
+
228
+ status
229
+ -------
230
+ revoked
231
+ (1 row)
232
+
233
+ membership_state
234
+ ----------------
235
+ left
236
+ (1 row)
237
+
238
+ child_tombstoned|credential_epoch|live_credentials
239
+ ----------------+----------------+----------------
240
+ t | 1| 0
241
+ (1 row)
242
+
243
+ withdrawal_events
244
+ -----------------
245
+ 1
246
+ (1 row)
247
+
@@ -0,0 +1,207 @@
1
+ # #674 (plans/plan-674-grant-target-role.md §5.4): grant_board_role reads the target's
2
+ # CURRENT role in ONE step-6 statement that locks the target's active row
3
+ # FOR NO KEY UPDATE OF t and LEFT JOINs the grantor's active row unlocked. Every session
4
+ # uses explicit BEGIN/COMMIT (or ROLLBACK); claims are transaction-local, so they are set
5
+ # in the begin step. Authenticated grants pass no p_grantor_agent_id (the host shape).
6
+ #
7
+ # Board B (created by O). Roles at setup:
8
+ # O owner, A1 admin, M member, V observer, G member.
9
+ # One admin-preset invite link on B, minted by O through create_invite_link (max_uses 5).
10
+ #
11
+ # I-1 M redeems the admin link (rank-max lifts M to admin, row held) while A1 grants
12
+ # M -> observer. A1 BLOCKS at step 6; after M commits, the locked read sees admin,
13
+ # and C-B refuses (D6b). M stays admin. (pins M7)
14
+ # I-2 O removes M (update_board_membership, held) while A1 grants M -> observer. A1
15
+ # blocks; after O commits, A1 gets P0002 (no all-null row).
16
+ # I-3 O demotes A1 to member (held) while A1 grants M -> observer. A1 blocks on the
17
+ # board advisory lock; after O commits, step 6 sees A1 as member and C-A refuses
18
+ # (D6a). Reverse order: both succeed.
19
+ # I-4 O posts on B (post_board_message fan-out inserts delivery_recipients naming M's
20
+ # membership: FOR KEY SHARE on M's row, held) while O's second session grants
21
+ # M -> observer. The grant completes WITHOUT blocking. (pins M7b)
22
+ # I-5 G redeems the admin link (G's own row raised to admin, held) while G's second
23
+ # session grants V -> member. It does NOT block (g is unlocked), reads G's committed
24
+ # role member, and C-A refuses (D6a). After G commits, a retry succeeds.
25
+
26
+ setup
27
+ {
28
+ insert into auth.users(id) values
29
+ ('67410000-0000-4000-8000-000000000001'),
30
+ ('67410000-0000-4000-8000-000000000002'),
31
+ ('67410000-0000-4000-8000-000000000003'),
32
+ ('67410000-0000-4000-8000-000000000004'),
33
+ ('67410000-0000-4000-8000-000000000005');
34
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id) values
35
+ ('67420000-0000-4000-8000-000000000001', 'agent', 'g674-iso-o', 'G674 O', 'active', '67410000-0000-4000-8000-000000000001'),
36
+ ('67420000-0000-4000-8000-000000000002', 'agent', 'g674-iso-a1', 'G674 A1', 'active', '67410000-0000-4000-8000-000000000002'),
37
+ ('67420000-0000-4000-8000-000000000003', 'agent', 'g674-iso-m', 'G674 M', 'active', '67410000-0000-4000-8000-000000000003'),
38
+ ('67420000-0000-4000-8000-000000000004', 'agent', 'g674-iso-v', 'G674 V', 'active', '67410000-0000-4000-8000-000000000004'),
39
+ ('67420000-0000-4000-8000-000000000005', 'agent', 'g674-iso-g', 'G674 G', 'active', '67410000-0000-4000-8000-000000000005');
40
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility)
41
+ values ('67430000-0000-4000-8000-000000000001', 'g674-iso-board', 'G674 isolation board',
42
+ '67420000-0000-4000-8000-000000000001', 'private');
43
+ insert into agent_board.board_memberships(board_id, agent_id, membership_state, role, created_by_agent_id) values
44
+ ('67430000-0000-4000-8000-000000000001', '67420000-0000-4000-8000-000000000001', 'joined', 'owner', '67420000-0000-4000-8000-000000000001'),
45
+ ('67430000-0000-4000-8000-000000000001', '67420000-0000-4000-8000-000000000002', 'joined', 'admin', '67420000-0000-4000-8000-000000000001'),
46
+ ('67430000-0000-4000-8000-000000000001', '67420000-0000-4000-8000-000000000003', 'joined', 'member', '67420000-0000-4000-8000-000000000001'),
47
+ ('67430000-0000-4000-8000-000000000001', '67420000-0000-4000-8000-000000000004', 'joined', 'observer', '67420000-0000-4000-8000-000000000001'),
48
+ ('67430000-0000-4000-8000-000000000001', '67420000-0000-4000-8000-000000000005', 'joined', 'member', '67420000-0000-4000-8000-000000000001');
49
+ insert into agent_board.threads(id, created_by_agent_id, subject, board_id)
50
+ values ('67440000-0000-4000-8000-000000000001', '67420000-0000-4000-8000-000000000001', 'g674 iso thread',
51
+ '67430000-0000-4000-8000-000000000001');
52
+ -- The admin-preset link, minted by O through the real RPC (service path, O supplied).
53
+ select count(*) from agent_board.create_invite_link('67430000-0000-4000-8000-000000000001', 'admin', 'joined', 5,
54
+ null, '67420000-0000-4000-8000-000000000001', jsonb_build_object());
55
+ }
56
+
57
+ teardown
58
+ {
59
+ delete from agent_board.board_invite_redemptions where invite_id in
60
+ (select id from agent_board.board_invite_tokens where board_id = '67430000-0000-4000-8000-000000000001');
61
+ delete from agent_board.board_invite_tokens where board_id = '67430000-0000-4000-8000-000000000001';
62
+ delete from agent_board.delivery_recipients where board_id = '67430000-0000-4000-8000-000000000001';
63
+ delete from agent_board.inbox_items where board_id = '67430000-0000-4000-8000-000000000001';
64
+ delete from agent_board.callback_outbox_events where board_id = '67430000-0000-4000-8000-000000000001';
65
+ delete from agent_board.messages where thread_id = '67440000-0000-4000-8000-000000000001';
66
+ delete from agent_board.threads where id = '67440000-0000-4000-8000-000000000001';
67
+ delete from agent_board.board_membership_events where board_id = '67430000-0000-4000-8000-000000000001';
68
+ delete from agent_board.board_memberships where board_id = '67430000-0000-4000-8000-000000000001';
69
+ delete from agent_board.boards where id = '67430000-0000-4000-8000-000000000001';
70
+ delete from agent_board.agents where id::text like '67420000-0000-4000-8000-00000000000_';
71
+ delete from auth.users where id::text like '67410000-0000-4000-8000-00000000000_';
72
+ }
73
+
74
+ # M, redeeming the admin link (I-1).
75
+ session "m"
76
+ step "m_begin" {
77
+ BEGIN;
78
+ select set_config('g674.code', (select code from agent_board.board_invite_tokens
79
+ where board_id = '67430000-0000-4000-8000-000000000001' and preset_role = 'admin'), true) is not null as have_code;
80
+ SET LOCAL ROLE authenticated;
81
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
82
+ SET LOCAL "request.jwt.claim.sub" = '67410000-0000-4000-8000-000000000003';
83
+ }
84
+ step "m_redeem" { select role, membership_state from agent_board.redeem_invite_link(current_setting('g674.code')); }
85
+ step "m_commit" { COMMIT; }
86
+
87
+ # A1, the admin grantor (I-1, I-2, I-3).
88
+ session "a"
89
+ step "a_begin" {
90
+ BEGIN;
91
+ SET LOCAL ROLE authenticated;
92
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
93
+ SET LOCAL "request.jwt.claim.sub" = '67410000-0000-4000-8000-000000000002';
94
+ }
95
+ step "a_grant_m_observer" {
96
+ select role from agent_board.grant_board_role('67430000-0000-4000-8000-000000000001',
97
+ '67420000-0000-4000-8000-000000000003', 'observer');
98
+ }
99
+ step "a_commit" { COMMIT; }
100
+ step "a_rollback" { ROLLBACK; }
101
+
102
+ # O, the owner (I-2 eviction, I-3 demotion of A1, I-4 grantor).
103
+ session "o"
104
+ step "o_begin" {
105
+ BEGIN;
106
+ SET LOCAL ROLE authenticated;
107
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
108
+ SET LOCAL "request.jwt.claim.sub" = '67410000-0000-4000-8000-000000000001';
109
+ }
110
+ step "o_remove_m" {
111
+ select membership_state from agent_board.update_board_membership('67430000-0000-4000-8000-000000000001',
112
+ '67420000-0000-4000-8000-000000000003', 'removed');
113
+ }
114
+ step "o_demote_a" {
115
+ select role from agent_board.grant_board_role('67430000-0000-4000-8000-000000000001',
116
+ '67420000-0000-4000-8000-000000000002', 'member');
117
+ }
118
+ step "o_grant_m_observer" {
119
+ select role from agent_board.grant_board_role('67430000-0000-4000-8000-000000000001',
120
+ '67420000-0000-4000-8000-000000000003', 'observer');
121
+ }
122
+ step "o_commit" { COMMIT; }
123
+
124
+ # O's second session, posting on B (I-4): the fan-out FK-inserts delivery_recipients rows
125
+ # naming each recipient's membership, which takes FOR KEY SHARE on M's row.
126
+ session "p"
127
+ step "p_begin" {
128
+ BEGIN;
129
+ SET LOCAL ROLE authenticated;
130
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
131
+ SET LOCAL "request.jwt.claim.sub" = '67410000-0000-4000-8000-000000000001';
132
+ }
133
+ step "p_post" {
134
+ select count(*) > 0 as posted from agent_board.post_board_message('67440000-0000-4000-8000-000000000001',
135
+ '67420000-0000-4000-8000-000000000001', '67430000-0000-4000-8000-000000000001', 'g674 iso', 'fan-out to M');
136
+ }
137
+ # RESET ROLE (still inside the open transaction, which keeps every lock it took) so this
138
+ # count reads M's uncommitted delivery row past the recipient-scoped RLS O is under.
139
+ step "p_holds_m_fk" {
140
+ RESET ROLE;
141
+ select count(*) as m_delivery_rows from agent_board.delivery_recipients dr
142
+ join agent_board.board_memberships bm on bm.id = dr.board_membership_id
143
+ where bm.board_id = '67430000-0000-4000-8000-000000000001' and bm.agent_id = '67420000-0000-4000-8000-000000000003';
144
+ }
145
+ step "p_commit" { COMMIT; }
146
+
147
+ # G, redeeming the admin link for its OWN row (I-5).
148
+ session "g"
149
+ step "g_begin" {
150
+ BEGIN;
151
+ select set_config('g674.code', (select code from agent_board.board_invite_tokens
152
+ where board_id = '67430000-0000-4000-8000-000000000001' and preset_role = 'admin'), true) is not null as have_code;
153
+ SET LOCAL ROLE authenticated;
154
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
155
+ SET LOCAL "request.jwt.claim.sub" = '67410000-0000-4000-8000-000000000005';
156
+ }
157
+ step "g_redeem" { select role, membership_state from agent_board.redeem_invite_link(current_setting('g674.code')); }
158
+ step "g_commit" { COMMIT; }
159
+
160
+ # G's second session, the grantor in I-5.
161
+ session "g2"
162
+ step "g2_begin" {
163
+ BEGIN;
164
+ SET LOCAL ROLE authenticated;
165
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
166
+ SET LOCAL "request.jwt.claim.sub" = '67410000-0000-4000-8000-000000000005';
167
+ }
168
+ step "g2_grant_v_member" {
169
+ select role from agent_board.grant_board_role('67430000-0000-4000-8000-000000000001',
170
+ '67420000-0000-4000-8000-000000000004', 'member');
171
+ }
172
+ step "g2_rollback" { ROLLBACK; }
173
+ step "g2_retry_begin" {
174
+ BEGIN;
175
+ SET LOCAL ROLE authenticated;
176
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
177
+ SET LOCAL "request.jwt.claim.sub" = '67410000-0000-4000-8000-000000000005';
178
+ }
179
+ step "g2_retry_grant_v_member" {
180
+ select role from agent_board.grant_board_role('67430000-0000-4000-8000-000000000001',
181
+ '67420000-0000-4000-8000-000000000004', 'member');
182
+ }
183
+ step "g2_commit" { COMMIT; }
184
+
185
+ session "check"
186
+ step "check" {
187
+ select a.handle, bm.role, bm.membership_state
188
+ from agent_board.board_memberships bm join agent_board.agents a on a.id = bm.agent_id
189
+ where bm.board_id = '67430000-0000-4000-8000-000000000001'
190
+ order by a.handle, bm.membership_state;
191
+ }
192
+
193
+ # I-1: the target is promoted through the product while it is being demoted.
194
+ permutation "m_begin" "m_redeem" "a_begin" "a_grant_m_observer" "m_commit" "a_rollback" "check"
195
+ # I-2: eviction during a grant.
196
+ permutation "o_begin" "o_remove_m" "a_begin" "a_grant_m_observer" "o_commit" "a_rollback" "check"
197
+ # I-3: two grants on one board, advisory-serialised. Demotion first: the grantor's role is
198
+ # read AFTER the advisory wait (C-A refuses). Grant first: both succeed.
199
+ permutation "o_begin" "o_demote_a" "a_begin" "a_grant_m_observer" "o_commit" "a_rollback" "check"
200
+ permutation "a_begin" "a_grant_m_observer" "o_begin" "o_demote_a" "a_commit" "o_commit" "check"
201
+ # I-4: FK-share non-interference -- the grant completes without blocking. The poster commits
202
+ # BEFORE the grantor so that a grant which DID block (M7b, FOR UPDATE) is released and the
203
+ # golden diff shows `<waiting ...>`/`<... completed>` instead of a truncated run.
204
+ permutation "p_begin" "p_post" "p_holds_m_fk" "o_begin" "o_grant_m_observer" "p_commit" "o_commit" "check"
205
+ # I-5: a concurrent raise of the grantor is seen as the older, lower rank (refusal), never
206
+ # a wrongful allow; the retry after commit succeeds.
207
+ permutation "g_begin" "g_redeem" "g2_begin" "g2_grant_v_member" "g2_rollback" "g_commit" "g2_retry_begin" "g2_retry_grant_v_member" "g2_commit" "check"
@@ -0,0 +1,81 @@
1
+ # Concurrent mints from a 59-row mixed rolling-hour census share the true root
2
+ # lock. Expired legacy grants and revoked session mints still consume the hour.
3
+ setup
4
+ {
5
+ grant usage on schema extensions to service_role;
6
+ insert into auth.users(id) values ('77000000-0000-4000-8000-000000000001');
7
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, owner_user_id)
8
+ values ('78000000-0000-4000-8000-000000000001', 'agent', 'sid-limits-root', 'SID limits root', 'active',
9
+ '77000000-0000-4000-8000-000000000001');
10
+ insert into agent_board.session_identities(agent_id, root_agent_id, depth)
11
+ values ('78000000-0000-4000-8000-000000000001', '78000000-0000-4000-8000-000000000001', 0);
12
+ insert into agent_board.delegation_policies(id, root_agent_id, mode, max_active_children, allowed_scopes, child_ttl_ceiling)
13
+ values ('79000000-0000-4000-8000-000000000001', '78000000-0000-4000-8000-000000000001', 'root_minted', 200, '[]', interval '8 hours');
14
+ do $$ declare n integer; child uuid; begin
15
+ for n in 1..59 loop
16
+ child := gen_random_uuid();
17
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, owner_user_id, parent_agent_id, root_agent_id)
18
+ values (child, 'agent', 'sid-limits-history-' || n, 'SID limits history', case when n <= 30 then 'active' else 'revoked' end,
19
+ '77000000-0000-4000-8000-000000000001', '78000000-0000-4000-8000-000000000001', '78000000-0000-4000-8000-000000000001');
20
+ insert into agent_board.session_identities(agent_id, parent_agent_id, root_agent_id, depth, identity_kind, harness, tombstoned_at, tombstone_reason)
21
+ values (child, '78000000-0000-4000-8000-000000000001', '78000000-0000-4000-8000-000000000001', 1,
22
+ case when n <= 30 then 'legacy' else 'session' end, case when n <= 30 then null else 'codex' end,
23
+ case when n <= 30 then null else now() end, case when n <= 30 then null else 'revoked' end);
24
+ insert into agent_board.delegation_grants(child_agent_id, root_agent_id, parent_agent_id, delegation_policy_id, granted_scopes, grant_expires_at, revoked_at)
25
+ values (child, '78000000-0000-4000-8000-000000000001', '78000000-0000-4000-8000-000000000001',
26
+ '79000000-0000-4000-8000-000000000001', '[]', now() - interval '1 hour', case when n <= 30 then null else now() end);
27
+ end loop;
28
+ end $$;
29
+ }
30
+
31
+ teardown
32
+ {
33
+ do $$ declare v_auth_ids uuid[]; begin
34
+ select array_agg(auth_user_id) into v_auth_ids from agent_board.agents
35
+ where root_agent_id = '78000000-0000-4000-8000-000000000001' and auth_user_id is not null;
36
+ delete from agent_board.session_id_aliases where root_agent_id = '78000000-0000-4000-8000-000000000001';
37
+ delete from agent_board.session_identities where root_agent_id = '78000000-0000-4000-8000-000000000001';
38
+ delete from agent_board.agents where root_agent_id = '78000000-0000-4000-8000-000000000001';
39
+ delete from agent_board.agents where id = '78000000-0000-4000-8000-000000000001';
40
+ delete from auth.users where id = any(v_auth_ids) or id = '77000000-0000-4000-8000-000000000001';
41
+ end $$;
42
+ }
43
+
44
+ session "a"
45
+ step "a_begin" { BEGIN; SET LOCAL ROLE service_role; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
46
+ step "a_mint" {
47
+ do $$ begin
48
+ perform * from agent_board.mint_delegated_child('78000000-0000-4000-8000-000000000001', 'sid-limit-a', '[]',
49
+ public.test_pubkey('sid-limit-a'), interval '1 hour', false, 'codex', 'limit-a');
50
+ end $$;
51
+ }
52
+ step "a_commit" { COMMIT; }
53
+
54
+ session "b"
55
+ step "b_begin" { BEGIN; SET LOCAL ROLE service_role; SET LOCAL "request.jwt.claim.role" = 'service_role'; }
56
+ step "b_mint" {
57
+ do $$ begin
58
+ perform * from agent_board.mint_delegated_child('78000000-0000-4000-8000-000000000001', 'sid-limit-b', '[]',
59
+ public.test_pubkey('sid-limit-b'), interval '1 hour', false, 'codex', 'limit-b');
60
+ end $$;
61
+ }
62
+ step "b_duplicate" {
63
+ do $$ begin
64
+ perform * from agent_board.mint_delegated_child('78000000-0000-4000-8000-000000000001', 'sid-limit-b', '[]',
65
+ public.test_pubkey('sid-limit-b'), interval '1 hour', false, 'codex', 'limit-a');
66
+ end $$;
67
+ }
68
+ step "b_commit" { COMMIT; }
69
+
70
+ session "check"
71
+ step "check" {
72
+ select count(*) as mixed_hourly, count(*) filter (where revoked_at is not null) as tombstoned_hourly
73
+ from agent_board.delegation_grants where root_agent_id = '78000000-0000-4000-8000-000000000001';
74
+ select count(*) as new_principals from agent_board.agents where handle in ('sid-limit-a', 'sid-limit-b');
75
+ select count(*) as aliases from agent_board.session_id_aliases where root_agent_id = '78000000-0000-4000-8000-000000000001';
76
+ }
77
+
78
+ permutation "a_begin" "a_mint" "b_begin" "b_mint" "a_commit" "b_commit" "check"
79
+ permutation "b_begin" "b_mint" "a_begin" "a_mint" "b_commit" "a_commit" "check"
80
+ permutation "a_begin" "a_mint" "b_begin" "b_duplicate" "a_commit" "b_commit" "check"
81
+ permutation "b_begin" "b_duplicate" "a_begin" "a_mint" "b_commit" "a_commit" "check"