@consiliency/agent-board-schema 1.24.0 → 1.27.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 (33) hide show
  1. package/fixtures/already-applied.json +12 -0
  2. package/migrations/20260906000100_agent_board_enroll_resumption.sql +749 -0
  3. package/migrations/20260907000100_agent_board_signed_authority_provenance.sql +243 -0
  4. package/migrations/20260908000100_agent_board_signer_lifecycle_revalidation.sql +451 -0
  5. package/package.json +1 -1
  6. package/schema-manifest.json +30 -3
  7. package/src/tooling.test.mjs +3 -3
  8. package/tests/agent_board_atomic_fleet_onboarding.test.sql +129 -112
  9. package/tests/agent_board_claimkey.test.sql +25 -3
  10. package/tests/agent_board_consent_emission.test.sql +17 -13
  11. package/tests/agent_board_consent_gating.test.sql +55 -11
  12. package/tests/agent_board_delegation_policies.test.sql +3 -1
  13. package/tests/agent_board_enroll_resumption.test.sql +412 -0
  14. package/tests/agent_board_federated_lock_order_class.test.sql +1 -3
  15. package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -6
  16. package/tests/agent_board_fleet_machine_reactivation.test.sql +56 -5
  17. package/tests/agent_board_mint_harden.test.sql +39 -4
  18. package/tests/agent_board_outbox_wake_ordering.test.sql +27 -12
  19. package/tests/agent_board_rejectsem.test.sql +1 -1
  20. package/tests/agent_board_rolereset.test.sql +23 -2
  21. package/tests/agent_board_rowid.test.sql +24 -13
  22. package/tests/agent_board_scoped_send_regrant.test.sql +8 -4
  23. package/tests/agent_board_signed_authority_provenance.test.sql +477 -0
  24. package/tests/agent_board_signer_lifecycle.test.sql +709 -0
  25. package/tests/agent_board_source_strip_helper.test.sql +285 -0
  26. package/tests/agent_board_wake_recovery.test.sql +58 -40
  27. package/tests/catalog/p_metadata_parameters.json +693 -0
  28. package/tests/fixtures/test-prereqs.sql +313 -0
  29. package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +3 -3
  30. package/tests/isolation/expected/signed_grant_onboarding_deadlock.out +220 -0
  31. package/tests/isolation/expected/signer_lifecycle_revoke.out +307 -0
  32. package/tests/isolation/specs/signed_grant_onboarding_deadlock.spec +159 -0
  33. package/tests/isolation/specs/signer_lifecycle_revoke.spec +176 -0
