@consiliency/agent-board-schema 1.28.0 → 1.38.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 (43) hide show
  1. package/fixtures/already-applied.json +40 -0
  2. package/migrations/20260910000100_agent_board_ceiling_lock_comment_truth.sql +89 -0
  3. package/migrations/20260911000100_agent_board_ledger_revoked_board_creation.sql +330 -0
  4. package/migrations/20260912000100_agent_board_receipt_subject_gate.sql +348 -0
  5. package/migrations/20260913000100_agent_board_task_thread_lock_order.sql +292 -0
  6. package/migrations/20260914000100_agent_board_claim_subject_gate.sql +500 -0
  7. package/migrations/20260915000100_agent_board_fedsession_stored_identity_gate.sql +353 -0
  8. package/migrations/20260916000100_agent_board_federated_identity_injective_slug.sql +1646 -0
  9. package/migrations/20260917000100_agent_board_invite_no_downgrade.sql +398 -0
  10. package/migrations/20260918000100_agent_board_signed_write_enforced_repoint.sql +139 -0
  11. package/migrations/20260919000100_agent_board_signed_write_model_key.sql +107 -0
  12. package/package.json +1 -1
  13. package/schema-manifest.json +93 -3
  14. package/src/deployment-drift-script.test.mjs +424 -0
  15. package/src/deployment-drift.test.mjs +301 -0
  16. package/src/tooling.test.mjs +83 -2
  17. package/tests/agent_board_atomic_fleet_onboarding.test.sql +144 -29
  18. package/tests/agent_board_bundle_endpoint_compat.test.sql +73 -18
  19. package/tests/agent_board_claim_subject_gate.test.sql +1025 -0
  20. package/tests/agent_board_consent_substrate.test.sql +8 -1
  21. package/tests/agent_board_enroll_resumption.test.sql +7 -1
  22. package/tests/agent_board_federated_identity_injective_slug.test.sql +388 -0
  23. package/tests/agent_board_fedsession.test.sql +17 -2
  24. package/tests/agent_board_fedsession_stored_identity_gate.test.sql +606 -0
  25. package/tests/agent_board_fk_lock_edges.test.sql +422 -0
  26. package/tests/agent_board_invite_no_downgrade.test.sql +689 -0
  27. package/tests/agent_board_ledger_authority.test.sql +142 -71
  28. package/tests/agent_board_receipt_subject_gate.test.sql +624 -0
  29. package/tests/agent_board_signed_write_enforced.test.sql +366 -0
  30. package/tests/agent_board_signer_lifecycle.test.sql +23 -6
  31. package/tests/agent_board_source_strip_helper.test.sql +256 -1
  32. package/tests/agent_board_task_thread_lock_order.test.sql +337 -0
  33. package/tests/agent_board_write_enforcement_class.test.sql +1016 -0
  34. package/tests/catalog/p_metadata_parameters.json +12 -0
  35. package/tests/fixtures/isolation-substrate.sql +1 -1
  36. package/tests/fixtures/test-prereqs.sql +442 -11
  37. package/tests/isolation/expected/ledger_authority_revoke.out +219 -1
  38. package/tests/isolation/expected/task_thread_lock_order.out +203 -0
  39. package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +5 -4
  40. package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +4 -4
  41. package/tests/isolation/specs/ledger_authority_revoke.spec +115 -0
  42. package/tests/isolation/specs/task_thread_lock_order.spec +197 -0
  43. package/tests/seeds/20260916000100_agent_board_federated_identity_injective_slug.sql.pre.sql +196 -0
