@consiliency/agent-board-schema 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1010 @@
1
+ -- #276 pgTAP: a fleet-revoked agent loses BOARD AUTHORITY, not only send.
2
+ --
3
+ -- The claim under test: an agent carrying an agent-subject `fleet_revocation_events` row cannot
4
+ -- complete a board-role grant, a board authority-ceiling change, or a role-gate flip — on the
5
+ -- unsigned `authenticated` route or the signed gateway route — while an agent that was never in
6
+ -- a fleet is completely unaffected.
7
+ --
8
+ -- THE DIVERGENCE, asserted rather than described. `revoke_fleet_subject('agent', X)` leaves
9
+ -- `agents.status = 'active'` and X's signing keys live; only the ledger row and the bindings
10
+ -- change. Section 4 asserts that precondition POSITIVELY before probing the refusal, so a
11
+ -- future change that (say) also flipped `agents.status` would make these tests pass for the
12
+ -- WRONG reason and section 4 is what would catch it.
13
+ --
14
+ -- WHAT THIS FILE CAN AND CANNOT REACH. A pgTAP file runs in ONE transaction, so it proves the
15
+ -- PREDICATE (a ledger-revoked grantor is refused) but structurally cannot produce the
16
+ -- interleaving (`revoke_fleet_subject` committing between an authority write's ledger read and
17
+ -- its commit) or the `agents` FOR SHARE lock that serializes it. That lives in
18
+ -- tests/isolation/specs/ledger_authority_revoke.spec, driven by isolationtester across
19
+ -- committed sessions.
20
+ --
21
+ -- Methodology, inherited from agent_board_signer_lifecycle.test.sql:
22
+ -- * RPC probes are driven by the JWT claim blob only; SET LOCAL ROLE is never used, since it
23
+ -- would revoke _is_service_role() and mask the branch behind a permission error.
24
+ -- * Allow-side probes are lives_ok(...) then a SEPARATE state check. A bare
25
+ -- `select ... from <raising rpc>(...)` ABORTS the transaction instead of failing it,
26
+ -- silently dropping every later assertion (#113, CLAUDE.md rule 3).
27
+ -- * Every refusal probe is followed by a state check, because a gate that raises AND writes
28
+ -- is the failure mode throws_like alone cannot see.
29
+ -- * The class assertions in section 9 are catalog-derived (pg_proc + stripped_body), not a
30
+ -- list written here, so a NEW authority write that forgets the gate fails and NAMES ITSELF
31
+ -- (CLAUDE.md rule 1). The one exemption is asserted POSITIVELY — by three properties it
32
+ -- actually has — never by the absence of a match.
33
+
34
+ create extension if not exists pgtap with schema extensions;
35
+ set search_path = extensions, public, agent_board;
36
+
37
+ begin;
38
+
39
+ select plan(88);
40
+
41
+ create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
42
+ begin
43
+ perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',p_sub)::text, true);
44
+ end $$;
45
+ create function pg_temp.service() returns void language plpgsql as $$
46
+ begin
47
+ perform set_config('request.jwt.claims', '', true);
48
+ end $$;
49
+
50
+ -- Fixture -------------------------------------------------------------------
51
+ -- agent 1 = FLEET agent, owner of board 1, bound to machines A and B. The #276 subject.
52
+ -- agent 2 = grantee, an active member of every board here.
53
+ -- agent 3 = NON-FLEET agent, owner of board 2. It has NO fleet_machine_agents row at all —
54
+ -- the exemption category judgement 2 is about.
55
+ -- agent 4 = FLEET agent, owner of board 3, bound to machines A and B. Used for the
56
+ -- multi-binding case: its binding on machine A is revoked, B stays active.
57
+ -- agent 5 = operator principal that performs the revocations.
58
+
59
+ insert into auth.users(id, instance_id, aud, role, email)
60
+ select ('27600000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid,
61
+ '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated',
62
+ 'k276u'||n||'@t.test'
63
+ from generate_series(1,6) n;
64
+
65
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
66
+ select ('27610000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid,
67
+ 'agent','agent','k276-agent-'||n,'K276 Agent '||n,'active',
68
+ ('27600000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid
69
+ from generate_series(1,6) n;
70
+
71
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
72
+ values
73
+ ('27620000-0000-4000-8000-000000000001','k276-board-1','K276 Board 1','27610000-0000-4000-8000-000000000001','private','active'),
74
+ ('27620000-0000-4000-8000-000000000002','k276-board-2','K276 Board 2','27610000-0000-4000-8000-000000000003','private','active'),
75
+ ('27620000-0000-4000-8000-000000000003','k276-board-3','K276 Board 3','27610000-0000-4000-8000-000000000004','private','active'),
76
+ -- board 4 exists solely for the invite ALLOW side: its owner (agent 6) is never revoked
77
+ -- and never role-clamped, so a failure there is the gate and not fixture coupling.
78
+ ('27620000-0000-4000-8000-000000000004','k276-board-4','K276 Board 4','27610000-0000-4000-8000-000000000006','private','active');
79
+
80
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
81
+ values
82
+ ('27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000001','owner','joined','27610000-0000-4000-8000-000000000001'),
83
+ ('27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002','member','joined','27610000-0000-4000-8000-000000000001'),
84
+ ('27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000003','owner','joined','27610000-0000-4000-8000-000000000003'),
85
+ ('27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000002','member','joined','27610000-0000-4000-8000-000000000003'),
86
+ ('27620000-0000-4000-8000-000000000003','27610000-0000-4000-8000-000000000004','owner','joined','27610000-0000-4000-8000-000000000004'),
87
+ ('27620000-0000-4000-8000-000000000003','27610000-0000-4000-8000-000000000002','member','joined','27610000-0000-4000-8000-000000000004'),
88
+ ('27620000-0000-4000-8000-000000000004','27610000-0000-4000-8000-000000000006','owner','joined','27610000-0000-4000-8000-000000000006');
89
+
90
+ -- Agent 1's live signing key: the signed route needs a decision naming a live, owned key so
91
+ -- that a refusal can only be the #276 gate and never #269's revalidation.
92
+ insert into agent_board.agent_signing_keys(
93
+ id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint,
94
+ status, proof_state, expires_at, revoked_at)
95
+ values
96
+ ('27630000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000001',
97
+ 'primary', 1, public.test_pubkey('k276-a'), 'raw_ed25519_base64url',
98
+ public.test_key_fingerprint('k276-a'), 'active', 'proven', null, null);
99
+
100
+ -- Fleet substrate: one trust domain, two ACTIVE machines, and bindings for agents 1 and 4 on
101
+ -- BOTH machines. Two machines is what makes judgement 1 testable at all.
102
+ insert into agent_board.fleet_trust_domains(id, slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
103
+ values ('27640000-0000-4000-8000-000000000001','k276-domain','K276 Domain','tailnet',
104
+ '27610000-0000-4000-8000-000000000005','active', now());
105
+
106
+ insert into agent_board.fleet_machines(id, trust_domain_id, owner_agent_id, machine_label, machine_fingerprint, lifecycle_state, enrolled_at)
107
+ values
108
+ ('27650000-0000-4000-8000-00000000000a','27640000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000005','k276-machine-a','fp-k276-a','active', now()),
109
+ ('27650000-0000-4000-8000-00000000000b','27640000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000005','k276-machine-b','fp-k276-b','active', now());
110
+
111
+ insert into agent_board.fleet_machine_agents(id, machine_id, agent_id, agent_role, lifecycle_state)
112
+ values
113
+ ('27660000-0000-4000-8000-00000000001a','27650000-0000-4000-8000-00000000000a','27610000-0000-4000-8000-000000000001','phase_worker','active'),
114
+ ('27660000-0000-4000-8000-00000000001b','27650000-0000-4000-8000-00000000000b','27610000-0000-4000-8000-000000000001','phase_worker','active'),
115
+ ('27660000-0000-4000-8000-00000000004a','27650000-0000-4000-8000-00000000000a','27610000-0000-4000-8000-000000000004','phase_worker','active'),
116
+ ('27660000-0000-4000-8000-00000000004b','27650000-0000-4000-8000-00000000000b','27610000-0000-4000-8000-000000000004','phase_worker','active');
117
+
118
+ create function pg_temp.gwa() returns jsonb language sql immutable as $$
119
+ select jsonb_build_object(
120
+ 'agent_id', '27610000-0000-4000-8000-000000000001',
121
+ 'signing_key_id', '27630000-0000-4000-8000-000000000001',
122
+ 'key_id', 'primary',
123
+ 'key_version', 1,
124
+ 'algorithm', 'Ed25519',
125
+ 'verification_status', 'verified',
126
+ 'payload_digest', 'sha256:' || repeat('a', 64),
127
+ 'signature_digest', 'sha256:' || repeat('b', 64),
128
+ 'canonicalization_version', 'agent-provenance-jcs.v1',
129
+ 'enforcement_mode', 'require_for_agent_writes',
130
+ 'redacted', true);
131
+ $$;
132
+
133
+ create function pg_temp.grant_payload(p_role text) returns jsonb language sql immutable as $$
134
+ select jsonb_build_object(
135
+ 'p_board_id', '27620000-0000-4000-8000-000000000001',
136
+ 'p_agent_id', '27610000-0000-4000-8000-000000000002',
137
+ 'p_grantee_role', p_role);
138
+ $$;
139
+
140
+ create function pg_temp.role_on(p_board uuid, p_agent uuid) returns text language sql stable as $$
141
+ select role from agent_board.board_memberships where board_id = p_board and agent_id = p_agent;
142
+ $$;
143
+
144
+ -- ===========================================================================
145
+ -- 1. The gate helper exists, is unreachable by a caller, and is documented.
146
+ -- A SECURITY DEFINER function grants EXECUTE to PUBLIC by default and this one locks and
147
+ -- reads rows for an arbitrary agent id with no authorization of its own (#118).
148
+ -- ===========================================================================
149
+
150
+ select has_function('agent_board', '_assert_actor_not_fleet_revoked', array['uuid','text'],
151
+ 'the fleet-revocation gate helper exists');
152
+
153
+ select is(
154
+ (select string_agg(distinct coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'), ','
155
+ order by coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'))
156
+ from pg_proc p, aclexplode(p.proacl) a
157
+ where p.oid = 'agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure
158
+ and a.privilege_type = 'EXECUTE'),
159
+ 'postgres,service_role',
160
+ 'the gate helper executes for service_role only (never PUBLIC/anon/authenticated)');
161
+
162
+ select isnt(
163
+ (select obj_description('agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure, 'pg_proc')),
164
+ null,
165
+ 'the gate helper carries its COMMENT');
166
+
167
+ -- The null branch is unreachable from all three chokepoints (each resolves or gates its actor
168
+ -- first), so it is pinned by calling the helper DIRECTLY rather than left as a branch no
169
+ -- mutation could falsify (CLAUDE.md rule 2).
170
+ select pg_temp.service();
171
+
172
+ select throws_like(
173
+ $$select agent_board._assert_actor_not_fleet_revoked(null::uuid, 'probe')$$,
174
+ '%fleet_revoked_unresolved%',
175
+ 'the gate fails closed on a null actor');
176
+
177
+ -- ===========================================================================
178
+ -- 2. ALLOW SIDE, unsigned and signed: a fleet-bound but NOT-revoked owner is untouched.
179
+ -- This is the blast-radius assertion — the gate must cost an unrevoked fleet agent nothing.
180
+ -- ===========================================================================
181
+
182
+ select pg_temp.claims('27600000-0000-0000-0000-000000000001');
183
+
184
+ select lives_ok(
185
+ $$create temp table _c1 as
186
+ select * from agent_board.grant_board_role(
187
+ '27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002','admin')$$,
188
+ 'an ACTIVE, fleet-BOUND, unrevoked owner still completes an unsigned grant');
189
+
190
+ select is(
191
+ pg_temp.role_on('27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002'),
192
+ 'admin',
193
+ 'and it actually raised the grantee role');
194
+
195
+ select pg_temp.service();
196
+
197
+ select lives_ok(
198
+ $$create temp table _sg as
199
+ select * from agent_board.signed_grant_board_role(pg_temp.grant_payload('member'), pg_temp.gwa())$$,
200
+ 'and the SIGNED route by the same unrevoked owner still completes');
201
+
202
+ select is(
203
+ pg_temp.role_on('27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002'),
204
+ 'member',
205
+ 'and the signed grant actually wrote the grantee role');
206
+
207
+ -- ===========================================================================
208
+ -- 3. JUDGEMENT 2, asserted POSITIVELY: a principal with NO fleet binding is unaffected.
209
+ -- A predicate phrased as "has an active binding" would refuse this grant. Never asserted by
210
+ -- the absence of a match — agent 3 completes a real grant and the role is read back.
211
+ -- ===========================================================================
212
+
213
+ select is(
214
+ (select count(*)::int from agent_board.fleet_machine_agents
215
+ where agent_id = '27610000-0000-4000-8000-000000000003'),
216
+ 0,
217
+ 'agent 3 has NO fleet binding at all — the exemption category, established before it is used');
218
+
219
+ select pg_temp.claims('27600000-0000-0000-0000-000000000003');
220
+
221
+ select lives_ok(
222
+ $$create temp table _c2 as
223
+ select * from agent_board.grant_board_role(
224
+ '27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000002','admin')$$,
225
+ 'a NON-FLEET owner still completes a grant [judgement 2: no capability regression]');
226
+
227
+ select is(
228
+ pg_temp.role_on('27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000002'),
229
+ 'admin',
230
+ 'and the non-fleet grant actually wrote the role');
231
+
232
+ select lives_ok(
233
+ $$create temp table _c3 as
234
+ select * from agent_board.set_board_role_gate('27620000-0000-4000-8000-000000000002', true)$$,
235
+ 'a NON-FLEET owner still flips the role gate');
236
+
237
+ select lives_ok(
238
+ $$create temp table _c4 as
239
+ select * from agent_board.set_board_authority_ceiling('27620000-0000-4000-8000-000000000002','owner')$$,
240
+ 'a NON-FLEET owner still sets the board authority ceiling');
241
+
242
+ select is(
243
+ (select max_grantable_role from agent_board.board_authority_ceilings
244
+ where board_id = '27620000-0000-4000-8000-000000000002'),
245
+ 'owner',
246
+ 'and the non-fleet ceiling write landed');
247
+
248
+ -- ===========================================================================
249
+ -- 4. THE PRECONDITION, asserted before the refusal is probed. revoke_fleet_subject('agent')
250
+ -- changes the ledger and the bindings and NOTHING else — the agent stays active and its
251
+ -- signing key stays live. Without this, every refusal below could be passing because some
252
+ -- OTHER predicate started failing, which is the "succeeds/fails for the wrong reason"
253
+ -- trap this repo has been bitten by.
254
+ -- ===========================================================================
255
+
256
+ select pg_temp.service();
257
+
258
+ select lives_ok(
259
+ $$create temp table _rv as
260
+ select * from agent_board.revoke_fleet_subject(
261
+ 'agent','27610000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000005',
262
+ 'k276 operator revocation')$$,
263
+ 'revoke_fleet_subject(agent, 1) succeeds');
264
+
265
+ select is(
266
+ (select count(*)::int from agent_board.fleet_revocation_events
267
+ where subject_kind = 'agent' and subject_id = '27610000-0000-4000-8000-000000000001'),
268
+ 1,
269
+ 'it wrote exactly one agent-subject ledger row');
270
+
271
+ select is(
272
+ (select status from agent_board.agents where id = '27610000-0000-4000-8000-000000000001'),
273
+ 'active',
274
+ 'PRECONDITION: the revoked agent is STILL active — the ledger row is the only authority signal');
275
+
276
+ select is(
277
+ (select status from agent_board.agent_signing_keys where id = '27630000-0000-4000-8000-000000000001'),
278
+ 'active',
279
+ 'PRECONDITION: its signing key is STILL live, so #269 revalidation cannot be what refuses below');
280
+
281
+ select is(
282
+ (select role from agent_board.board_memberships
283
+ where board_id = '27620000-0000-4000-8000-000000000001'
284
+ and agent_id = '27610000-0000-4000-8000-000000000001'),
285
+ 'owner',
286
+ 'PRECONDITION: it is STILL a board owner, so the owner gate cannot be what refuses below');
287
+
288
+ -- ===========================================================================
289
+ -- 5. THE REFUSAL: unsigned route. This is the wider door — `authenticated`, no gateway.
290
+ -- ===========================================================================
291
+
292
+ select pg_temp.claims('27600000-0000-0000-0000-000000000001');
293
+
294
+ select throws_like(
295
+ $$select * from agent_board.grant_board_role(
296
+ '27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002','owner')$$,
297
+ '%fleet_revoked:%grant_board_role%',
298
+ 'a ledger-revoked grantor cannot complete an UNSIGNED grant');
299
+
300
+ select is(
301
+ pg_temp.role_on('27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002'),
302
+ 'member',
303
+ 'and the refused unsigned grant wrote nothing');
304
+
305
+ -- ===========================================================================
306
+ -- 6. THE REFUSAL: signed route. Inherited by construction — the check lives INSIDE
307
+ -- grant_board_role, and signed_grant_board_role is its only other referrer in the database.
308
+ -- Asserted anyway, because "by construction" is a claim until something fails on it.
309
+ -- ===========================================================================
310
+
311
+ select pg_temp.service();
312
+
313
+ select throws_like(
314
+ $$select * from agent_board.signed_grant_board_role(pg_temp.grant_payload('owner'), pg_temp.gwa())$$,
315
+ '%fleet_revoked:%grant_board_role%',
316
+ 'a ledger-revoked signer cannot complete a SIGNED grant either [Portal #362 divergence test]');
317
+
318
+ select is(
319
+ pg_temp.role_on('27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002'),
320
+ 'member',
321
+ 'and the refused signed grant wrote nothing');
322
+
323
+ -- IDENTITY, not a count. `count(*) = 1` would still read 1 if the ALLOW-side write stopped
324
+ -- recording and the REFUSAL path started recording instead — the two errors cancel, and the
325
+ -- assertion cannot tell the difference. Pinning the surviving row's id to the one the
326
+ -- section-2 allow-side call returned makes both halves observable: a second row breaks the
327
+ -- count half, a substituted row breaks the identity half.
328
+ select is(
329
+ (select string_agg(se.id::text, ',' order by se.id) from agent_board.signature_evidence se
330
+ where se.operation = 'grant_board_role'),
331
+ (select sg.signature_evidence_id::text from _sg sg),
332
+ 'and it recorded no NEW evidence row — the only grant_board_role evidence in the database is '
333
+ 'still exactly the row section 2''s allow-side signed write returned');
334
+
335
+ select throws_like(
336
+ $$select * from agent_board.signed_set_board_authority_ceiling(
337
+ jsonb_build_object('p_board_id','27620000-0000-4000-8000-000000000001',
338
+ 'p_max_grantable_role','observer'),
339
+ pg_temp.gwa())$$,
340
+ '%fleet_revoked:%set_board_authority_ceiling%',
341
+ 'and the SIGNED ceiling wrapper is refused for the same principal');
342
+
343
+ -- A service_role gateway that SUPPLIES the revoked principal as the grantor argument is
344
+ -- refused too: the gate binds the principal the write is attributed to, not the connection role.
345
+ select throws_like(
346
+ $$select * from agent_board.grant_board_role(
347
+ '27620000-0000-4000-8000-000000000001','27610000-0000-4000-8000-000000000002','owner',
348
+ '27610000-0000-4000-8000-000000000001')$$,
349
+ '%fleet_revoked:%',
350
+ 'service_role supplying the revoked principal as p_grantor_agent_id is refused too');
351
+
352
+ -- Back to the authenticated owner: the signed probes above ran as service_role, and the two
353
+ -- setters gate on current_agent_id(), which service_role does not have.
354
+ select pg_temp.claims('27600000-0000-0000-0000-000000000001');
355
+
356
+ -- The two SETTERS are probed last in this section deliberately. Each refusal probe leaves a
357
+ -- write behind when the gate is removed, and a ceiling written by an earlier probe would then
358
+ -- make the signed grant below fail on _grant_within_ceiling instead of on the gate -- masking
359
+ -- the evidence-row assertion from every mutation. Ordering is load-bearing, not cosmetic.
360
+ select throws_like(
361
+ $$select * from agent_board.set_board_authority_ceiling(
362
+ '27620000-0000-4000-8000-000000000001','observer')$$,
363
+ '%fleet_revoked:%set_board_authority_ceiling%',
364
+ 'a ledger-revoked owner cannot change the board authority ceiling');
365
+
366
+ select is(
367
+ (select count(*)::int from agent_board.board_authority_ceilings
368
+ where board_id = '27620000-0000-4000-8000-000000000001'),
369
+ 0,
370
+ 'and the refused ceiling change wrote no ceiling row (the raise rolled the upsert back)');
371
+
372
+ select throws_like(
373
+ $$select * from agent_board.set_board_role_gate('27620000-0000-4000-8000-000000000001', true)$$,
374
+ '%fleet_revoked:%set_board_role_gate%',
375
+ 'a ledger-revoked owner cannot flip the board role gate');
376
+
377
+ select is(
378
+ (select role_gate_enforced from agent_board.boards where id = '27620000-0000-4000-8000-000000000001'),
379
+ false,
380
+ 'and the refused gate flip left the boards row alone (the raise rolled the UPDATE back)');
381
+
382
+ -- ===========================================================================
383
+ -- 7. JUDGEMENT 1 — the multi-binding case, and the agreement with the send path.
384
+ --
385
+ -- Agent 4 is bound to machines A and B. Its binding on A is revoked permanently; B stays
386
+ -- active. That is a PER-MACHINE fact. evaluate_fleet_sender_policy answers it by candidate
387
+ -- selection (it finds B and proceeds), so authority must too. Only the AGENT-SUBJECT ledger
388
+ -- row is agent-wide, and then both must refuse. Both halves are asserted on BOTH paths, so
389
+ -- the two can never be right separately and wrong together.
390
+ -- ===========================================================================
391
+
392
+ select pg_temp.service();
393
+
394
+ select lives_ok(
395
+ $$create temp table _rb as
396
+ select * from agent_board.revoke_fleet_machine_agent_binding(
397
+ '27660000-0000-4000-8000-00000000004a','27610000-0000-4000-8000-000000000005',
398
+ 'k276 per-machine binding revocation')$$,
399
+ 'agent 4''s binding on machine A is revoked; its binding on machine B stays active');
400
+
401
+ select is(
402
+ (select lifecycle_state from agent_board.fleet_machine_agents
403
+ where id = '27660000-0000-4000-8000-00000000004b'),
404
+ 'active',
405
+ 'PRECONDITION: the machine-B binding really is still active (the multi-binding shape exists)');
406
+
407
+ select is(
408
+ (select denial_class from agent_board.evaluate_fleet_sender_policy(
409
+ '27610000-0000-4000-8000-000000000004','27610000-0000-4000-8000-000000000002',
410
+ '27650000-0000-4000-8000-00000000000b')),
411
+ 'recipient_policy_denied',
412
+ 'SEND PATH: one revoked binding does not make agent 4 machine_revoked — it gets past the '
413
+ 'revocation check on its surviving binding');
414
+
415
+ select pg_temp.claims('27600000-0000-0000-0000-000000000004');
416
+
417
+ select lives_ok(
418
+ $$create temp table _c5 as
419
+ select * from agent_board.grant_board_role(
420
+ '27620000-0000-4000-8000-000000000003','27610000-0000-4000-8000-000000000002','admin')$$,
421
+ 'AUTHORITY PATH agrees: one revoked binding does not remove agent 4''s ability to grant');
422
+
423
+ select is(
424
+ pg_temp.role_on('27620000-0000-4000-8000-000000000003','27610000-0000-4000-8000-000000000002'),
425
+ 'admin',
426
+ 'and that grant really landed');
427
+
428
+ -- Now the AGENT-WIDE act, on the same agent, with a binding still nominally in play.
429
+ select pg_temp.service();
430
+
431
+ -- Mint an admin link for agent 4 while it is STILL unrevoked. Section 10 redeems it: this is
432
+ -- the outstanding-link case, the one the mint gate structurally cannot reach and the reason
433
+ -- both halves of the invite route are gated.
434
+ select lives_ok(
435
+ $$create temp table _pre as select * from agent_board.create_invite_link(
436
+ '27620000-0000-4000-8000-000000000003','admin','joined',5, now() + interval '2 days',
437
+ '27610000-0000-4000-8000-000000000004')$$,
438
+ 'agent 4 mints an admin invite link BEFORE it is revoked');
439
+
440
+ select lives_ok(
441
+ $$create temp table _rv4 as
442
+ select * from agent_board.revoke_fleet_subject(
443
+ 'agent','27610000-0000-4000-8000-000000000004','27610000-0000-4000-8000-000000000005',
444
+ 'k276 agent-wide revocation')$$,
445
+ 'agent 4 is now revoked as an agent-SUBJECT');
446
+
447
+ select is(
448
+ (select denial_class from agent_board.evaluate_fleet_sender_policy(
449
+ '27610000-0000-4000-8000-000000000004','27610000-0000-4000-8000-000000000002',
450
+ '27650000-0000-4000-8000-00000000000b')),
451
+ 'machine_revoked',
452
+ 'SEND PATH: the agent-subject ledger row denies agent 4 on the machine it was still bound to');
453
+
454
+ select pg_temp.claims('27600000-0000-0000-0000-000000000004');
455
+
456
+ select throws_like(
457
+ $$select * from agent_board.grant_board_role(
458
+ '27620000-0000-4000-8000-000000000003','27610000-0000-4000-8000-000000000002','owner')$$,
459
+ '%fleet_revoked:%',
460
+ 'AUTHORITY PATH agrees: the agent-subject row denies agent 4 too [judgement 1: one answer]');
461
+
462
+ select is(
463
+ pg_temp.role_on('27620000-0000-4000-8000-000000000003','27610000-0000-4000-8000-000000000002'),
464
+ 'admin',
465
+ 'and the refused grant left agent 2 where the earlier grant put it');
466
+
467
+ -- ===========================================================================
468
+ -- 8. NO SUPERSESSION CLAUSE, mirrored from the send path. reactivate_fleet_machine stamps a
469
+ -- supersession point that the sender policy honours for MACHINE-subject events only; it
470
+ -- never un-revokes a sender agent. A future `effective_at` comparison added to the gate
471
+ -- would let a reactivated machine silently restore a revoked agent's authority, so the
472
+ -- absence is pinned as a behaviour, not left to the comment.
473
+ -- ===========================================================================
474
+
475
+ select pg_temp.service();
476
+
477
+ select lives_ok(
478
+ $$create temp table _rm as
479
+ select * from agent_board.revoke_fleet_subject(
480
+ 'machine','27650000-0000-4000-8000-00000000000b','27610000-0000-4000-8000-000000000005',
481
+ 'k276 machine revocation')$$,
482
+ 'machine B is revoked, so it has a machine-subject ledger row that CAN be superseded');
483
+
484
+ select lives_ok(
485
+ $$create temp table _ra as
486
+ select * from agent_board.reactivate_fleet_machine(
487
+ '27650000-0000-4000-8000-00000000000b','27610000-0000-4000-8000-000000000005','k276 reactivation')$$,
488
+ 'and it is reactivated, stamping the supersession point AFTER agent 4''s agent-subject revocation');
489
+
490
+ select pg_temp.claims('27600000-0000-0000-0000-000000000004');
491
+
492
+ select throws_like(
493
+ $$select * from agent_board.grant_board_role(
494
+ '27620000-0000-4000-8000-000000000003','27610000-0000-4000-8000-000000000002','owner')$$,
495
+ '%fleet_revoked:%',
496
+ 'a machine reactivation does NOT restore a ledger-revoked agent''s authority (no supersession)');
497
+
498
+ -- ===========================================================================
499
+ -- 9. CLASS ASSERTIONS (catalog-derived, CLAUDE.md rule 1).
500
+ --
501
+ -- The class is not "the three functions #276 happened to edit". It is: every agent_board
502
+ -- function that WRITES a board-authority relation, derived from pg_proc by scanning stripped
503
+ -- bodies (never raw prosrc — #196/#199, a comment or a literal naming a marker must not be
504
+ -- able to satisfy any of these). Both sides are derived and compared, so a new authority
505
+ -- write that omits the gate appears in one set and not the other and the difference NAMES it.
506
+ --
507
+ -- THE MEMBERSHIPS ARM MATCHES ANY INSERT, and the reason is a defect this assertion had.
508
+ -- It first matched `update … set … role =` only. Review found `create_board` — which is
509
+ -- EXECUTE-able by `authenticated` — seeding `role = 'owner'` with an INSERT, and its
510
+ -- `on conflict … do update set role = 'owner'` is an UPDATE of bm.role that the UPDATE-shaped
511
+ -- regex STILL could not match, because the table name precedes `do update`. The description
512
+ -- said "WRITE" while the regex checked one syntax: a comment claiming more than the assertion
513
+ -- does, which is this repo's recorded failure shape.
514
+ --
515
+ -- Shape-blind costs nothing and is strictly stronger. Measured: the any-insert form names the
516
+ -- SAME five membership writers a role-token form does. It subsumes on-conflict by
517
+ -- construction (that clause exists only inside an insert) and it also catches a positional
518
+ -- insert naming no columns, which any column-matching form would miss.
519
+ -- ===========================================================================
520
+
521
+ select is(
522
+ (select string_agg(p.proname, ',' order by p.proname)
523
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
524
+ where n.nspname = 'agent_board'
525
+ and (agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+agent_board\.board_memberships\M'
526
+ or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.board_memberships\M[^;]*\mset\M[^;]*\mrole\s*='
527
+ or agent_board_test.stripped_body(p.oid) ~* '(insert\s+into|update)\s+agent_board\.board_authority_ceilings\M'
528
+ or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.boards\M[^;]*\mset\M[^;]*\mrole_gate_enforced\s*='
529
+ or agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+agent_board\.board_invite_tokens\M')),
530
+ '_fedsession_autojoin_default_board,_redeem_unbounded_invite_for_agent,'
531
+ || '_upsert_board_membership_state,bootstrap_board_owner_memberships,create_board,'
532
+ || 'create_invite_link,grant_board_role,set_board_authority_ceiling,set_board_role_gate',
533
+ 'the functions that WRITE a board-authority relation — in ANY shape, including a mintable '
534
+ 'invite preset_role — are exactly these nine');
535
+
536
+ -- EITHER helper counts as consulting the ledger: the invite redeem path calls the UNLOCKED
537
+ -- predicate (see the migration header), so a side that matched only the share-locking assert
538
+ -- would report the gated redeem path as ungated. The two helpers are excluded because they ARE
539
+ -- the gate — _assert_actor_not_fleet_revoked calls _actor_fleet_revoked, so it matches its own
540
+ -- regex.
541
+ select is(
542
+ (select string_agg(p.proname, ',' order by p.proname)
543
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
544
+ where n.nspname = 'agent_board'
545
+ and p.proname not in ('_assert_actor_not_fleet_revoked', '_actor_fleet_revoked')
546
+ and agent_board_test.stripped_body(p.oid) ~ '\m(_assert_actor_not_fleet_revoked|_actor_fleet_revoked)\s*\('),
547
+ '_redeem_unbounded_invite_for_agent,create_invite_link,grant_board_role,'
548
+ || 'set_board_authority_ceiling,set_board_role_gate',
549
+ 'the functions that consult the fleet revocation ledger are exactly these five');
550
+
551
+ -- The gate proper: the two sets must differ by EXACTLY the four asserted exemptions, and any
552
+ -- other member of the difference is NAMED in the failure. Each excluded name is licensed by a
553
+ -- positive disposition below, never by the absence of a match.
554
+ select is(
555
+ (select string_agg(x.proname || ':' || x.why, '; ' order by x.proname)
556
+ from (
557
+ select p.proname,
558
+ case when agent_board_test.stripped_body(p.oid) ~ '\m(_assert_actor_not_fleet_revoked|_actor_fleet_revoked)\s*\('
559
+ then 'consults the ledger but writes no authority relation'
560
+ else 'writes an authority relation with NO ledger gate'
561
+ end as why
562
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
563
+ where n.nspname = 'agent_board'
564
+ and p.proname not in (
565
+ -- the four asserted exemptions...
566
+ '_upsert_board_membership_state', 'create_board',
567
+ '_fedsession_autojoin_default_board', 'bootstrap_board_owner_memberships',
568
+ -- ...and the two helpers, which ARE the gate rather than users of it
569
+ '_assert_actor_not_fleet_revoked', '_actor_fleet_revoked')
570
+ and ((agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+agent_board\.board_memberships\M'
571
+ or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.board_memberships\M[^;]*\mset\M[^;]*\mrole\s*='
572
+ or agent_board_test.stripped_body(p.oid) ~* '(insert\s+into|update)\s+agent_board\.board_authority_ceilings\M'
573
+ or agent_board_test.stripped_body(p.oid) ~* 'update\s+agent_board\.boards\M[^;]*\mset\M[^;]*\mrole_gate_enforced\s*='
574
+ or agent_board_test.stripped_body(p.oid) ~* 'insert\s+into\s+agent_board\.board_invite_tokens\M')
575
+ <> (agent_board_test.stripped_body(p.oid) ~ '\m(_assert_actor_not_fleet_revoked|_actor_fleet_revoked)\s*\('))
576
+ ) x),
577
+ null,
578
+ 'no function writes an authority relation without the ledger gate, or vice versa');
579
+
580
+ -- ---------------------------------------------------------------------------
581
+ -- EXEMPTION A — _upsert_board_membership_state (the unauthorized membership primitive).
582
+ --
583
+ -- The signature is asserted as an EXACT STRING, not as a `role` word match. `_` is a word
584
+ -- CHARACTER in POSIX regex, so there is no word boundary between `p_` and `role`:
585
+ -- 'p_role text' ~ '\mrole\M' -> false
586
+ -- 'p_grantee_role text' ~ '\mrole\M' -> false
587
+ -- and every parameter in this codebase is `p_*`. The word-match form this assertion first used
588
+ -- would therefore have stayed GREEN while somebody added `p_role` and threaded it into the role
589
+ -- write — destroying the very claim the assertion exists to make.
590
+ --
591
+ -- string_agg over ALL oids of the name, not a single-row lookup: `create or replace` cannot add
592
+ -- a parameter, so that mutation can only land as a new OVERLOAD, and a scalar subquery over two
593
+ -- oids ABORTS the file instead of failing one assertion (CLAUDE.md rule 3).
594
+ -- ---------------------------------------------------------------------------
595
+ select is(
596
+ (select string_agg(pg_get_function_identity_arguments(p.oid), ' | ' order by p.oid)
597
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
598
+ where n.nspname = 'agent_board' and p.proname = '_upsert_board_membership_state'),
599
+ 'p_board_id uuid, p_agent_id uuid, p_membership_state text, p_created_by_agent_id uuid, '
600
+ || 'p_metadata jsonb, p_membership_id uuid, p_event_kind text',
601
+ 'EXEMPTION A1: _upsert_board_membership_state takes EXACTLY these parameters — no role among '
602
+ 'them, and no overload carrying one');
603
+
604
+ select is(
605
+ (select string_agg(distinct coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'), ','
606
+ order by coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'))
607
+ from pg_proc p, aclexplode(p.proacl) a
608
+ where p.oid = (select p2.oid from pg_proc p2 join pg_namespace n2 on n2.oid = p2.pronamespace
609
+ where n2.nspname = 'agent_board' and p2.proname = '_upsert_board_membership_state')
610
+ and a.privilege_type = 'EXECUTE'),
611
+ 'postgres,service_role',
612
+ 'EXEMPTION A2: and it is not EXECUTE-able by authenticated, anon or PUBLIC');
613
+
614
+ select pg_temp.service();
615
+
616
+ select lives_ok(
617
+ $$create temp table _cl as select agent_board._upsert_board_membership_state(
618
+ '27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000003',
619
+ 'left','27610000-0000-4000-8000-000000000003','{}'::jsonb)$$,
620
+ 'EXEMPTION A3 (setup): the owner of board 2 is moved to a terminal state');
621
+
622
+ select lives_ok(
623
+ $$create temp table _cl2 as select agent_board._upsert_board_membership_state(
624
+ '27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000003',
625
+ 'joined','27610000-0000-4000-8000-000000000003','{}'::jsonb)$$,
626
+ 'EXEMPTION A3 (act): and re-entered as active');
627
+
628
+ select is(
629
+ pg_temp.role_on('27620000-0000-4000-8000-000000000002','27610000-0000-4000-8000-000000000003'),
630
+ 'member',
631
+ 'EXEMPTION A3: its only role write is the ROLERESET clamp to the table default — it DEMOTES '
632
+ 'an owner and can never confer authority, which is why it needs no ledger gate');
633
+
634
+
635
+ -- EXEMPTION A4 — the claim A1 could not make on its own.
636
+ --
637
+ -- A1 pins the HELPER's signature, and A3 demonstrates the ROLERESET clamp. Neither establishes
638
+ -- the broader sentence the exemption actually rests on: "no caller can hand this helper a role".
639
+ -- A1 covers the helper; A3 covers one code path. The gap is every OTHER function that reaches
640
+ -- the primitive — a caller that gained a role parameter and threaded it through would leave
641
+ -- both green while making the exemption false.
642
+ --
643
+ -- So the whole caller set is swept, in the EXACT-signature form rather than a word match (`_`
644
+ -- is a word character in POSIX regex, so `\mrole\M` does not match `p_role`, and every
645
+ -- parameter here is `p_*` — the same evasion G2 closed). A new caller, a removed caller, or an
646
+ -- existing caller gaining ANY parameter all change this string, and the failure prints the set.
647
+ --
648
+ -- Swept and clean at the time of writing: not one of the five declares a role-bearing
649
+ -- parameter. `invite_board_member` and `update_board_membership` admit at the membership floor
650
+ -- or move membership STATE; a `member` holds no authority to grant anything onward. That is why
651
+ -- they are outside the ratified criterion and are recorded as a bounded residual in the
652
+ -- migration header rather than gated here.
653
+ select is(
654
+ (select string_agg(p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')', ' | '
655
+ order by p.proname)
656
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
657
+ where n.nspname = 'agent_board'
658
+ and p.proname <> '_upsert_board_membership_state'
659
+ and agent_board_test.stripped_body(p.oid) ~ '\m_upsert_board_membership_state\s*\('),
660
+ 'invite_board_member(p_board_id uuid, p_agent_id uuid, p_created_by_agent_id uuid, p_metadata jsonb)'
661
+ || ' | join_board(p_board_id uuid, p_agent_id uuid, p_membership_state text, p_created_by_agent_id uuid, p_metadata jsonb)'
662
+ || ' | request_join_board(p_board_id uuid, p_agent_id uuid, p_metadata jsonb)'
663
+ || ' | resolve_board_request(p_board_id uuid, p_agent_id uuid, p_membership_state text, p_created_by_agent_id uuid, p_metadata jsonb)'
664
+ || ' | update_board_membership(p_board_id uuid, p_agent_id uuid, p_membership_state text, p_metadata jsonb)',
665
+ 'EXEMPTION A4: EVERY caller of _upsert_board_membership_state takes EXACTLY these parameters '
666
+ '— not one declares a role, so "no caller can hand it one" is checked rather than argued');
667
+
668
+ -- ---------------------------------------------------------------------------
669
+ -- EXEMPTION B — create_board, the ONE authenticated-callable member of the class.
670
+ -- ---------------------------------------------------------------------------
671
+ select is(
672
+ (select string_agg(pg_get_function_identity_arguments(p.oid), ' | ' order by p.oid)
673
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
674
+ where n.nspname = 'agent_board' and p.proname = 'create_board'),
675
+ 'p_created_by_agent_id uuid, p_slug text, p_name text, p_description text, p_metadata jsonb, '
676
+ || 'p_visibility text',
677
+ 'EXEMPTION B1: create_board takes EXACTLY these parameters — no role among them, and no '
678
+ 'overload carrying one, so the owner seat it writes is a literal and not a caller choice');
679
+
680
+ select ok(
681
+ agent_board_test.stripped_body('agent_board.create_board(uuid,text,text,text,jsonb,text)'::regprocedure, true)
682
+ like '%values (v_board.id, v_board.created_by_agent_id, ''joined'', ''owner'', v_board.created_by_agent_id)%',
683
+ 'EXEMPTION B2: the seat it writes is the literal owner for the board OWN created_by_agent_id '
684
+ '— the resolved actor, seeding itself on a board it just inserted');
685
+
686
+ select ok(
687
+ agent_board_test.stripped_body('agent_board.create_board(uuid,text,text,text,jsonb,text)'::regprocedure, true)
688
+ like '%board slug already in use%'
689
+ and strpos(agent_board_test.stripped_body('agent_board.create_board(uuid,text,text,text,jsonb,text)'::regprocedure, true),
690
+ 'update agent_board.boards as b')
691
+ < strpos(agent_board_test.stripped_body('agent_board.create_board(uuid,text,text,text,jsonb,text)'::regprocedure, true),
692
+ 'insert into agent_board.board_memberships'),
693
+ 'EXEMPTION B3: the slug-COLLISION branch refuses a non-owner and updates the boards row '
694
+ 'BEFORE the else-branch membership insert — the seat is on the fresh-board path only, so it '
695
+ 'cannot elevate a membership on a pre-existing board');
696
+
697
+ -- ...and the residual, asserted as BEHAVIOUR so #280 closing flips a test rather than a comment.
698
+ select pg_temp.claims('27600000-0000-0000-0000-000000000001');
699
+
700
+ select lives_ok(
701
+ $$create temp table _cb as select * from agent_board.create_board(
702
+ '27610000-0000-4000-8000-000000000001','k276-residual-board','K276 Residual Board')$$,
703
+ 'RESIDUAL (#280): a ledger-revoked agent CAN still create a board and be seeded its owner');
704
+
705
+ select is(
706
+ (select bm.role from agent_board.board_memberships bm
707
+ join agent_board.boards b on b.id = bm.board_id
708
+ where b.slug = 'k276-residual-board'
709
+ and bm.agent_id = '27610000-0000-4000-8000-000000000001'),
710
+ 'owner',
711
+ 'RESIDUAL (#280): and the owner seat really landed — this is the manufacture-new-authority '
712
+ 'gap #276 does NOT close, measured rather than described');
713
+
714
+ select throws_like(
715
+ $$select * from agent_board.grant_board_role(
716
+ (select id from agent_board.boards where slug = 'k276-residual-board'),
717
+ '27610000-0000-4000-8000-000000000002','admin')$$,
718
+ '%fleet_revoked:%',
719
+ 'but the gate still holds on the board it just created: it cannot GRANT there either, even '
720
+ 'as that board owner with no ceiling row');
721
+
722
+ -- ---------------------------------------------------------------------------
723
+ -- EXEMPTIONS C and D — the two remaining service_role-only seeders. (What used to be
724
+ -- exemption C, _redeem_unbounded_invite_for_agent, is now GATED — see section 10.)
725
+ -- ---------------------------------------------------------------------------
726
+ select is(
727
+ (select string_agg(p.proname || '=' ||
728
+ has_function_privilege('authenticated', p.oid, 'EXECUTE')::text, ',' order by p.proname)
729
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
730
+ where n.nspname = 'agent_board'
731
+ and p.proname in ('_fedsession_autojoin_default_board','bootstrap_board_owner_memberships')),
732
+ '_fedsession_autojoin_default_board=false,bootstrap_board_owner_memberships=false',
733
+ 'EXEMPTIONS C/D: neither remaining seeder is EXECUTE-able by authenticated');
734
+
735
+ select ok(
736
+ agent_board_test.stripped_body('agent_board._fedsession_autojoin_default_board(agent_board.identity_providers,uuid)'::regprocedure, true)
737
+ like '%p_provider.default_board_id, p_agent_id, ''member'', ''joined''%',
738
+ 'EXEMPTION C: the fedsession autojoin seeds the literal member — the BASE role, which confers '
739
+ 'no authority at all');
740
+
741
+ select is(
742
+ (select string_agg(pg_get_function_identity_arguments(p.oid), ' | ' order by p.oid)
743
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
744
+ where n.nspname = 'agent_board' and p.proname = 'bootstrap_board_owner_memberships'),
745
+ '',
746
+ 'EXEMPTION D: bootstrap_board_owner_memberships takes NO arguments — it backfills the board '
747
+ 'own recorded owner_agent_id and can name no other principal');
748
+
749
+
750
+ -- ===========================================================================
751
+ -- 10. G3 — THE INVITE ROUTE. An admin-preset link is a deferred grant of admin, so a
752
+ -- ledger-revoked agent must not be able to mint one, and an outstanding one must stop
753
+ -- conferring. Both halves are driven, and the discriminating pair against the adjacent
754
+ -- R-LINK-2 check is asserted explicitly: the creator here is ACTIVE (so R-LINK-2 passes)
755
+ -- and only the ledger row refuses.
756
+ --
757
+ -- Agent 3 is the NON-FLEET owner of board 2 and is used for the allow side; agent 1 is the
758
+ -- ledger-revoked owner of board 1. Agent 5 redeems.
759
+ -- ===========================================================================
760
+
761
+ -- ALLOW SIDE FIRST: an unrevoked owner still mints and the link still redeems, so the gate
762
+ -- cannot be passing below merely because invites are broken.
763
+ select pg_temp.claims('27600000-0000-0000-0000-000000000006');
764
+
765
+ select lives_ok(
766
+ $$create temp table _mk as select * from agent_board.create_invite_link(
767
+ '27620000-0000-4000-8000-000000000004','admin','joined',5,
768
+ now() + interval '2 days')$$,
769
+ 'ALLOW: a NON-REVOKED owner still mints an admin invite link');
770
+
771
+ select is(
772
+ (select preset_role from _mk),
773
+ 'admin',
774
+ 'and the minted link really carries the admin preset');
775
+
776
+ select pg_temp.claims('27600000-0000-0000-0000-000000000005');
777
+
778
+ select lives_ok(
779
+ $$create temp table _rd as select * from agent_board.redeem_invite_link((select code from _mk))$$,
780
+ 'ALLOW: and it redeems, conferring admin on the redeemer');
781
+
782
+ select is(
783
+ pg_temp.role_on('27620000-0000-4000-8000-000000000004','27610000-0000-4000-8000-000000000005'),
784
+ 'admin',
785
+ 'and the admin seat really landed — this is the capability #276 must not regress');
786
+
787
+ -- MINT, REFUSED. Agent 1 is ledger-revoked (section 4) and still an ACTIVE owner of board 1.
788
+ select pg_temp.claims('27600000-0000-0000-0000-000000000001');
789
+
790
+ select throws_like(
791
+ $$select * from agent_board.create_invite_link(
792
+ '27620000-0000-4000-8000-000000000001','admin','joined',5, now() + interval '2 days')$$,
793
+ '%fleet_revoked:%create_invite_link%',
794
+ 'G3 MINT: a ledger-revoked owner cannot mint an admin invite link');
795
+
796
+ select is(
797
+ (select count(*)::int from agent_board.board_invite_tokens
798
+ where board_id = '27620000-0000-4000-8000-000000000001'),
799
+ 0,
800
+ 'and the refused mint left NO token behind — the raise discarded it, so no code escaped');
801
+
802
+ select throws_like(
803
+ $$select * from agent_board.create_invite_link(
804
+ '27620000-0000-4000-8000-000000000001','member','joined',5, now() + interval '2 days')$$,
805
+ '%fleet_revoked:%create_invite_link%',
806
+ 'and a member-preset link is refused too: the gate is on the principal, not on the preset');
807
+
808
+ -- REDEEM, REFUSED — via a link minted BEFORE the creator was revoked, which is the case the
809
+ -- mint gate cannot reach and the reason both halves exist.
810
+ -- The link was minted in section 7 while agent 4 was still unrevoked; agent 4 has since been
811
+ -- ledger-revoked there. Assert the discriminating precondition: R-LINK-2
812
+ -- passes for it, so a refusal below can only be the #276 clause.
813
+ select is(
814
+ (select status from agent_board.agents where id = '27610000-0000-4000-8000-000000000004'),
815
+ 'active',
816
+ 'PRECONDITION: the link creator is STILL active, so R-LINK-2 passes and only the ledger '
817
+ 'clause can refuse the redemption');
818
+
819
+ select pg_temp.claims('27600000-0000-0000-0000-000000000005');
820
+
821
+ select throws_like(
822
+ $$select * from agent_board.redeem_invite_link((select code from _pre))$$,
823
+ 'invite is not usable',
824
+ 'G3 REDEEM: an outstanding link from a ledger-revoked creator stops conferring — and raises '
825
+ 'the UNIFORM message, not a fleet_revoked prefix, so a code-bearer learns nothing about the '
826
+ 'creator lifecycle');
827
+
828
+ select is(
829
+ (select count(*)::int from agent_board.board_memberships
830
+ where board_id = '27620000-0000-4000-8000-000000000003'
831
+ and agent_id = '27610000-0000-4000-8000-000000000005'),
832
+ 0,
833
+ 'and the refused redemption wrote NO membership');
834
+
835
+ select is(
836
+ (select uses_count from agent_board.board_invite_tokens where code = (select code from _pre)),
837
+ 0,
838
+ 'and consumed no use');
839
+
840
+ select is(
841
+ (select count(*)::int from agent_board.board_invite_redemptions r
842
+ join agent_board.board_invite_tokens t on t.id = r.invite_id
843
+ where t.code = (select code from _pre)),
844
+ 0,
845
+ 'and left no redemption ledger row');
846
+
847
+ -- The REDEEMER's own revocation is NOT a bar: receiving a membership is not conferring one.
848
+ select pg_temp.service();
849
+
850
+ select lives_ok(
851
+ $$create temp table _mk2 as select * from agent_board.create_invite_link(
852
+ '27620000-0000-4000-8000-000000000004','member','joined',5, now() + interval '2 days',
853
+ '27610000-0000-4000-8000-000000000006')$$,
854
+ 'a NON-revoked creator mints a second link on board 4');
855
+
856
+ select pg_temp.claims('27600000-0000-0000-0000-000000000001');
857
+
858
+ select lives_ok(
859
+ $$create temp table _rd2 as select * from agent_board.redeem_invite_link((select code from _mk2))$$,
860
+ 'a ledger-revoked REDEEMER may still redeem: receiving authority is not conferring it, and '
861
+ 'the gate is keyed on the creator');
862
+
863
+ select is(
864
+ (select membership_state from agent_board.board_memberships
865
+ where board_id = '27620000-0000-4000-8000-000000000004'
866
+ and agent_id = '27610000-0000-4000-8000-000000000001'),
867
+ 'joined',
868
+ 'and that redemption really placed the revoked redeemer');
869
+
870
+ -- The residual companion: the board a revoked agent CAN still create is not a way back in.
871
+ select throws_like(
872
+ $$select * from agent_board.create_invite_link(
873
+ (select id from agent_board.boards where slug = 'k276-residual-board'),
874
+ 'admin','joined',5, now() + interval '2 days')$$,
875
+ '%fleet_revoked:%',
876
+ 'RESIDUAL (#280) is contained: on the board it just created, a revoked agent still cannot '
877
+ 'mint a link either — so the residual confers nothing on anyone else');
878
+
879
+
880
+ -- The RESIDUAL BOUND rests on a lock that is already in this function: the token FOR UPDATE is
881
+ -- its FIRST statement, before the ledger read, so two redemptions of the SAME token cannot both
882
+ -- sit between their ledger read and their commit. Delete that ordering and the disclosure in
883
+ -- the migration header ("at most one per outstanding token") stops being true, which is why it
884
+ -- is pinned here rather than asserted in prose. Both markers guarded with `> 0` (#198).
885
+ -- Two claims, not one, and the first version of this assertion made only the second. It
886
+ -- compared the POSITION of the token table reference against the ledger read — a proxy for
887
+ -- "locks the token first" that says nothing about whether the read is a lock at all.
888
+ -- Mutation-tested: demoting `for update` to a plain read left it GREEN while destroying the
889
+ -- per-token bound it exists to defend. It now requires the token select to BE a lock, requires
890
+ -- that lock to be the function's only one (so `for update` is an unambiguous position marker),
891
+ -- and only then compares positions. Both markers guarded with `> 0` (#198).
892
+ select ok(
893
+ agent_board_test.stripped_body('agent_board._redeem_unbounded_invite_for_agent(text,uuid)'::regprocedure)
894
+ ~ 'from agent_board\.board_invite_tokens t[^;]*\mfor update\M'
895
+ and (select count(*) from regexp_matches(
896
+ agent_board_test.stripped_body('agent_board._redeem_unbounded_invite_for_agent(text,uuid)'::regprocedure),
897
+ 'for update', 'g')) = 1
898
+ and strpos(agent_board_test.stripped_body('agent_board._redeem_unbounded_invite_for_agent(text,uuid)'::regprocedure),
899
+ 'for update') > 0
900
+ and strpos(agent_board_test.stripped_body('agent_board._redeem_unbounded_invite_for_agent(text,uuid)'::regprocedure),
901
+ 'agent_board._actor_fleet_revoked(') > 0
902
+ and strpos(agent_board_test.stripped_body('agent_board._redeem_unbounded_invite_for_agent(text,uuid)'::regprocedure),
903
+ 'for update')
904
+ < strpos(agent_board_test.stripped_body('agent_board._redeem_unbounded_invite_for_agent(text,uuid)'::regprocedure),
905
+ 'agent_board._actor_fleet_revoked('),
906
+ 'the redeem path LOCKS the token (FOR UPDATE, its only row lock) before it reads the ledger — '
907
+ 'which is what bounds the accepted window to one redemption PER OUTSTANDING TOKEN rather '
908
+ 'than per creator');
909
+
910
+ -- FLEET ONBOARDING INHERITS THE REDEEM GATE, through _redeem_fleet_invite_for_agent. This is a
911
+ -- real behaviour change on a live path: an onboarding invite minted by a since-revoked owner
912
+ -- stops admitting. Asserted from the call graph rather than left for an operator to discover.
913
+ select ok(
914
+ agent_board_test.stripped_body('agent_board._redeem_fleet_invite_for_agent(text,uuid)'::regprocedure)
915
+ ~ '\m_redeem_unbounded_invite_for_agent\s*\(',
916
+ 'the FLEET redemption core delegates to the gated unbounded core, so fleet onboarding '
917
+ 'inherits the creator-revocation gate by construction');
918
+
919
+ select is(
920
+ (select string_agg(p.proname, ',' order by p.proname)
921
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
922
+ where n.nspname = 'agent_board'
923
+ and agent_board_test.stripped_body(p.oid) ~ '\m_redeem_unbounded_invite_for_agent\s*\('),
924
+ '_redeem_fleet_invite_for_agent,_redeem_invite_for_agent',
925
+ 'and those are the only two callers of it, so no redemption route bypasses the gate');
926
+
927
+ -- The gate's serialization rests on a claim about OTHER functions: every writer of an
928
+ -- agent-subject ledger row takes the agents row lock BEFORE the write, so the FOR SHARE in the
929
+ -- helper conflicts with it. Pinned from the catalog, so a future ledger writer that skips the
930
+ -- lock fails here and names itself instead of silently reopening the TOCTOU.
931
+ select is(
932
+ (select string_agg(p.proname, ',' order by p.proname)
933
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
934
+ where n.nspname = 'agent_board'
935
+ and agent_board_test.stripped_body(p.oid) ~* '(insert\s+into|update)\s+agent_board\.fleet_revocation_events\M'),
936
+ '_revoke_agent_principal,revoke_fleet_subject',
937
+ 'the writers of the fleet revocation ledger are exactly these two');
938
+
939
+ -- Both markers guarded with `> 0`: a bare `strpos(A) < strpos(B)` is `0 < N` = TRUE when the
940
+ -- LEFT marker is absent, so it passes on a body it no longer matches (#198).
941
+ select ok(
942
+ strpos(agent_board_test.stripped_body('agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb)'::regprocedure),
943
+ 'agent_board.agents a where a.id = p_subject_id for update') > 0
944
+ and strpos(agent_board_test.stripped_body('agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb)'::regprocedure),
945
+ 'insert into agent_board.fleet_revocation_events') > 0
946
+ and strpos(agent_board_test.stripped_body('agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb)'::regprocedure),
947
+ 'agent_board.agents a where a.id = p_subject_id for update')
948
+ < strpos(agent_board_test.stripped_body('agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb)'::regprocedure),
949
+ 'insert into agent_board.fleet_revocation_events'),
950
+ 'revoke_fleet_subject locks the agents row BEFORE writing the ledger — what the gate''s FOR SHARE conflicts with');
951
+
952
+ select ok(
953
+ strpos(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure),
954
+ 'agent_board.agents a where a.id = p_agent_id for update') > 0
955
+ and strpos(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure),
956
+ 'insert into agent_board.fleet_revocation_events') > 0
957
+ and strpos(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure),
958
+ 'agent_board.agents a where a.id = p_agent_id for update')
959
+ < strpos(agent_board_test.stripped_body('agent_board._revoke_agent_principal(uuid,uuid,text)'::regprocedure),
960
+ 'insert into agent_board.fleet_revocation_events'),
961
+ '_revoke_agent_principal locks the agents row BEFORE writing the ledger too');
962
+
963
+ -- The lock the gate itself takes, and its MODE. FOR KEY SHARE would not conflict with the FOR
964
+ -- NO KEY UPDATE a plain status change takes; the isolation spec is what proves the conflict,
965
+ -- this is what stops the mode being quietly weakened.
966
+ select ok(
967
+ agent_board_test.stripped_body('agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure)
968
+ ~ 'agent_board\.agents a where a\.id = p_actor_agent_id for share',
969
+ 'the gate takes FOR SHARE on the actor''s agents row (not KEY SHARE, not a bare read)');
970
+
971
+ select ok(
972
+ strpos(agent_board_test.stripped_body('agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure),
973
+ 'for share') > 0
974
+ and strpos(agent_board_test.stripped_body('agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure),
975
+ 'agent_board._actor_fleet_revoked(') > 0
976
+ and strpos(agent_board_test.stripped_body('agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure),
977
+ 'for share')
978
+ < strpos(agent_board_test.stripped_body('agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure),
979
+ 'agent_board._actor_fleet_revoked('),
980
+ 'and it takes that lock BEFORE reading the ledger, which is the whole point of taking it');
981
+
982
+ -- The split, asserted from the catalog. The predicate lives in ONE function so the locked and
983
+ -- unlocked callers cannot drift into asking two different questions, and the UNLOCKED one must
984
+ -- stay unlocked — an `agents` lock reappearing in it is the deadlock the header enumerates.
985
+ select ok(
986
+ agent_board_test.stripped_body('agent_board._actor_fleet_revoked(uuid)'::regprocedure)
987
+ ~ 'agent_board\.fleet_revocation_events'
988
+ and agent_board_test.stripped_body('agent_board._actor_fleet_revoked(uuid)'::regprocedure)
989
+ !~ '\mfor\s+(share|update|key\s+share|no\s+key\s+update)\M',
990
+ 'the unlocked predicate reads the ledger and takes NO row lock — what makes it safe to call '
991
+ 'from the invite redeem path, which is reached holding a boards lock');
992
+
993
+ select is(
994
+ (select string_agg(distinct coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'), ','
995
+ order by coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'))
996
+ from pg_proc p, aclexplode(p.proacl) a
997
+ where p.oid = 'agent_board._actor_fleet_revoked(uuid)'::regprocedure
998
+ and a.privilege_type = 'EXECUTE'),
999
+ 'postgres,service_role',
1000
+ 'and the unlocked predicate is service_role-only, like the assert it was split out of');
1001
+
1002
+ -- The gate must not have acquired a supersession clause. `effective_at` appearing in the body
1003
+ -- would mean a machine reactivation could restore a revoked agent's authority.
1004
+ select ok(
1005
+ agent_board_test.stripped_body('agent_board._assert_actor_not_fleet_revoked(uuid,text)'::regprocedure)
1006
+ !~ '\m(effective_at|reactivated_at)\M',
1007
+ 'the gate has no supersession clause, matching the send path''s agent-subject predicate');
1008
+
1009
+ select finish();
1010
+ rollback;