@@ -0,0 +1,477 @@
1
+ -- #247 pgTAP: signed provenance for the two authority-granting writes.
2
+ --
3
+ -- The claim under test: `grant_board_role` and `set_board_authority_ceiling` can now be
4
+ -- performed through a signed-write gateway that records a `signature_evidence` row, and the
5
+ -- principal that row names is the one the write was actually AUTHORIZED as -- never a
6
+ -- caller-supplied argument.
7
+ --
8
+ -- Methodology:
9
+ -- * RPC probes are driven by the JWT claim blob only. SET LOCAL ROLE is never used: it
10
+ -- would revoke _is_service_role() and mask the gateway branch behind a permission error
11
+ -- on the function itself (same reason as agent_board_consent_emission).
12
+ -- * Allow-side probes are lives_ok(...) followed by a SEPARATE state check. A bare
13
+ -- `select ... from <raising rpc>(...)` aborts the transaction instead of failing, which
14
+ -- would silently drop every later assertion (#113).
15
+ -- * The payload deliberately names a DIFFERENT grantor than the gateway decision in the
16
+ -- anti-forge cases. If provenance were keyed off the payload rather than the gateway's
17
+ -- verified actor, those assertions flip.
18
+ -- * The classification tripwires at the end are catalog-derived (pg_proc.proargnames),
19
+ -- not a list written here: if a p_metadata parameter is ever added to one of the three
20
+ -- RPCs classified as unable to carry client-side signing evidence, the assertion fails
21
+ -- and NAMES the member, forcing reclassification rather than leaving the client-side
22
+ -- exclusion silently stale.
23
+
24
+ create extension if not exists pgtap with schema extensions;
25
+ set search_path = extensions, public, agent_board;
26
+
27
+ begin;
28
+
29
+ select plan(40);
30
+
31
+ create function pg_temp.claims(p_sub text) returns void language plpgsql as $$
32
+ begin
33
+ perform set_config('request.jwt.claims', json_build_object('role','authenticated','sub',p_sub)::text, true);
34
+ end $$;
35
+ create function pg_temp.service() returns void language plpgsql as $$
36
+ begin
37
+ perform set_config('request.jwt.claims', '', true);
38
+ end $$;
39
+
40
+ -- Fixture -------------------------------------------------------------------
41
+ -- owner = agent 1 (board owner, and the signer the gateway verifies)
42
+ -- grantee = agent 2 (an active member whose role the owner raises)
43
+ -- outsider = agent 3 (a member with NO authority; the forged grantor in the M7 probes)
44
+
45
+ insert into auth.users(id, instance_id, aud, role, email)
46
+ select ('24700000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid,
47
+ '00000000-0000-0000-0000-000000000000', 'authenticated', 'authenticated',
48
+ 'k247u'||n||'@t.test'
49
+ from generate_series(1,3) n;
50
+
51
+ insert into agent_board.agents(id, agent_type, kind, handle, display_name, status, auth_user_id)
52
+ select ('24710000-0000-4000-8000-' || lpad(n::text,12,'0'))::uuid,
53
+ 'agent','agent','k247-agent-'||n,'K247 Agent '||n,'active',
54
+ ('24700000-0000-0000-0000-' || lpad(n::text,12,'0'))::uuid
55
+ from generate_series(1,3) n;
56
+
57
+ insert into agent_board.boards(id, slug, name, created_by_agent_id, visibility, status)
58
+ values ('24720000-0000-4000-8000-000000000001','k247-board','K247 Board',
59
+ '24710000-0000-4000-8000-000000000001'::uuid, 'private','active');
60
+
61
+ insert into agent_board.board_memberships(board_id, agent_id, role, membership_state, created_by_agent_id)
62
+ values
63
+ ('24720000-0000-4000-8000-000000000001','24710000-0000-4000-8000-000000000001','owner','joined','24710000-0000-4000-8000-000000000001'),
64
+ ('24720000-0000-4000-8000-000000000001','24710000-0000-4000-8000-000000000002','member','joined','24710000-0000-4000-8000-000000000001'),
65
+ ('24720000-0000-4000-8000-000000000001','24710000-0000-4000-8000-000000000003','member','joined','24710000-0000-4000-8000-000000000001');
66
+
67
+ insert into agent_board.agent_signing_keys(
68
+ id, agent_id, key_id, key_version, public_key, public_key_format, fingerprint, status, proof_state)
69
+ values (
70
+ '24730000-0000-4000-8000-000000000001',
71
+ '24710000-0000-4000-8000-000000000001',
72
+ 'primary', 1,
73
+ public.test_pubkey('k247-primary'), 'raw_ed25519_base64url',
74
+ public.test_key_fingerprint('k247-primary'), 'active', 'proven');
75
+
76
+ -- The gateway decision the wrappers are called with. `agent_id` is the VERIFIED signer.
77
+ create function pg_temp.gw(p_actor text) returns jsonb language sql immutable as $$
78
+ select jsonb_build_object(
79
+ 'agent_id', p_actor,
80
+ 'signing_key_id', '24730000-0000-4000-8000-000000000001',
81
+ 'key_id', 'primary',
82
+ 'key_version', 1,
83
+ 'algorithm', 'Ed25519',
84
+ 'verification_status', 'verified',
85
+ 'payload_digest', 'sha256:' || repeat('a', 64),
86
+ 'signature_digest', 'sha256:' || repeat('b', 64),
87
+ 'canonicalization_version', 'agent-provenance-jcs.v1',
88
+ 'enforcement_mode', 'require_for_agent_writes',
89
+ 'redacted', true);
90
+ $$;
91
+
92
+ -- ===========================================================================
93
+ -- 1. The surface exists, and the DROP did not leave the ACL or COMMENT behind.
94
+ -- (DROP FUNCTION discards both; a recreated SECURITY DEFINER function grants
95
+ -- EXECUTE to PUBLIC by default. That hole opened for real in this repo once.)
96
+ -- ===========================================================================
97
+
98
+ select has_function('agent_board', 'signed_grant_board_role', array['jsonb','jsonb'],
99
+ 'signed_grant_board_role wrapper exists');
100
+ select has_function('agent_board', 'signed_set_board_authority_ceiling', array['jsonb','jsonb'],
101
+ 'signed_set_board_authority_ceiling wrapper exists');
102
+ select has_function('agent_board', 'set_board_authority_ceiling', array['uuid','text','uuid'],
103
+ 'the ceiling setter carries the delegable actor parameter');
104
+
105
+ select is(
106
+ (select string_agg(distinct coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'), ','
107
+ order by coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'))
108
+ from pg_proc p, aclexplode(p.proacl) a
109
+ where p.oid = 'agent_board.signed_grant_board_role(jsonb,jsonb)'::regprocedure
110
+ and a.privilege_type = 'EXECUTE'),
111
+ 'postgres,service_role',
112
+ 'signed_grant_board_role executes for service_role only (never PUBLIC/anon/authenticated)');
113
+
114
+ select is(
115
+ (select string_agg(distinct coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'), ','
116
+ order by coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'))
117
+ from pg_proc p, aclexplode(p.proacl) a
118
+ where p.oid = 'agent_board.signed_set_board_authority_ceiling(jsonb,jsonb)'::regprocedure
119
+ and a.privilege_type = 'EXECUTE'),
120
+ 'postgres,service_role',
121
+ 'signed_set_board_authority_ceiling executes for service_role only');
122
+
123
+ select is(
124
+ (select string_agg(distinct coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'), ','
125
+ order by coalesce(pg_get_userbyid(nullif(a.grantee,0)),'PUBLIC'))
126
+ from pg_proc p, aclexplode(p.proacl) a
127
+ where p.oid = 'agent_board.set_board_authority_ceiling(uuid,text,uuid)'::regprocedure
128
+ and a.privilege_type = 'EXECUTE'),
129
+ 'authenticated,postgres,service_role',
130
+ 'the recreated ceiling setter restated its grants and did NOT keep PUBLIC EXECUTE');
131
+
132
+ select isnt(
133
+ (select obj_description('agent_board.set_board_authority_ceiling(uuid,text,uuid)'::regprocedure, 'pg_proc')),
134
+ null,
135
+ 'the recreated ceiling setter restated its COMMENT (DROP FUNCTION discards it)');
136
+
137
+ select ok(
138
+ strpos(lower(agent_board_test.stripped_body('agent_board.set_board_authority_ceiling(uuid,text,uuid)'::regprocedure)),
139
+ 'perform agent_board._lock_board_authority_ceiling(p_board_id)') > 0
140
+ and strpos(lower(agent_board_test.stripped_body('agent_board.set_board_authority_ceiling(uuid,text,uuid)'::regprocedure)),
141
+ 'perform agent_board._lock_board_authority_ceiling(p_board_id)')
142
+ < strpos(lower(agent_board_test.stripped_body('agent_board.set_board_authority_ceiling(uuid,text,uuid)'::regprocedure)),
143
+ 'insert into agent_board.board_authority_ceilings'),
144
+ 'the recreated setter still takes the advisory lock before its gate and upsert');
145
+
146
+ -- ===========================================================================
147
+ -- 2. D4 -- the new parameter changes NO existing unsigned call.
148
+ -- ===========================================================================
149
+
150
+ select pg_temp.claims('24700000-0000-0000-0000-000000000001');
151
+
152
+ select lives_ok(
153
+ $$select * from agent_board.set_board_authority_ceiling(
154
+ '24720000-0000-4000-8000-000000000001'::uuid, 'admin')$$,
155
+ 'D4: the pre-#247 two-argument call by an authenticated owner still succeeds');
156
+
157
+ select is(
158
+ (select max_grantable_role from agent_board.board_authority_ceilings
159
+ where board_id = '24720000-0000-4000-8000-000000000001'),
160
+ 'admin',
161
+ 'D4: and it wrote the ceiling it was asked for');
162
+
163
+ select pg_temp.claims('24700000-0000-0000-0000-000000000003');
164
+
165
+ select throws_like(
166
+ $$select * from agent_board.set_board_authority_ceiling(
167
+ '24720000-0000-4000-8000-000000000001'::uuid, 'owner')$$,
168
+ '%only a board owner%',
169
+ 'D4: a non-owner is still refused by the owner gate, with the same message');
170
+
171
+ -- The new parameter cannot be used to act as someone else while authenticated.
172
+ select throws_like(
173
+ $$select * from agent_board.set_board_authority_ceiling(
174
+ '24720000-0000-4000-8000-000000000001'::uuid, 'owner',
175
+ '24710000-0000-4000-8000-000000000001'::uuid)$$,
176
+ '%must match current_agent_id()%',
177
+ 'an authenticated caller cannot borrow another agent as p_actor_agent_id');
178
+
179
+ -- ===========================================================================
180
+ -- 3. signed_grant_board_role -- the write happens AND leaves evidence.
181
+ -- ===========================================================================
182
+
183
+ select pg_temp.service();
184
+
185
+ select lives_ok(
186
+ $$select * from agent_board.signed_grant_board_role(
187
+ jsonb_build_object(
188
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
189
+ 'p_agent_id', '24710000-0000-4000-8000-000000000002',
190
+ 'p_grantee_role', 'admin'),
191
+ pg_temp.gw('24710000-0000-4000-8000-000000000001'))$$,
192
+ 'a signed grant by the verified board owner succeeds');
193
+
194
+ select is(
195
+ (select role from agent_board.board_memberships
196
+ where board_id = '24720000-0000-4000-8000-000000000001'
197
+ and agent_id = '24710000-0000-4000-8000-000000000002'),
198
+ 'admin',
199
+ 'the signed grant actually raised the grantee role');
200
+
201
+ select is(
202
+ (select count(*)::int from agent_board.signature_evidence
203
+ where operation = 'grant_board_role'),
204
+ 1,
205
+ 'the signed grant recorded exactly one signature_evidence row');
206
+
207
+ select is(
208
+ (select agent_id from agent_board.signature_evidence where operation = 'grant_board_role'),
209
+ '24710000-0000-4000-8000-000000000001'::uuid,
210
+ 'M7: the evidence row names the GATEWAY-VERIFIED grantor');
211
+
212
+ select is(
213
+ (select verification_status from agent_board.signature_evidence where operation = 'grant_board_role'),
214
+ 'verified',
215
+ 'the evidence row carries the gateway verification status');
216
+
217
+ select is(
218
+ (select external_evidence_ref from agent_board.signature_evidence where operation = 'grant_board_role'),
219
+ 'board-authority:grant_board_role:24720000-0000-4000-8000-000000000001:24710000-0000-4000-8000-000000000002',
220
+ 'the evidence ref is derived server-side from the board and grantee');
221
+
222
+ -- The source-shape CHECK is asserted, not inferred from the insert having succeeded.
223
+ select ok(
224
+ (select source_kind = 'external'
225
+ and source_message_id is null
226
+ and source_task_event_id is null
227
+ and source_artifact_id is null
228
+ and source_delivery_recipient_id is null
229
+ and external_evidence_ref is not null
230
+ from agent_board.signature_evidence where operation = 'grant_board_role'),
231
+ 'the authority evidence row satisfies the external source shape the CHECK requires');
232
+
233
+ -- ===========================================================================
234
+ -- 4. M7 -- a caller-supplied grantor can never become the recorded principal.
235
+ -- ===========================================================================
236
+
237
+ select throws_like(
238
+ $$select * from agent_board.signed_grant_board_role(
239
+ jsonb_build_object(
240
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
241
+ 'p_agent_id', '24710000-0000-4000-8000-000000000002',
242
+ 'p_grantee_role', 'owner',
243
+ 'p_grantor_agent_id', '24710000-0000-4000-8000-000000000003'),
244
+ pg_temp.gw('24710000-0000-4000-8000-000000000001'))$$,
245
+ '%actor_mismatch%',
246
+ 'M7: a payload grantor that differs from the verified signer is refused outright');
247
+
248
+ select is(
249
+ (select count(*)::int from agent_board.signature_evidence where operation = 'grant_board_role'),
250
+ 1,
251
+ 'M7: and the refused forge attempt recorded no second evidence row');
252
+
253
+ select is(
254
+ (select role from agent_board.board_memberships
255
+ where board_id = '24720000-0000-4000-8000-000000000001'
256
+ and agent_id = '24710000-0000-4000-8000-000000000002'),
257
+ 'admin',
258
+ 'M7: and the refused forge attempt did not raise the grantee to owner');
259
+
260
+ -- The DIVERGENCE case, which is the one M7 is really about. Above, the forged grantor had
261
+ -- no authority, so a mutation that trusted the payload would still be stopped by the
262
+ -- ceiling gate -- the damage would be caught downstream, not by provenance. Here the forged
263
+ -- grantor is the OWNER and the verified signer is a mere member: a wrapper that took the
264
+ -- grantor from the payload would perform a grant AUTHORIZED AS THE OWNER while
265
+ -- _record_signed_write_evidence (which reads only the gateway actor) recorded it against
266
+ -- the member. That row would be evidence naming a principal who did not authorize the
267
+ -- write, which is worse than no row at all.
268
+ select throws_like(
269
+ $$select * from agent_board.signed_grant_board_role(
270
+ jsonb_build_object(
271
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
272
+ 'p_agent_id', '24710000-0000-4000-8000-000000000002',
273
+ 'p_grantee_role', 'admin',
274
+ 'p_grantor_agent_id', '24710000-0000-4000-8000-000000000001'),
275
+ pg_temp.gw('24710000-0000-4000-8000-000000000003'))$$,
276
+ '%actor_mismatch%',
277
+ 'M7: a payload grantor with MORE authority than the verified signer is refused');
278
+
279
+ select is(
280
+ (select count(*)::int from agent_board.signature_evidence where operation = 'grant_board_role'),
281
+ 1,
282
+ 'M7: the divergence attempt recorded no evidence row naming the wrong principal');
283
+
284
+ -- The wrapper adds no authority: an unprivileged verified signer is still refused,
285
+ -- by the SAME gates the unsigned function applies.
286
+ select throws_like(
287
+ $$select * from agent_board.signed_grant_board_role(
288
+ jsonb_build_object(
289
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
290
+ 'p_agent_id', '24710000-0000-4000-8000-000000000002',
291
+ 'p_grantee_role', 'owner'),
292
+ pg_temp.gw('24710000-0000-4000-8000-000000000003'))$$,
293
+ '%exceeds grantor role or board authority ceiling%',
294
+ 'the wrapper grants no authority: a member signer still cannot mint an owner');
295
+
296
+ -- This probe named a 'failed' verification but carried NO agent_id, so it was refused for
297
+ -- ACTOR ABSENCE and never once exercised the failed status -- a description claiming what no
298
+ -- assertion backed. It is now two probes, each refused for the reason it names.
299
+ select throws_like(
300
+ $$select * from agent_board.signed_grant_board_role(
301
+ jsonb_build_object(
302
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
303
+ 'p_agent_id', '24710000-0000-4000-8000-000000000002',
304
+ 'p_grantee_role', 'admin'),
305
+ jsonb_build_object('verification_status','failed'))$$,
306
+ '%actor_mismatch%',
307
+ 'a gateway decision naming no actor cannot write evidence at all');
308
+
309
+ -- The status probe proper: the SAME decision the allow-side probes use -- verified signer,
310
+ -- live key, an owner granting a role the inner gates permit -- with only verification_status
311
+ -- flipped to 'failed'. Without the #269 verification check this grant COMPLETES and mints an
312
+ -- evidence row recording 'failed' beside a mutation that succeeded.
313
+ select throws_like(
314
+ $$select * from agent_board.signed_grant_board_role(
315
+ jsonb_build_object(
316
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
317
+ 'p_agent_id', '24710000-0000-4000-8000-000000000002',
318
+ 'p_grantee_role', 'admin'),
319
+ pg_temp.gw('24710000-0000-4000-8000-000000000001')
320
+ || jsonb_build_object('verification_status','failed'))$$,
321
+ '%signer_not_verified%',
322
+ 'a FAILED verification is refused for its STATUS, by a signer who is otherwise fully entitled');
323
+
324
+ select is(
325
+ (select count(*)::int from agent_board.signature_evidence where operation = 'grant_board_role'),
326
+ 1,
327
+ 'and the failed-verification attempt minted no evidence row');
328
+
329
+ -- ===========================================================================
330
+ -- 5. signed_set_board_authority_ceiling.
331
+ -- ===========================================================================
332
+
333
+ select lives_ok(
334
+ $$select * from agent_board.signed_set_board_authority_ceiling(
335
+ jsonb_build_object(
336
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
337
+ 'p_max_grantable_role', 'member'),
338
+ pg_temp.gw('24710000-0000-4000-8000-000000000001'))$$,
339
+ 'a signed ceiling change by the verified board owner succeeds');
340
+
341
+ select is(
342
+ (select max_grantable_role from agent_board.board_authority_ceilings
343
+ where board_id = '24720000-0000-4000-8000-000000000001'),
344
+ 'member',
345
+ 'the signed ceiling change actually wrote the ceiling');
346
+
347
+ select is(
348
+ (select agent_id from agent_board.signature_evidence
349
+ where operation = 'set_board_authority_ceiling'),
350
+ '24710000-0000-4000-8000-000000000001'::uuid,
351
+ 'M7: the ceiling evidence row names the gateway-verified actor');
352
+
353
+ select throws_like(
354
+ $$select * from agent_board.signed_set_board_authority_ceiling(
355
+ jsonb_build_object(
356
+ 'p_board_id', '24720000-0000-4000-8000-000000000001',
357
+ 'p_max_grantable_role', 'owner'),
358
+ pg_temp.gw('24710000-0000-4000-8000-000000000003'))$$,
359
+ '%only a board owner%',
360
+ 'the ceiling wrapper grants no authority: a non-owner signer is still refused');
361
+
362
+ select is(
363
+ (select count(*)::int from agent_board.signature_evidence
364
+ where operation = 'set_board_authority_ceiling'),
365
+ 1,
366
+ 'and the refused ceiling change recorded no evidence row');
367
+
368
+ -- ===========================================================================
369
+ -- 6. Classification tripwires (catalog-derived, CLAUDE.md rule 1).
370
+ --
371
+ -- The client classifies these three as unable to carry client-side signing
372
+ -- evidence BECAUSE they declare no p_metadata parameter -- the client's
373
+ -- attachSigningMetadata merges into p_metadata and nowhere else. That reason is
374
+ -- asserted here positively and from pg_catalog. If a p_metadata parameter is ever
375
+ -- added to one of them, this fails and names it, forcing the client-side
376
+ -- classification to be revisited instead of going quietly stale.
377
+ --
378
+ -- WHERE THE CONVERSE CLASS IS ASSERTED -- not here, and pointing at it rather than
379
+ -- leaving the gap this note used to record. "Every RPC the client signs into p_metadata
380
+ -- declares that parameter" was FALSE for nine of the thirty-nine (append_task_event,
381
+ -- delete_callback_endpoint, mark_read, raise_conflict, raise_escalation, release_claim,
382
+ -- renew_claim, resolve_conflict, resolve_escalation -- all inherited from the pre-#247
383
+ -- hand-written signed set), which is why it could not be written green when this file
384
+ -- was authored. message-board#261 reclassified all nine and pinned the class:
385
+ -- scripts/build-p-metadata-snapshot.mjs derives EVERY agent_board function from
386
+ -- pg_catalog with no name filter, each with a declares_p_metadata boolean, into
387
+ -- tests/catalog/p_metadata_parameters.json; `--check`s it against a live database in the
388
+ -- same CI job that runs this suite; and the client's projection test asserts that every
389
+ -- OVERLOAD of every signed RPC declares the parameter, naming any signature that does
390
+ -- not. The class is pinned on the CLIENT side because that is where the signed set
391
+ -- lives; this section stays the catalog-side tripwire for the three RPCs #247
392
+ -- adjudicated.
393
+ -- ===========================================================================
394
+
395
+ select is(
396
+ (select string_agg(p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')', '; '
397
+ order by p.proname)
398
+ from pg_proc p
399
+ join pg_namespace n on n.oid = p.pronamespace
400
+ where n.nspname = 'agent_board'
401
+ and p.proname in ('grant_board_role','set_board_authority_ceiling','set_board_role_gate')
402
+ and not ('p_metadata' = any(coalesce(p.proargnames, array[]::text[])))),
403
+ 'grant_board_role(p_board_id uuid, p_agent_id uuid, p_grantee_role text, p_grantor_agent_id uuid); '
404
+ || 'set_board_authority_ceiling(p_board_id uuid, p_max_grantable_role text, p_actor_agent_id uuid); '
405
+ || 'set_board_role_gate(p_board_id uuid, p_enforced boolean)',
406
+ 'the three authority RPCs that cannot carry p_metadata.signed_write are exactly these, with these signatures');
407
+
408
+ select is(
409
+ (select count(*)::int
410
+ from pg_proc p
411
+ join pg_namespace n on n.oid = p.pronamespace
412
+ where n.nspname = 'agent_board'
413
+ and p.proname in ('grant_board_role','set_board_authority_ceiling','set_board_role_gate')
414
+ and 'p_metadata' = any(coalesce(p.proargnames, array[]::text[]))),
415
+ 0,
416
+ 'none of the three has acquired a p_metadata parameter');
417
+
418
+ select is(
419
+ (select string_agg(distinct p.proname, ',' order by p.proname)
420
+ from pg_proc p
421
+ join pg_namespace n on n.oid = p.pronamespace
422
+ where n.nspname = 'agent_board'
423
+ and p.proname in ('create_invite_link','create_fleet_onboarding_invite','create_or_find_thread')
424
+ and 'p_metadata' = any(coalesce(p.proargnames, array[]::text[]))),
425
+ 'create_fleet_onboarding_invite,create_invite_link,create_or_find_thread',
426
+ -- NOT "the RPCs the client signs": create_or_find_thread is in this set and the client
427
+ -- classifies it unsigned-deliberate, and the client signs 39 RPCs, not three. The set is
428
+ -- exactly the three whose p_metadata premise #247 adjudicated -- two it now signs, one it
429
+ -- deliberately does not -- so the label states the set it actually queries. The wider
430
+ -- claim this once implied ("every signed RPC declares p_metadata") is NOT asserted here
431
+ -- and must not be read out of it; see the note above section 6.
432
+ 'the three RPCs #247 adjudicated that declare p_metadata are exactly these');
433
+
434
+ -- create_or_find_thread is classified unsigned-deliberate on the strength of being
435
+ -- idempotent -- a key-collision returns the EXISTING thread rather than writing a new
436
+ -- one. Asserted positively rather than left as an unexplained exclusion.
437
+ --
438
+ -- Driven through lives_ok + a captured result, NOT a bare `is((select ... from rpc(...)))`.
439
+ -- create_or_find_thread is an ADMISSION rpc: the bare form ABORTS the transaction on a
440
+ -- raise instead of failing it, and pgTAP then emits no `not ok` at all while the remaining
441
+ -- assertions silently never run (#113, CLAUDE.md rule 3). The runner's lint did not flag
442
+ -- the bare form here only because its MUT_RPCS list did not name this function; that list
443
+ -- is corrected in the same change, so the next admission rpc used this way is caught by the
444
+ -- lint rather than by a reviewer.
445
+ create temporary table k247_idem(
446
+ seq int generated always as identity,
447
+ thread_id uuid,
448
+ reused boolean
449
+ ) on commit drop;
450
+
451
+ select lives_ok($$
452
+ insert into k247_idem(thread_id, reused)
453
+ select thread_id, reused
454
+ from agent_board.create_or_find_thread(
455
+ '24710000-0000-4000-8000-000000000001'::uuid, 'K247 idem', 'k247-idem-key')
456
+ $$, 'create_or_find_thread admits the first call for the key');
457
+
458
+ select lives_ok($$
459
+ insert into k247_idem(thread_id, reused)
460
+ select thread_id, reused
461
+ from agent_board.create_or_find_thread(
462
+ '24710000-0000-4000-8000-000000000001'::uuid, 'K247 idem', 'k247-idem-key')
463
+ $$, 'and admits an identical second call rather than raising');
464
+
465
+ select is(
466
+ (select count(distinct thread_id)::int from k247_idem),
467
+ 1,
468
+ 'create_or_find_thread is idempotent on its key -- the stated reason it is deliberately unsigned');
469
+
470
+ select is(
471
+ (select reused from k247_idem order by seq desc limit 1),
472
+ true,
473
+ 'and it reports the reuse rather than performing a second authority-free write');
474
+
475
+ select finish();
476
+
477
+ rollback;