@@ -0,0 +1,366 @@
1
+ -- #310 — `signed_write_enforced` must describe the model this project actually ships.
2
+ --
3
+ -- The flag reported `false` on a system that was enforcing, because its predicate still encoded
4
+ -- the abandoned "authenticated cannot execute the send RPCs" model. `20260918000100` re-points it
5
+ -- at what enforcement means now. This file pins BOTH halves of that meaning, and the split is
6
+ -- deliberate rather than convenient:
7
+ --
8
+ -- * the GRANT half is in the flag, because privileges are catalog facts a shipped function can
9
+ -- read soundly at runtime;
10
+ -- * the BODY half is here, because deciding "this writer derives its actor from the session and
11
+ -- gates" means reading function source, and a shipped runtime scan of `prosrc` is satisfied by
12
+ -- a COMMENT (#196/#199). The strip helpers that make such a scan sound are test-only, and
13
+ -- shipping a second copy into `agent_board` is the two-copies-drift failure #199 names.
14
+ --
15
+ -- CONSEQUENCE, with the qualifier that makes it true. A new ungated writer THAT IS THE
16
+ -- COUNTERPART OF A SIGNED GATEWAY turns an assertion in this file red rather than the flag. A new
17
+ -- ungated writer with NO signed twin turns NEITHER red.
18
+ --
19
+ -- That is by construction, not by accident: every class-scoping predicate below bounds the class
20
+ -- with `signed\_%` and derives the counterpart by stripping the prefix, so a writer without a
21
+ -- signed twin is outside the class before any assertion runs. Measured, because a claim about
22
+ -- one's own blind spot deserves evidence too: an `authenticated`-executable SECURITY DEFINER
23
+ -- writer that derives nothing from the session left the flag `true` and all 19 assertions green.
24
+ --
25
+ -- The repo has many such writers already -- `create_board`, `join_board`, `invite_board_room`-
26
+ -- style admission RPCs, `acquire_claim`, `delegate_task` -- none of which has a signed gateway.
27
+ -- This file makes NO claim about them. Closing that properly needs an EFFECT-derived class
28
+ -- ("every authenticated-executable function that writes a message/task/authority row") on the
29
+ -- shape `agent_board_consent_gating.test.sql` EC-4 already uses, which is a bigger class than
30
+ -- #310's subject; filed as a follow-up rather than smuggled in here.
31
+ --
32
+ -- An earlier draft of this note said "a new UNGATED writer" flatly. A review seat read the
33
+ -- scoping predicates and showed the claim was broader than the class. Recording that here rather
34
+ -- than only fixing the wording, because the unqualified sentence is the kind that gets copied.
35
+ --
36
+ -- THE CALL CLOSURE IS LOAD-BEARING, not a flourish. Only 3 of the 7 unsigned counterparts name
37
+ -- `_resolved_actor_agent_id` in their own body; the send family DELEGATES to `post_message`. A
38
+ -- direct body scan would report four of seven ungated and be WRONG.
39
+ --
40
+ -- WHAT MAKES A CLOSURE ASSERTION FAIL-CLOSED is where its POPULATION comes from, not which of the
41
+ -- two spellings it uses -- an earlier version of this note said the spelling was the thing, and
42
+ -- the rewrite below falsifies that. "The set that does NOT reach X is empty" is fail-OPEN when the
43
+ -- population is drawn from the closure itself, because an empty closure empties the population
44
+ -- too. It is fail-CLOSED when the population comes from `pg_proc`, because then an empty closure
45
+ -- puts EVERY member in the non-reaching set. The assertions below take the population from
46
+ -- pg_proc and pin its size with a canary, so both directions are covered. Proven by shimming the
47
+ -- helper to return zero rows, not assumed.
48
+
49
+ begin;
50
+ select plan(22);
51
+
52
+ -- ---------------------------------------------------------------------------
53
+ -- THE DEFECT: the flag now describes a system that is enforcing.
54
+ -- ---------------------------------------------------------------------------
55
+ select is(
56
+ (agent_board.get_bundle_compatibility()->>'signed_write_enforced'),
57
+ 'true',
58
+ '#310: signed_write_enforced is TRUE on a system that is enforcing — the whole point of the issue');
59
+
60
+ select is(
61
+ (select count(*)::text from pg_proc p join pg_namespace n on n.oid = p.pronamespace
62
+ where n.nspname = 'agent_board' and p.proname = 'signed_send_direct_message'),
63
+ '1',
64
+ '#310 anchor: the signed gateway exists — without it the grant clause below is vacuously true, so this is what stops "enforced" meaning "nothing to enforce"');
65
+
66
+ -- ---------------------------------------------------------------------------
67
+ -- THE GRANT HALF. The old predicate carried a FOUR-name hand list; there are SEVEN
68
+ -- gateways, which is rule 1's failure in its plainest form. This set comes from pg_proc.
69
+ -- ---------------------------------------------------------------------------
70
+ select is(
71
+ (select coalesce(array_agg(distinct p.proname::text order by p.proname::text), array[]::text[])
72
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
73
+ where n.nspname = 'agent_board' and p.proname like 'signed\_%'),
74
+ array['signed_append_task_event', 'signed_create_tasking_delivery', 'signed_grant_board_role',
75
+ 'signed_post_board_message', 'signed_send_direct_message', 'signed_send_multicast_message',
76
+ 'signed_set_board_authority_ceiling']::text[],
77
+ '#310: there are SEVEN signed gateways — the retired predicate named four, and a new gateway fails here NAMING itself');
78
+
79
+ select is(
80
+ (select coalesce(array_agg(r.rolname || ':' || p.proname order by r.rolname, p.proname), array[]::text[])
81
+ from pg_proc p
82
+ join pg_namespace n on n.oid = p.pronamespace
83
+ cross join (values ('authenticated'), ('anon')) as r(rolname)
84
+ where n.nspname = 'agent_board' and p.proname like 'signed\_%'
85
+ and has_function_privilege(r.rolname, p.oid, 'EXECUTE')),
86
+ array[]::text[],
87
+ '#310: no signed_* gateway is EXECUTE-able by authenticated or anon — via has_function_privilege, which resolves inherited and PUBLIC-default grants that role_routine_grants cannot see');
88
+
89
+ -- The positive complement: the operator path must EXIST, or "nobody can call it" would satisfy
90
+ -- the assertion above while meaning the gateway is dead rather than protected.
91
+ select is(
92
+ (select coalesce(array_agg(distinct p.proname::text order by p.proname::text), array[]::text[])
93
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
94
+ where n.nspname = 'agent_board' and p.proname like 'signed\_%'
95
+ and has_function_privilege('service_role', p.oid, 'EXECUTE')),
96
+ array['signed_append_task_event', 'signed_create_tasking_delivery', 'signed_grant_board_role',
97
+ 'signed_post_board_message', 'signed_send_direct_message', 'signed_send_multicast_message',
98
+ 'signed_set_board_authority_ceiling']::text[],
99
+ '#310: every signed_* IS executable by service_role — the operator path is protected, not dead');
100
+
101
+ -- ---------------------------------------------------------------------------
102
+ -- THE PAIRING, by NAME rather than by a list: each `signed_X` has an unsigned `X`. This is what
103
+ -- makes "the enforcement class" catalog-derived — the class is not "the send RPCs someone
104
+ -- remembered", it is the unsigned counterpart of every gateway that exists.
105
+ -- ---------------------------------------------------------------------------
106
+ -- Spelled as the MISSING set, not the present set. The present-set form was filtered by
107
+ -- `where exists (counterpart)`, so a gateway with NO counterpart was filtered OUT of its own
108
+ -- assertion and the aggregate still matched -- the description claimed "a gateway without one
109
+ -- fails here" and the author's own mutation table refuted it: an orphan gateway reddened
110
+ -- other assertions and never this one. What that form actually detected was counterpart
111
+ -- REMOVAL. This form detects both, and names the orphan.
112
+ select is(
113
+ (select coalesce(array_agg(s.counterpart order by s.counterpart), array[]::text[])
114
+ from (select regexp_replace(p.proname, '^signed_', '') as counterpart
115
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
116
+ where n.nspname = 'agent_board' and p.proname like 'signed\_%') s
117
+ where not exists (select 1 from pg_proc p2 join pg_namespace n2 on n2.oid = p2.pronamespace
118
+ where n2.nspname = 'agent_board' and p2.proname = s.counterpart)),
119
+ array[]::text[],
120
+ '#310: every signed gateway HAS an unsigned counterpart — a gateway whose counterpart is missing or was never created fails here and names itself');
121
+
122
+ select is(
123
+ (select coalesce(array_agg(distinct s.counterpart order by s.counterpart), array[]::text[])
124
+ from (select regexp_replace(p.proname, '^signed_', '') as counterpart
125
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
126
+ where n.nspname = 'agent_board' and p.proname like 'signed\_%') s
127
+ where exists (select 1 from pg_proc p2 join pg_namespace n2 on n2.oid = p2.pronamespace
128
+ where n2.nspname = 'agent_board' and p2.proname = s.counterpart
129
+ and has_function_privilege('authenticated', p2.oid, 'EXECUTE'))),
130
+ array['append_task_event', 'create_tasking_delivery', 'grant_board_role', 'post_board_message',
131
+ 'send_direct_message', 'send_multicast_message', 'set_board_authority_ceiling']::text[],
132
+ '#310: every unsigned counterpart IS authenticated-executable — that is the shipped model (the gate is inside the function), and this pins it rather than leaving it implied');
133
+
134
+ -- ---------------------------------------------------------------------------
135
+ -- THE BODY HALF, over the CALL CLOSURE.
136
+ -- ---------------------------------------------------------------------------
137
+ -- PER-OID, not per-name, and that is a correction rather than a refinement. An earlier draft
138
+ -- aggregated `distinct root_name`, so adding an authenticated, UNGATED OVERLOAD of an existing
139
+ -- counterpart left every expected name present -- another overload was still gated and supplied
140
+ -- the name. Verified: the whole file stayed green with such an overload installed. Overloads are
141
+ -- separate functions with separate grants, so the population is separate functions too.
142
+ --
143
+ -- Spelled as "the set that does NOT reach it is empty", which is fail-CLOSED here precisely
144
+ -- because the population comes from pg_proc and not from the closure: if the closure helper ever
145
+ -- returns nothing, every member lands in the non-reaching set and this goes red. The canary
146
+ -- below pins the population size so the set cannot go empty by the population going empty.
147
+ --
148
+ -- WHAT "REACHES" PROVES, and what it does not. The closure establishes textual REACHABILITY,
149
+ -- not data flow: a function could call the resolver and ignore the result, or call it on a
150
+ -- branch that never executes. Two things narrow that gap without closing it. The resolver is
151
+ -- SIDE-EFFECTFUL on the live path -- for `authenticated` it raises 42501 on a missing session or
152
+ -- an id mismatch -- so even a result-ignoring call enforces session PRESENCE; and the
153
+ -- drop-the-resolver mutation pins regressions in the existing members. What it does NOT enforce
154
+ -- is actor BINDING: a function could resolve an actor and then write a caller-supplied id
155
+ -- anyway. For the eleven current overloads the stronger property holds by reading -- the send
156
+ -- family delegates to post_message, and set_board_authority_ceiling feeds v_actor into its owner
157
+ -- gate on every path -- but that is a reading, not an assertion, and it is stated as one.
158
+ select is(
159
+ (select coalesce(array_agg(pop.oid::regprocedure::text order by pop.oid::regprocedure::text), array[]::text[])
160
+ from (select p.oid
161
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
162
+ where n.nspname = 'agent_board'
163
+ and p.proname in (select regexp_replace(sp.proname, '^signed_', '')
164
+ from pg_proc sp join pg_namespace sn on sn.oid = sp.pronamespace
165
+ where sn.nspname = 'agent_board' and sp.proname like 'signed\_%')) pop
166
+ where not exists (select 1 from agent_board_test.fn_call_closure() cl
167
+ where cl.root_oid = pop.oid and cl.reached_name = '_resolved_actor_agent_id')),
168
+ array[]::text[],
169
+ '#310: EVERY unsigned counterpart OVERLOAD REACHES the session resolver — per-OID, because a single ungated overload is a whole door and its name is already spoken for by the gated one');
170
+
171
+ select is(
172
+ (select count(*)::text
173
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
174
+ where n.nspname = 'agent_board'
175
+ and p.proname in (select regexp_replace(sp.proname, '^signed_', '')
176
+ from pg_proc sp join pg_namespace sn on sn.oid = sp.pronamespace
177
+ where sn.nspname = 'agent_board' and sp.proname like 'signed\_%')),
178
+ '11',
179
+ '#310 canary: there are ELEVEN counterpart overloads across the seven names — pins the population of the assertion above, so an empty non-reaching set cannot mean an empty population');
180
+
181
+ -- THE RELAY DOOR. The flag asks whether a `signed_%`-NAMED function is executable by a
182
+ -- low-privilege role. That is not the same question as "can a low-privilege role reach the
183
+ -- signed path", and the gap is reachable: an `authenticated`-executable SECURITY DEFINER
184
+ -- function with ANY OTHER NAME can call a gateway through its owner's privileges. Verified --
185
+ -- such a relay left the flag `true` and every assertion in this file green.
186
+ --
187
+ -- The flag cannot close this (it would have to read bodies). The closure can, and does -- but
188
+ -- only within the bounds below, and those bounds point the DANGEROUS way for this one assertion.
189
+ --
190
+ -- WHY THE DIRECTION MATTERS HERE AND NOWHERE ELSE IN THIS FILE. Every other closure assertion is
191
+ -- MUST-REACH: a call edge the closure misses puts a member in the non-reaching set and turns it
192
+ -- RED. This is the file's only MUST-NOT-REACH assertion, so a missed edge turns it GREEN. Same
193
+ -- instrument, opposite failure direction, which is why its limits are stated here rather than
194
+ -- left implied:
195
+ --
196
+ -- * SCHEMA SCOPE. fn_call_closure's population is `agent_board` only. A SECURITY DEFINER relay
197
+ -- in ANOTHER schema -- `public`, say -- calling agent_board.signed_post_board_message is
198
+ -- invisible to this assertion AND to the flag. Reaching that state requires a migration,
199
+ -- not grant drift, which is why it is disclosed rather than treated as live.
200
+ -- * DYNAMIC SQL. A gateway named inside an `EXECUTE '...'` literal forms no call edge, because
201
+ -- stripped_body removes literals in its default mode. agent_board_fk_lock_edges.test.sql
202
+ -- carries an FKL-11 dynamic-SQL tripwire for exactly this; fn_call_closure has none.
203
+ --
204
+ -- Both are recorded rather than closed, and neither is reachable by granting alone.
205
+ select is(
206
+ (select coalesce(array_agg(distinct cl.root_oid::regprocedure::text order by cl.root_oid::regprocedure::text), array[]::text[])
207
+ from agent_board_test.fn_call_closure() cl
208
+ join pg_proc rp on rp.oid = cl.root_oid
209
+ join pg_namespace rn on rn.oid = rp.pronamespace
210
+ join pg_proc gp on gp.proname = cl.reached_name
211
+ join pg_namespace gn on gn.oid = gp.pronamespace
212
+ where rn.nspname = 'agent_board' and gn.nspname = 'agent_board'
213
+ and gp.proname like 'signed\_%'
214
+ and (has_function_privilege('authenticated', rp.oid, 'EXECUTE')
215
+ or has_function_privilege('anon', rp.oid, 'EXECUTE'))),
216
+ array[]::text[],
217
+ '#310: nothing IN agent_board executable by authenticated or anon reaches a signed_* gateway through its STATIC call closure — closes the in-schema SECURITY DEFINER relay; see the scope note above for the two doors it does not see');
218
+
219
+ -- CANARY for the predicate set the coverage assertion below depends on. Two of its members are
220
+ -- named rather than matched by pattern, so pattern drift would silently shrink the set and make
221
+ -- the coverage claim easier to satisfy. Pinning the membership makes that drift loud.
222
+ select is(
223
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
224
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
225
+ where n.nspname = 'agent_board' and p.prorettype = 'boolean'::regtype
226
+ and (p.proname ~ '^_(current_)?agent_(can|has)_'
227
+ or p.proname ~ '^_grant_within_'
228
+ or p.proname in ('_invite_permitted', '_membership_consent_present'))),
229
+ array['_agent_can_access_thread', '_agent_has_board_access', '_current_agent_can_access_task',
230
+ '_current_agent_can_access_thread', '_current_agent_can_mutate_task',
231
+ '_current_agent_can_post_to_thread', '_current_agent_can_send_to_board',
232
+ '_current_agent_has_board_access', '_grant_within_ceiling', '_invite_permitted',
233
+ '_membership_consent_present']::text[],
234
+ '#310 canary: the authorization-predicate set is exactly these eleven — note `_is_service_role` is excluded BY PATTERN, not by name: it discriminates a role, it does not authorize, and counting it would let a function that only checks it read as gated');
235
+
236
+ -- PER-OID, for the same reason the resolver assertion above is: this was per-NAME, so an
237
+ -- authenticated overload that reaches the resolver but NO authorization predicate was absorbed
238
+ -- -- the gated sibling overload supplied the `exists` for the name. Measured: such an overload
239
+ -- reddened ONLY the population canary above. That is a real tripwire, but the routine response
240
+ -- to a count canary is to bump the count, and that edit would have silently absorbed the hole.
241
+ -- A canary is not a substitute for the assertion it guards.
242
+ select is(
243
+ (select coalesce(array_agg(pop.oid::regprocedure::text order by pop.oid::regprocedure::text), array[]::text[])
244
+ from (select p.oid
245
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
246
+ where n.nspname = 'agent_board'
247
+ and p.proname in (select regexp_replace(sp.proname, '^signed_', '')
248
+ from pg_proc sp join pg_namespace sn on sn.oid = sp.pronamespace
249
+ where sn.nspname = 'agent_board' and sp.proname like 'signed\_%')) pop
250
+ where not exists (
251
+ select 1 from agent_board_test.fn_call_closure() cl
252
+ join pg_proc ap on ap.proname = cl.reached_name
253
+ join pg_namespace an on an.oid = ap.pronamespace
254
+ where cl.root_oid = pop.oid
255
+ and an.nspname = 'agent_board' and ap.prorettype = 'boolean'::regtype
256
+ and (ap.proname ~ '^_(current_)?agent_(can|has)_'
257
+ or ap.proname ~ '^_grant_within_'
258
+ or ap.proname in ('_invite_permitted', '_membership_consent_present')))),
259
+ array['agent_board.set_board_authority_ceiling(uuid,text,uuid)']::text[],
260
+ '#310: exactly ONE counterpart OVERLOAD authorizes without reaching a shared predicate — per-OID, so an ungated overload cannot hide behind a gated sibling of the same name');
261
+
262
+ -- ---------------------------------------------------------------------------
263
+ -- THE EXEMPTION, asserted positively and BEHAVIOURALLY. `set_board_authority_ceiling` gates
264
+ -- inline on the actor's board role instead of through a shared predicate, so the closure cannot
265
+ -- see it. An exemption claimed by absence is this repo's named soft spot; these three assertions
266
+ -- make the claim evidence.
267
+ -- ---------------------------------------------------------------------------
268
+ select agent_board.register_agent('agent', 'swe-owner', 'SWE Owner');
269
+ select agent_board.register_agent('agent', 'swe-other', 'SWE Other');
270
+
271
+ create temp table _swe on commit drop as
272
+ select (select id from agent_board.agents where handle = 'swe-owner') as owner_id,
273
+ (select id from agent_board.agents where handle = 'swe-other') as other_id;
274
+
275
+ select lives_ok(
276
+ $$ select agent_board.create_board((select owner_id from _swe), 'swe-board', 'SWE Board') $$,
277
+ 'the owner creates a board');
278
+
279
+ create temp table _sweb on commit drop as
280
+ select id as board_id from agent_board.boards where slug = 'swe-board';
281
+
282
+ select lives_ok(
283
+ $$ create temp table _swecap as select * from agent_board.set_board_authority_ceiling(
284
+ (select board_id from _sweb), 'admin', (select owner_id from _swe)) $$,
285
+ '#310 exemption, allow side: the board OWNER can set the authority ceiling');
286
+
287
+ select is(
288
+ (select max_grantable_role from agent_board.board_authority_ceilings
289
+ where board_id = (select board_id from _sweb)),
290
+ 'admin',
291
+ '#310 exemption: the owner call actually took effect — a refusal that also refused the owner would satisfy the deny assertion below on its own');
292
+
293
+ select throws_ok(
294
+ $$ select agent_board.set_board_authority_ceiling(
295
+ (select board_id from _sweb), 'owner', (select other_id from _swe)) $$,
296
+ '42501',
297
+ null,
298
+ '#310 exemption, deny side (stranger): an agent with no membership at all is refused 42501');
299
+
300
+ -- THE DENY SIDE ABOVE IS NOT ENOUGH ON ITS OWN, and this pair is why. A stranger is refused by
301
+ -- any gate that checks membership AT ALL, so it cannot distinguish "owner-only" from
302
+ -- "member-or-better". Measured: widening the inline gate from `bm.role = 'owner'` to
303
+ -- `bm.role in ('owner','admin','member')` left BOTH assertions above green. The subject that
304
+ -- separates the two claims is an ACTIVE NON-OWNER MEMBER, so that is what this seats.
305
+ select lives_ok(
306
+ $$ select agent_board._upsert_board_membership_state(
307
+ (select board_id from _sweb), (select other_id from _swe), 'joined', (select owner_id from _swe)) $$,
308
+ 'the other agent becomes an ACTIVE member of the board');
309
+
310
+ select is(
311
+ (select role from agent_board.board_memberships
312
+ where board_id = (select board_id from _sweb) and agent_id = (select other_id from _swe)
313
+ order by case when membership_state in ('joined','subscribed') then 0 else 1 end, created_at desc, id desc
314
+ limit 1),
315
+ 'member',
316
+ '#310 fixture: that membership is a plain member, not an owner — the precondition the assertion below needs');
317
+
318
+ select throws_ok(
319
+ $$ select agent_board.set_board_authority_ceiling(
320
+ (select board_id from _sweb), 'owner', (select other_id from _swe)) $$,
321
+ '42501',
322
+ null,
323
+ '#310 exemption, deny side (non-owner MEMBER): the inline gate is OWNER-only, not merely membership-checking — this is the assertion the stranger case cannot make');
324
+
325
+ -- ---------------------------------------------------------------------------
326
+ -- THE INSTRUMENT. Both of these are #310's own acceptance criteria.
327
+ -- ---------------------------------------------------------------------------
328
+ select is(
329
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
330
+ from pg_proc p join pg_namespace n on n.oid = p.pronamespace
331
+ where n.nspname = 'agent_board'
332
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
333
+ and agent_board_test.stripped_body(p.oid, true) ~* 'role_routine_grants'),
334
+ array[]::text[],
335
+ '#310: NO shipped function reads information_schema.role_routine_grants — it shows direct grants only and is filtered to visible roles, the fail-open shape #302 corrected');
336
+
337
+ -- The #118 class: recreating a SECURITY DEFINER function drops its ACL and COMMENT, and a
338
+ -- CREATE OR REPLACE that changed the signature would silently re-grant EXECUTE to PUBLIC.
339
+ -- Measured before the migration was written: authenticated and service_role held EXECUTE, anon
340
+ -- did not, PUBLIC held nothing. This pins that it still holds.
341
+ -- Aggregated over pg_roles, NOT a four-name candidate list. The list form was the
342
+ -- exemption-list soft spot rule 1 names: a widening grant to any OTHER existing role -- a portal
343
+ -- role, `dashboard_user` -- changes the real executor set and passes a membership test over four
344
+ -- names. Superusers and the function's OWNER are excluded because their access is implicit
345
+ -- rather than granted, and excluding the owner BY OID rather than by name keeps this portable to
346
+ -- a cluster where the bundle is owned by something other than `postgres`.
347
+ select is(
348
+ (select coalesce(array_agg(r.rolname::text order by r.rolname::text), array[]::text[])
349
+ from pg_roles r
350
+ cross join (select p.oid, p.proowner from pg_proc p join pg_namespace n on n.oid = p.pronamespace
351
+ where n.nspname = 'agent_board' and p.proname = 'get_bundle_compatibility') f
352
+ where not r.rolsuper
353
+ and r.oid <> f.proowner
354
+ and has_function_privilege(r.rolname, f.oid, 'EXECUTE')),
355
+ array['authenticated', 'service_role']::text[],
356
+ '#310 / #118 class: exactly authenticated and service_role hold granted EXECUTE on the health RPC — over ALL non-superuser roles, so a grant to any role at all fails here');
357
+
358
+ select isnt(
359
+ (select obj_description(
360
+ (select p.oid from pg_proc p join pg_namespace n on n.oid = p.pronamespace
361
+ where n.nspname = 'agent_board' and p.proname = 'get_bundle_compatibility'), 'pg_proc')),
362
+ null,
363
+ '#310 / #118 class: the COMMENT survived the recreate — DROP FUNCTION discards it and a recreate that omits it loses the only in-database statement of what the flag means');
364
+
365
+ select * from finish();
366
+ rollback;
@@ -646,13 +646,30 @@ select ok(
646
646
  'and it takes them agents-BEFORE-keys, matching _revoke_agent_principal [SOLE PIN: no isolation permutation discriminates this ordering]');
647
647
 
648
648
  -- ===========================================================================
649
- -- 9. CLASS (catalog-derived, CLAUDE.md rule 1): the agents/board_memberships cycle class.
649
+ -- 9. CLASS: the agents/board_memberships cycle class AS FAR AS A TEXT SCAN CAN SEE IT.
650
650
  --
651
651
  -- F4 was an ABBA between signed_grant_board_role and onboard_federated_fleet_agent on
652
- -- (board_memberships, agents). The fix is only as good as the claim that those are the ONLY
653
- -- two functions that lock both — and that claim must come from pg_catalog, not from this
654
- -- file. Any function that row-locks both relations can close the same cycle, so every member
655
- -- must take the board advisory lock that serialises them.
652
+ -- (board_memberships, agents). The fix is only as good as the claim about which functions
653
+ -- lock both relations. Any function that row-locks both can close the same cycle, so every
654
+ -- member here must take the board advisory lock that serialises them.
655
+ --
656
+ -- WHAT THIS ASSERTION ACTUALLY DELIVERS, corrected under message-board#281. It finds locks by
657
+ -- matching `for (update|share)` and `update agent_board.board_memberships` in the body text.
658
+ -- That is a claim about EXPLICIT locks only. A lock PostgreSQL takes on our behalf through a
659
+ -- FOREIGN KEY appears nowhere in a body, so this scan is structurally blind to it, and the
660
+ -- header it carried until #281 — "every function that row-locks BOTH" — claimed a closure it
661
+ -- cannot compute. Measured: this scan finds THREE members; the FK-aware derivation in
662
+ -- agent_board_fk_lock_edges.test.sql finds 28. One of the 25 it cannot see,
663
+ -- `_upsert_board_membership_state` (board_memberships FOR UPDATE, then agents FOR KEY SHARE
664
+ -- via board_membership_events_agent_id_fkey inside `_emit_membership_event`), is one half of a
665
+ -- live ABBA against onboard_federated_fleet_agent.
666
+ --
667
+ -- So this assertion is kept for what it IS — the explicit-lock members must stay
668
+ -- advisory-serialised, and a new explicit both-locker still names itself here — and the
669
+ -- class-completeness claim now lives in agent_board_fk_lock_edges.test.sql (FKL-9), which
670
+ -- derives FK-implied edges from pg_constraint. It is NOT upgraded in place: the FK-aware class
671
+ -- is 25 members short of serialised, and greening THAT is the ordering fix #281 deliberately
672
+ -- separates from the tooling.
656
673
  --
657
674
  -- THE CALL GRAPH IS CLOSED OVER FIRST, because a body-text scan cannot see a lock taken in a
658
675
  -- CALLEE and EVERY lock the wrapper takes is in one: board_memberships in grant_board_role's
@@ -708,7 +725,7 @@ select is(
708
725
  'onboard_federated_fleet_agent => serialised by the board advisory lock',
709
726
  'signed_grant_board_role => serialised by the board advisory lock'
710
727
  ]::text[],
711
- 'every function that row-locks BOTH agents and board_memberships takes the board advisory lock (transitive; a new one names itself)');
728
+ 'every function that EXPLICITLY row-locks both agents and board_memberships takes the board advisory lock (transitive; a new one names itself). FK-implied locks are invisible here and are covered by agent_board_fk_lock_edges.test.sql');
712
729
 
713
730
  select finish();
714
731