@consiliency/agent-board-schema 1.35.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.
@@ -0,0 +1,1016 @@
1
+ -- Consiliency/message-board#325 — derive the write-enforcement class from EFFECT, not from the
2
+ -- `signed_*` NAME.
3
+ --
4
+ -- agent_board_signed_write_enforced.test.sql (#310/#324) closes a class scoped by
5
+ -- `proname like 'signed\_%'` and derived by stripping the prefix: 7 names, 11 overloads. That
6
+ -- file DISCLOSES its own blind spot in its header — "a new ungated writer with NO signed twin
7
+ -- turns NEITHER the flag nor an assertion red". This file closes that blind spot by scoping the
8
+ -- class the other way: not by what a function is CALLED, but by what it DOES.
9
+ --
10
+ -- THE CLASS every `authenticated`-EXECUTABLE function in `agent_board` that writes an
11
+ -- `agent_board` relation — ITSELF OR THROUGH ANYTHING IT CALLS. Every part of that
12
+ -- comes from `pg_catalog`: the population from `pg_proc` + `has_function_privilege`,
13
+ -- the write-target universe from `pg_class`, the delegation edges from
14
+ -- `agent_board_test.fn_call_closure()`. Nothing here is a list.
15
+ --
16
+ -- Measured on a scratch database built from this working tree's bundle: 59 writer overloads
17
+ -- across 37 relations.
18
+ --
19
+ -- ===========================================================================================
20
+ -- MEMBERSHIP IS TRANSITIVE, AND THE FIRST VERSION OF THIS FILE GOT THAT WRONG.
21
+ -- ===========================================================================================
22
+ -- The first revision scoped membership by DML IN THE ROOT'S OWN BODY: 44 overloads, 32 tables.
23
+ -- That reintroduced, at the MEMBERSHIP layer, precisely the blindness #325 exists to remove.
24
+ -- #324's direct-body scan misjudged four of seven send-family functions for delegating; this
25
+ -- file's own class predicate then did the same thing one level up.
26
+ --
27
+ -- Every write HELPER is revoked from `authenticated`, so the delegating WRAPPERS are the only
28
+ -- authenticated entry points — and a wrapper contains no DML. Measured: FIFTEEN
29
+ -- authenticated-executable functions reach a direct writer through their closure and were
30
+ -- OUTSIDE the 44 —
31
+ --
32
+ -- send_direct_message (x2) post_board_message (x2) send_multicast_message (x2)
33
+ -- create_tasking_delivery (x2) join_board leave_board
34
+ -- invite_board_member request_join_board resolve_board_request
35
+ -- redeem_invite_link update_board_membership
36
+ --
37
+ -- `send_direct_message` -> `post_message` -> `insert into agent_board.messages` is a live chain,
38
+ -- pinned positively by WEC-4 below.
39
+ --
40
+ -- THE ATTACK THAT MADE IT BLOCKING, and it needs no new privilege. `_upsert_delivery_recipient`
41
+ -- UPDATEs `threads` and INSERTs `delivery_recipients`, and is revoked from `authenticated`. A new
42
+ -- authenticated function whose entire body is `perform agent_board._upsert_delivery_recipient(…)`
43
+ -- is an authenticated write of two class relations with no session derivation and no gate — and
44
+ -- under own-body membership the class size stayed 44 and EVERY assertion stayed GREEN. It is
45
+ -- #324's hole with one extra call. Mutation `w` pins it.
46
+ --
47
+ -- The repo had already paid for this lesson twice, in writing:
48
+ -- * `agent_board_consent_gating.test.sql` EC-4 puts the SINK NAME in its class predicate for
49
+ -- exactly this reason, so a one-hop wrapper cannot vanish from its own class.
50
+ -- * the #113 lint comment in `scripts/run-agent-board-schema-tests.sh` says in so many words
51
+ -- that DML-in-body "MISSES delegating RPCs such as join_board, whose writes live in a helper".
52
+ -- Both were on disk while the first revision was written. Recorded here because the failure was
53
+ -- not a lack of evidence.
54
+ --
55
+ -- ===========================================================================================
56
+ -- THE PRICE OF TRANSITIVE MEMBERSHIP: THE FAIL-CLOSED DIRECTION INVERTS. Disclosed, not hidden.
57
+ -- ===========================================================================================
58
+ -- The signed_write header states the rule this file must now obey: an "empty violating set"
59
+ -- assertion is fail-CLOSED when its population comes from `pg_proc`, and fail-OPEN when the
60
+ -- population is drawn from the CLOSURE, because an empty closure empties the population too.
61
+ --
62
+ -- The population is NOW drawn from the closure. So with `fn_call_closure()` dead, `_wec_pop`
63
+ -- goes empty and the two "violating set is empty" assertions — WEC-5 and WEC-9 — pass over
64
+ -- NOTHING. Measured, not reasoned: mutation `e` shims the helper to zero rows and those two go
65
+ -- GREEN, where under own-body membership they went red.
66
+ --
67
+ -- What replaces them as the tripwire is the EXACT-SET population pins, which is why WEC-2,
68
+ -- WEC-3, WEC-8 and WEC-8 are exact sets and not counts: a dead closure empties them against a
69
+ -- 59-name expectation and they fail LOUDLY. WEC-4 is the third leg and the one aimed squarely
70
+ -- at the instrument — it asserts a specific delegation chain still exists, so a closure that
71
+ -- has stopped resolving edges reds a named assertion instead of quietly widening a `not exists`.
72
+ --
73
+ -- The honest statement is therefore NOT "this file is fail-closed". It is: the two half
74
+ -- assertions are fail-OPEN against a dead instrument, and four population/liveness pins beside
75
+ -- them are fail-CLOSED. That is a weaker property than the first revision claimed, and it is
76
+ -- the cost of covering delegation at all.
77
+ --
78
+ -- ===========================================================================================
79
+ -- WHAT THIS FILE FOUND THAT ITS OWN PLAN DID NOT.
80
+ -- ===========================================================================================
81
+ --
82
+ -- THE PLAN'S HALF B IS VACUOUS AS SPELLED. The plan says: "every writer reaches a shared authz
83
+ -- predicate OR raises 42501 IN ITS CLOSURE. Expected violating set: EMPTY." That is true — 59 of
84
+ -- 59 — but for a reason that makes it worthless: `_resolved_actor_agent_id`, the session resolver
85
+ -- Half A already pins, ITSELF raises 42501. Every writer satisfying Half A satisfies
86
+ -- closure-42501 automatically, so Half B would restate Half A and no mutation could red it.
87
+ -- Measured over the 59:
88
+ --
89
+ -- 42501 anywhere in the writer's CLOSURE 59 of 59 (satisfied via the resolver)
90
+ -- 42501 in the writer's OWN body 38 of 59
91
+ -- reaches a shared authz predicate 28 of 59
92
+ -- NEITHER of the two above 16 of 59 ← the plan says this is 0
93
+ --
94
+ -- The plan's partition ("15 reach a shared predicate, the other 29 gate inline, 44/44") does not
95
+ -- reproduce at any population.
96
+ --
97
+ -- AND ONE CLAIM FROM THIS FILE'S OWN FIRST REVISION DID NOT SURVIVE THE WIDER CLASS. Over the 44
98
+ -- the predicate-reachers were a strict SUBSET of the own-body raisers, so the shared-predicate
99
+ -- arm added no member and was disclosed as subsumed. Over the 59 that is FALSE: five writers —
100
+ -- create_tasking_delivery, leave_board, post_board_message, send_direct_message and
101
+ -- send_multicast_message (the jsonb-recipient overloads) — reach a shared predicate and raise no
102
+ -- 42501 of their own. They are all DELEGATING members, i.e. exactly the ones own-body membership
103
+ -- could not see. The arm is now load-bearing rather than subsumed, and the reason is worth
104
+ -- keeping: a property measured over a class that was defined too narrowly is a property about
105
+ -- the narrowness.
106
+ --
107
+ -- THE PLAN'S STRUCTURAL PRIVILEGE BOUND DOES NOT HOLD, and section 6 predicted it might:
108
+ -- "measured for `board_memberships` only — re-verify per table". Re-verified per relation over
109
+ -- all 37:
110
+ --
111
+ -- authenticated / anon holds NO INSERT/UPDATE/DELETE/TRUNCATE on ANY of the 37. HOLDS.
112
+ -- service_role holds direct DML on THIRTEEN of the 37 (WEC-18). FAILS.
113
+ --
114
+ -- ===========================================================================================
115
+ -- INSTRUMENT LIMITS — disclosed rather than discovered.
116
+ -- ===========================================================================================
117
+ --
118
+ -- 1. SCHEMA SCOPE. `fn_call_closure()` draws its population from `agent_board` only, so a
119
+ -- writer, resolver or relay in ANOTHER schema forms no edge. Under transitive membership
120
+ -- this cuts BOTH ways and that is new: a missed edge no longer only under-credits a gate,
121
+ -- it can drop a function OUT OF THE CLASS. Bounded by WEC-3 being an exact set.
122
+ --
123
+ -- 2. ASSEMBLED SQL. Fragment concatenation and `EXECUTE format('%I', …)` form no call edge and
124
+ -- name no relation. The structural bound in WEC-17/19/20 is what bounds this — and per the
125
+ -- finding above, it bounds it only against `authenticated` and `anon`, NOT service_role.
126
+ --
127
+ -- 3. REACHABILITY IS NOT DATA FLOW. The closure proves a writer REACHES the resolver, not that
128
+ -- the resolved actor controls the write. The resolver is SIDE-EFFECTFUL — for an
129
+ -- `authenticated` caller it raises 42501 on an absent session or a mismatched actor id — so
130
+ -- even a result-ignoring call enforces session PRESENCE. It does NOT enforce actor BINDING.
131
+ -- For `create_thread`, probed behaviourally in WEC-14..17, the stronger property holds by
132
+ -- READING (it inserts `v_actor_agent_id`, the resolver's return, never the caller's
133
+ -- argument). That is a reading of ONE member, stated as one and not generalised to 59.
134
+ --
135
+ -- 4. VIEWS. Views ARE in the write-target universe below (`relkind in ('r','v','m','p')`), and
136
+ -- that is a correction. The first revision scanned base tables only and justified it with
137
+ -- "a write to a view is a write to its base table, which this set already contains" — true
138
+ -- of the EFFECT and FALSE as a property of a REGEX, which matches the name written, not the
139
+ -- name resolved. An `insert into agent_board.<auto-updatable view>` was therefore invisible.
140
+ -- Including views closes it. Measured today: agent_board has 26 views and NO function writes
141
+ -- one, so the arm is real but UNEXERCISED against shipped code — a fact about today's
142
+ -- catalog, not a property of the scan. Only the 59 BASE tables are pinned as the universe
143
+ -- canary; the view count is deliberately not pinned, because a new read-model view is
144
+ -- routine and should not red an enforcement file.
145
+ --
146
+ -- 5. REFERENTIAL ACTIONS. A CASCADE or SET NULL is a write path with no privilege, no function
147
+ -- and no body. #317 measured this on `board_memberships` (2 rows to 0 on one
148
+ -- `delete from boards`). Across this class: 88 writing edges — 74 CASCADE plus 14 SET NULL —
149
+ -- from 19 parent relations, ONE OF WHICH IS `auth.users`, outside agent_board (WEC-22,
150
+ -- WEC-23). An earlier revision counted CASCADE only while claiming to cover the class, which
151
+ -- is why the SET NULL arm is named here rather than left to a filter. Nothing gates them.
152
+ --
153
+ -- 6. REGEX, NOT A PARSER. The write-statement pattern is REUSED from
154
+ -- agent_board_receipt_subject_gate.test.sql rather than rewritten, so there is one pattern
155
+ -- to fix and not two to drift. From there it knows `merge into`, `update only`,
156
+ -- `truncate [table] [only]`, quoted identifier parts and whitespace around the schema dot.
157
+ -- It is reused but NOT verbatim: neither of that file's two alternations has a `copy` arm,
158
+ -- so `copy agent_board.threads (…) from …` left the class unchanged and every assertion
159
+ -- GREEN (mutation o). `copy` is folded in here. `COPY … TO`, a READ, now matches too — the
160
+ -- fail-CLOSED direction, over-reporting loudly. The receipt file on main still lacks the
161
+ -- arm: a pre-existing gap in a DIFFERENT file, recorded and not fixed in a test-only change.
162
+ -- Relation names are interpolated unescaped; every agent_board relname is `[a-z_]+` today.
163
+ --
164
+ -- ===========================================================================================
165
+ -- MECHANICS THAT ARE NOT OPTIONAL HERE (each cost a review round elsewhere in this repo).
166
+ -- ===========================================================================================
167
+ --
168
+ -- * `stripped_body(oid, TRUE)` — KEEP LITERALS — for every predicate matching an error code or
169
+ -- a marker. `42501` lives in `using errcode = '42501'`, a STRING LITERAL, and the DEFAULT
170
+ -- mode strips literals: asking the default returns 0 and silently inverts the answer. Note
171
+ -- the asymmetry that makes both modes correct in their place — literals are KEPT where a
172
+ -- literal is the evidence, and `fn_call_closure` reads the DEFAULT so a function named inside
173
+ -- a string cannot manufacture a call edge.
174
+ --
175
+ -- * No message string below contains a space-hyphen-hyphen-space sequence. A repo lint strips
176
+ -- `--` to end of line even inside a quoted string, so one in a message swallows the following
177
+ -- statements and reports unrelated files. Em dashes throughout.
178
+ --
179
+ -- * Every state-mutating RPC call runs inside `lives_ok($$ … $$)` with the result captured to a
180
+ -- table that EXISTS BEFORE the call, never bare and never inside `is((select … from rpc(…)))`.
181
+ -- A raise in the bare form aborts the transaction and pgTAP emits NO `not ok` (#113).
182
+ --
183
+ -- * `create_thread` returns TABLE, so it is called as `select * from …`, not projected.
184
+ --
185
+ -- ===========================================================================================
186
+ -- MUTATION EVIDENCE — 30 mutations, every row MEASURED against this working tree's bundle on a
187
+ -- scratch database, never predicted. `ran = 23/23` confirmed on EVERY row: a mutation that fails
188
+ -- loudly but TRUNCATES the run hides every assertion after the abort, and three mutations in #317
189
+ -- did exactly that. WEC-N === TAP assertion N by construction, verified mechanically.
190
+ --
191
+ -- EACH MUTATION WAS VERIFIED APPLIED AGAINST pg_catalog, not by the file having changed — a
192
+ -- `create or replace` whose text changed but whose meaning did not is semantically inert, and an
193
+ -- inert mutation that reds nothing is indistinguishable from a hole. The verification probe
194
+ -- tracks the CLOSURE membership rule: an own-body probe would have called the wrapper attack
195
+ -- below "class 59 -> 59", i.e. inert, while the file saw a new member. Each mutation ran inside
196
+ -- the SAME transaction the test file rolls back; no leftovers, confirmed by query afterwards.
197
+ --
198
+ -- mut what VERIFY (catalog) -> red
199
+ -- ----- -------------------------------------------- ----------------------- --------------
200
+ -- w THE WRAPPER ATTACK. An authenticated function class 59 -> 60 3, 5, 8, 9
201
+ -- whose whole body is `perform
202
+ -- agent_board._upsert_delivery_recipient(…)`
203
+ -- THE ROW THE TRANSITIVE-MEMBERSHIP REVISION EXISTS FOR. Under own-body membership it
204
+ -- reddened NOTHING: class stayed 44, all 23 assertions GREEN, while an authenticated
205
+ -- caller could UPDATE `threads` and INSERT `delivery_recipients` with no session
206
+ -- derivation and no gate. It is #324's hole with one extra call.
207
+ -- x A NEW UNGATED WRITER WHOSE ONLY 42501 IS strict-raise match 3, 5, 8, 9
208
+ -- PROSE: `raise notice 'see errcode 42501 …'` -> false
209
+ -- THE GUARD-DEFEATED-BY-PROSE ROW. Under the digits-only spelling this matched the
210
+ -- "raises" test, was EXCLUDED from the WEC-8 category, counted as GATED, and reddened
211
+ -- ONLY WEC-3 — which is exactly what WEC-3's own rationale says it may not rely on.
212
+ -- Closed by requiring the SYNTAX of a raise; see the `_wec_raise_pat` note.
213
+ -- y a new ON DELETE SET NULL foreign key into SET NULL edges on 1, 22, 23
214
+ -- `agent_board.threads` threads 0 -> 1
215
+ -- Reddened NOTHING while WEC-22/23 filtered `confdeltype = 'c'` and limit 5 claimed the
216
+ -- function-less path was counted across the class. 14 such edges already existed.
217
+ -- z a gater spelled `raise insufficient_privilege` strict-raise match 3
218
+ -- rather than with the SQLSTATE -> true
219
+ -- A CORRECTNESS ROW, not an attack: it is properly gated, so redding ONLY WEC-3 (the
220
+ -- class exact set) is the RIGHT answer. Under the digits-only spelling it fell into the
221
+ -- WEC-8 category instead — fail-closed, so nothing shipped wrong, but a wrong diagnosis.
222
+ -- v an auto-updatable VIEW over a class relation class 59 -> 60 2, 3, 5, 8, 9
223
+ -- plus an authenticated function inserting into
224
+ -- the VIEW rather than the relation
225
+ -- Also GREEN before the rework: the target universe was base tables only, and the scan
226
+ -- matches the name WRITTEN, not the relation it resolves to.
227
+ -- a a NEW ungated authenticated writer with class 59 -> 60 3, 5, 8, 9
228
+ -- direct DML and no gate
229
+ -- b an ungated authenticated OVERLOAD of overloads 1 -> 2 3, 5, 8, 9
230
+ -- `create_thread`
231
+ -- PER-OID PROOF: the gated sibling still supplies the NAME.
232
+ -- c `create_thread` drops its closure edge 1 -> 0 5, 8, 9,
233
+ -- `_resolved_actor_agent_id` call 14, 15, 16
234
+ -- Reds WEC-8 and leaves nothing of the behavioural probe standing. It also reds 14/15
235
+ -- BECAUSE the allow side now supplies a NULL actor id: with the resolver gone there is
236
+ -- nothing to write, which is the actor-binding property the NULL upgrade bought.
237
+ -- d `set_board_role_gate` loses its ONLY authority own-body raise 8, 9
238
+ -- check (owner gate + its 42501) true -> false (5 STAYS GREEN)
239
+ -- THE DISCRIMINATING MUTATION: the writer still derives its actor, so Half A passes, and
240
+ -- it now refuses NOBODY.
241
+ -- e `fn_call_closure()` shimmed to zero rows closure rows -> 0 2, 3, 4, 7, 8,
242
+ -- 18, 22, 23
243
+ -- THE FAIL-OPEN INVERSION, MEASURED. WEC-5 and WEC-9 stay GREEN — they are "violating
244
+ -- set is empty" over a population now drawn FROM the closure, so an empty closure
245
+ -- empties the population and they pass over nothing. Under own-body membership they
246
+ -- RED. What catches it instead: the exact-set population pins (2, 3, 8), the privilege
247
+ -- and referential sets that also derive from the class (18, 22, 23), and WEC-4, the
248
+ -- delegation-liveness assertion added for exactly this. Eight assertions red — but NOT
249
+ -- the two half assertions, and that is the honest shape of the trade.
250
+ -- f `merge into only "agent_board"."threads"` class 59 -> 60 3, 5, 8, 9
251
+ -- g `truncate table only agent_board.presence` class 59 -> 60 3, 5, 8, 9
252
+ -- h an authenticated function writing a NEW table class 59 -> 60 1, 2, 3, 5, 8, 9
253
+ -- i `grant insert on agent_board.threads` to anon anon INSERT -> true 17, 19
254
+ -- j `grant insert on agent_board.threads` to a new role INSERT 19
255
+ -- BRAND-NEW role -> true
256
+ -- THE THREE-ARM PROOF: `j` reds ONLY the aclexplode arm. WEC-17 names
257
+ -- authenticated/anon, WEC-18 names service_role, so a role neither mentions self-names
258
+ -- nowhere else. Neither arm subsumes the other.
259
+ -- k `_resolved_actor_agent_id` stops raising own-body raise 11, 16
260
+ -- true -> false
261
+ -- WHY WEC-11 IS AN ASSERTION AND NOT A SENTENCE. WEC-8 stays GREEN: it asks which
262
+ -- resolver each member REACHES, and the resolver is still reached, still called, still
263
+ -- there — it has simply stopped refusing. WEC-11 is the only structural catch.
264
+ -- l a new ON DELETE CASCADE foreign key into cascade fks on 1, 22, 23
265
+ -- `agent_board.threads` threads 0 -> 1
266
+ -- m a new table `threads_archive` plus an class 59 -> 60 1, 2, 3, 5, 8, 9
267
+ -- authenticated writer of it
268
+ -- NOT THE NEGATIVE PIN IT WAS DESIGNED TO BE, recorded rather than quietly dropped. The
269
+ -- intent was #287's `read_receipts_archive` case — a writer of a LOOK-ALIKE relation
270
+ -- must not be attributed to the real one — but any new agent_board relation joins the
271
+ -- target universe by construction, so this is a CORRECT new member. The boundary
272
+ -- property was measured DIRECTLY instead and HOLDS: the scan attributes that writer to
273
+ -- `threads_archive` and to NOTHING ELSE, in particular not to `threads`, while the
274
+ -- control `create_thread` is attributed to `threads`. That is the `\M` right-hand
275
+ -- boundary working, with a positive control beside it so a clean result cannot mean a
276
+ -- pattern matching nothing.
277
+ -- mctl `m` plus a REAL new writer in the same class 59 -> 61 1, 2, 3, 5, 8, 9
278
+ -- transaction
279
+ -- n a writer whose INSERT lives inside an class 59 -> 60 3, 5, 8, 9
280
+ -- `EXECUTE` STRING LITERAL
281
+ -- What `keep_literals = true` buys beyond error codes: dynamic SQL lives in a literal,
282
+ -- and the DEFAULT mode would strip this writer's only statement and report it clean.
283
+ -- o `copy agent_board.threads (…) from …` class 59 -> 60 3, 5, 8, 9
284
+ -- FOUND A REAL HOLE AND CLOSED IT. Before `copy` joined the alternation this function
285
+ -- was invisible and every assertion GREEN. The plan listed `copy` as a battery row and
286
+ -- the pattern it told this file to reuse has no `copy` arm in EITHER alternation, so
287
+ -- "the pattern already knows copy" was false; only running the row showed it.
288
+ -- p5 a new function whose NAME matches the shape names 3 -> 4 6
289
+ -- derivation-shape pattern
290
+ -- p6 a SECOND writer with `auth.uid()` in its own own-body auth.uid 3, 7, 8, 9
291
+ -- body writers 1 -> 2
292
+ -- p10 a new boolean `_agent_can_frobnicate` predicate set 11 -> 12 10
293
+ -- p12 `_require_platform_admin_user_id` stops own-body raise 12
294
+ -- raising true -> false
295
+ -- p13 `current_agent_id` STARTS raising 42501 own-body raise 13, 14, 15, 16
296
+ -- false -> true
297
+ -- The direction matters: if it raised, it would count as a refusal and WEC-8 would
298
+ -- silently widen what "gated" means.
299
+ -- p1415 `create_thread` refuses EVERYBODY body raises, no INSERT 3, 8, 14, 15
300
+ -- (unconditional raise)
301
+ -- THE ALLOW SIDE EARNING ITS PLACE. A gate admitting NOBODY leaves the DENY assertion
302
+ -- (16) GREEN; only WEC-14/15 catch it.
303
+ -- p18 one service_role privilege revoked from the service_role DELETE on 18
304
+ -- pinned exact set fleet_machines -> false
305
+ -- p20 `revoke insert on agent_board.threads` postgres INSERT 14, 15, 20
306
+ -- from `postgres` -> false
307
+ -- p21 `revoke select on agent_board.threads` authenticated SELECT 21
308
+ -- from `authenticated` -> false
309
+ --
310
+ -- COVERAGE: the union of the red columns above is EXACTLY assertions 1 through 23 — every
311
+ -- assertion in this file goes red under at least one mutation.
312
+ --
313
+ -- SUBSUMPTION, measured rather than claimed away. WEC-9's red-set is a strict SUBSET of WEC-8's:
314
+ -- no row reds 9 without 8, while `e` and `p1415` red 8 without 9. WEC-9 is therefore a STATEMENT
315
+ -- of the class-wide property, not an independent pin, and it says so at its own site. The
316
+ -- membership assertion that used to sit beside WEC-8 was subsumed the same way and was DELETED;
317
+ -- WEC-9 is kept because deleting it would leave the file's headline claim existing only as an
318
+ -- inference from a sixteen-member category listing.
319
+ --
320
+ -- WHAT NO MUTATION REDDENED, disclosed rather than left for a reviewer to find: a new writer that
321
+ -- is CORRECTLY GATED reds only WEC-3, the exact class set (mutation z is exactly this case). That
322
+ -- is by construction — a gated writer violates neither half — but it makes WEC-3 the sole
323
+ -- tripwire for that case, and the routine response to a red exact set is to add the name. Adding
324
+ -- the name is the correct fix HERE, unlike bumping a count, only because WEC-5/8/9 independently
325
+ -- establish that the added member is gated. THAT RATIONALE WAS TOO BROAD BEFORE mutation x: with
326
+ -- the digits-only raise spelling, a prose-42501 writer satisfied WEC-8 and WEC-9 by evasion, so
327
+ -- for that one shape they established nothing and WEC-3 really was alone. The strict raise
328
+ -- pattern is what makes the sentence true again.
329
+
330
+ create extension if not exists pgtap with schema extensions;
331
+ set search_path = extensions, public, agent_board;
332
+
333
+ begin;
334
+
335
+ select plan(23);
336
+
337
+ -- ===========================================================================================
338
+ -- THE CLASS, derived from pg_catalog. Nothing below is a list of names.
339
+ -- ===========================================================================================
340
+ -- The write-target universe. `relkind in ('r','v','m','p')` — base tables, views, matviews and
341
+ -- partitioned parents. Views are here because the scan matches the name a statement WRITES, not
342
+ -- the relation it ultimately resolves to; see limit 4.
343
+ create temp table _wec_target on commit drop as
344
+ select c.oid as reloid, c.relname::text as relname, c.relkind
345
+ from pg_class c
346
+ where c.relnamespace = 'agent_board'::regnamespace
347
+ and c.relkind in ('r', 'v', 'm', 'p');
348
+
349
+ -- DIRECT writers: every agent_board function with a write statement in its own stripped body.
350
+ -- The population is deliberately NOT restricted to authenticated-executable functions — the
351
+ -- helpers are revoked from authenticated, and they are the ones that hold the DML. Restricting
352
+ -- here is the mistake this revision corrects.
353
+ create temp table _wec_direct on commit drop as
354
+ select p.oid as fn_oid, t.relname, t.relkind
355
+ from pg_proc p
356
+ cross join _wec_target t
357
+ where p.pronamespace = 'agent_board'::regnamespace
358
+ and p.prokind in ('f','p')
359
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
360
+ and lower(agent_board_test.stripped_body(p.oid, true))
361
+ ~ ('(insert\s+into|merge\s+into|update|delete\s+from|truncate|copy)(\s+table)?(\s+only)?\s+'
362
+ || '(("?agent_board"?\s*\.\s*)?' || t.relname || '\M'
363
+ || '|("?agent_board"?\s*\.\s*)?"' || t.relname || '")');
364
+
365
+ create temp table _wec_closure on commit drop as
366
+ select * from agent_board_test.fn_call_closure();
367
+
368
+ -- THE RAISE PATTERN, defined ONCE because it is used by three assertions and two copies drift.
369
+ --
370
+ -- It matches a RAISE, not the digits. The obvious spelling — `stripped_body(oid, true) ~ '42501'`
371
+ -- — is PROSE-SATISFIABLE, and that is measured rather than feared: a function whose only 42501 is
372
+ -- inside `raise notice 'see errcode 42501 for details'` matched it, was therefore EXCLUDED from
373
+ -- the WEC-8 category and counted as GATED, and reddened only WEC-3. That is the
374
+ -- guard-defeated-by-prose class this repo has been burned by before, and `keep_literals = true`
375
+ -- is what makes it reachable here: literals must be kept so `using errcode = '42501'` is visible,
376
+ -- and keeping them lets a message string speak too. The fix is to require the SYNTAX of a raise
377
+ -- rather than the presence of the code. Mutation `x` pins it.
378
+ --
379
+ -- `insufficient_privilege` is in the alternation because it is 42501's condition NAME: a
380
+ -- `raise insufficient_privilege` gater is a real refusal. Under the digits-only spelling it fell
381
+ -- into the WEC-8 category — fail-CLOSED, so nothing shipped wrong, but with a wrong diagnosis.
382
+ --
383
+ -- WHAT IT STILL DOES NOT SEE, stated rather than discovered: `using errcode = v_code`, where the
384
+ -- code is computed. Such a function reads as not-raising, lands in the WEC-8 category and must
385
+ -- then satisfy the resolver arm — fail-CLOSED, and no agent_board function is written that way
386
+ -- today (measured: this pattern and the digits-only one agree on ALL 67 shipped raisers; they
387
+ -- differ only on the decoy).
388
+ create temp table _wec_raise_pat on commit drop as
389
+ select 'errcode\s*=\s*''?42501|sqlstate\s+''?42501|insufficient_privilege'::text as pat;
390
+
391
+ -- THE POPULATION, per-OID and TRANSITIVE: an authenticated-executable root that reaches a direct
392
+ -- writer through the call closure. `fn_call_closure()` is REFLEXIVE (every function reaches
393
+ -- itself), so this single `exists` covers "writes in its own body" and "writes through a callee"
394
+ -- without a second arm.
395
+ --
396
+ -- Overloads are separate functions with separate grants, so they are separate members: an
397
+ -- ungated overload is a whole door, and its NAME is already spoken for by its gated sibling.
398
+ --
399
+ -- The signature is BUILT from `proargtypes`, not rendered by `regprocedure::text`, because
400
+ -- `regprocedure` OMITS THE SCHEMA for anything visible on the current search_path — which this
401
+ -- file's header puts `agent_board` on. `pg_get_function_identity_arguments` is not the fix
402
+ -- either: it renders declared PARAMETER NAMES. `format_type` over `proargtypes` reproduces
403
+ -- regprocedure's rendering exactly and depends on no session state.
404
+ create temp table _wec_pop on commit drop as
405
+ select distinct p.oid,
406
+ 'agent_board.' || p.proname || '('
407
+ || coalesce((select string_agg(format_type(u.t, null), ',' order by u.ord)
408
+ from unnest(p.proargtypes) with ordinality as u(t, ord)), '')
409
+ || ')' as sig
410
+ from pg_proc p
411
+ where p.pronamespace = 'agent_board'::regnamespace
412
+ and p.prokind in ('f','p')
413
+ and p.prolang <> (select oid from pg_language where lanname = 'internal')
414
+ and has_function_privilege('authenticated', p.oid, 'EXECUTE')
415
+ and exists (select 1 from _wec_closure cl
416
+ join _wec_direct d on d.fn_oid = cl.reached_oid
417
+ where cl.root_oid = p.oid);
418
+
419
+ -- WEC-1. The universe canary. Without it, a write-target set that had gone empty would empty the
420
+ -- writer population too. Base tables only — see limit 4 on why the 26 views are not pinned.
421
+ select is(
422
+ (select count(*)::text from _wec_target where relkind = 'r'),
423
+ '59',
424
+ '#325 canary: agent_board has 59 base tables — the universe the write scan runs over, pinned so an empty violating set below cannot mean an empty universe');
425
+
426
+ -- WEC-2. The relations the class writes, as an EXACT SET. A new one names itself.
427
+ select is(
428
+ (select coalesce(array_agg(distinct d.relname order by d.relname), array[]::text[])
429
+ from _wec_direct d
430
+ where exists (select 1 from _wec_closure cl join _wec_pop p on p.oid = cl.root_oid
431
+ where cl.reached_oid = d.fn_oid)),
432
+ array[
433
+ 'agent_credentials',
434
+ 'agent_signing_key_challenges',
435
+ 'agent_signing_keys',
436
+ 'agents',
437
+ 'artifacts',
438
+ 'board_authority_ceilings',
439
+ 'board_invite_blocks',
440
+ 'board_invite_redemptions',
441
+ 'board_invite_tokens',
442
+ 'board_membership_events',
443
+ 'board_memberships',
444
+ 'boards',
445
+ 'callback_endpoints',
446
+ 'callback_outbox_events',
447
+ 'callback_subscriptions',
448
+ 'claims',
449
+ 'conflicts',
450
+ 'delegation_policies',
451
+ 'delegations',
452
+ 'delivery_recipients',
453
+ 'escalations',
454
+ 'fleet_harness_registrations',
455
+ 'fleet_machine_agents',
456
+ 'fleet_machines',
457
+ 'fleet_onboarding_invite_bounds',
458
+ 'fleet_receive_policies',
459
+ 'fleet_repo_scopes',
460
+ 'fleet_revocation_events',
461
+ 'fleet_trust_domains',
462
+ 'inbox_items',
463
+ 'messages',
464
+ 'presence',
465
+ 'read_receipts',
466
+ 'send_authz_telemetry',
467
+ 'task_events',
468
+ 'tasks',
469
+ 'threads'
470
+ ]::text[],
471
+ '#325: these 37 agent_board relations are written by an authenticated-executable function, directly or through a callee — five of them (board_invite_redemptions, board_membership_events, callback_outbox_events, delivery_recipients, send_authz_telemetry) were invisible while membership was own-body');
472
+
473
+ -- WEC-3. The class itself, as an EXACT SET of SIGNATURES, and it is signatures rather than
474
+ -- `count(*) = 59` for two reasons. The routine response to a red count is to bump the count,
475
+ -- which absorbs the member the canary caught (#324). And under transitive membership this set is
476
+ -- also the population tripwire for a dead closure: it fails against a 59-name expectation rather
477
+ -- than passing over nothing.
478
+ select is( -- pgtap-allow-raise-checked
479
+ (select coalesce(array_agg(p.sig order by p.sig), array[]::text[]) from _wec_pop p),
480
+ array[
481
+ 'agent_board.acquire_claim(uuid,text,text,timestamp with time zone,uuid,uuid,jsonb)',
482
+ 'agent_board.add_artifact(uuid,text,text,text,uuid,uuid,uuid,uuid,jsonb)',
483
+ 'agent_board.append_task_event(uuid,uuid,text,jsonb,text,uuid,timestamp with time zone)',
484
+ 'agent_board.block_board_inviter(uuid,uuid,uuid,jsonb)',
485
+ 'agent_board.create_board(uuid,text,text,text,jsonb,text)',
486
+ 'agent_board.create_fleet_onboarding_invite(uuid,uuid,text,text,text,text,text,text,text,integer,timestamp with time zone,uuid,jsonb)',
487
+ 'agent_board.create_invite_link(uuid,text,text,integer,timestamp with time zone,uuid,jsonb)',
488
+ 'agent_board.create_or_find_thread(uuid,text,text,text,jsonb)',
489
+ 'agent_board.create_task(uuid,uuid,text,text,uuid,uuid,text,text,jsonb)',
490
+ 'agent_board.create_tasking_delivery(uuid,uuid,text,text,jsonb,text,text,text,text,text,jsonb)',
491
+ 'agent_board.create_tasking_delivery(uuid,uuid,text,text,uuid[],uuid,text,text,text,jsonb)',
492
+ 'agent_board.create_thread(uuid,text,text,jsonb)',
493
+ 'agent_board.delegate_task(uuid,uuid,text,text,uuid,text,text,jsonb)',
494
+ 'agent_board.delete_callback_endpoint(uuid,uuid)',
495
+ 'agent_board.disable_callback_endpoint(uuid,uuid,text,jsonb)',
496
+ 'agent_board.grant_board_role(uuid,uuid,text,uuid)',
497
+ 'agent_board.heartbeat(uuid,uuid,text,text,interval,jsonb)',
498
+ 'agent_board.invite_board_member(uuid,uuid,uuid,jsonb)',
499
+ 'agent_board.issue_signing_key_challenge(uuid,text,interval)',
500
+ 'agent_board.join_board(uuid,uuid,text,uuid,jsonb)',
501
+ 'agent_board.leave_board(uuid,uuid,jsonb)',
502
+ 'agent_board.mark_read(uuid,uuid,bigint)',
503
+ 'agent_board.post_board_message(uuid,uuid,text,text,text,text,text,text,text,jsonb)',
504
+ 'agent_board.post_board_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)',
505
+ 'agent_board.post_message(uuid,uuid,text,text,uuid,jsonb)',
506
+ 'agent_board.provision_agent(text,text,jsonb,uuid)',
507
+ 'agent_board.raise_conflict(uuid,uuid,text,text,text,uuid,jsonb)',
508
+ 'agent_board.raise_escalation(uuid,uuid,text,text,text,uuid,uuid,jsonb)',
509
+ 'agent_board.record_signing_key_proof_result(uuid,uuid,text,boolean,text,jsonb)',
510
+ 'agent_board.redeem_invite_link(text,jsonb)',
511
+ 'agent_board.register_agent(text,text,text,text,jsonb)',
512
+ 'agent_board.register_callback_endpoint(uuid,text,jsonb)',
513
+ 'agent_board.register_fleet_machine(uuid,uuid,text,text,text,jsonb)',
514
+ 'agent_board.release_claim(uuid)',
515
+ 'agent_board.renew_claim(uuid,timestamp with time zone)',
516
+ 'agent_board.request_join_board(uuid,uuid,jsonb)',
517
+ 'agent_board.resolve_board_request(uuid,uuid,text,uuid,jsonb)',
518
+ 'agent_board.resolve_conflict(uuid,uuid,jsonb)',
519
+ 'agent_board.resolve_escalation(uuid,uuid,jsonb)',
520
+ 'agent_board.revoke_agent(uuid,text)',
521
+ 'agent_board.revoke_fleet_machine_agent_binding(uuid,uuid,text,jsonb)',
522
+ 'agent_board.revoke_fleet_subject(text,uuid,uuid,text,jsonb)',
523
+ 'agent_board.revoke_invite_link(text,jsonb)',
524
+ 'agent_board.rotate_callback_signing_key(uuid,uuid,jsonb)',
525
+ 'agent_board.self_enroll_human(text)',
526
+ 'agent_board.send_direct_message(uuid,uuid,jsonb,text,text,text,text,text,text,jsonb)',
527
+ 'agent_board.send_direct_message(uuid,uuid,uuid,text,text,text,text,text,jsonb)',
528
+ 'agent_board.send_multicast_message(uuid,uuid,jsonb,text,text,text,text,text,text,jsonb)',
529
+ 'agent_board.send_multicast_message(uuid,uuid,uuid[],text,text,text,text,text,jsonb)',
530
+ 'agent_board.set_board_authority_ceiling(uuid,text,uuid)',
531
+ 'agent_board.set_board_role_gate(uuid,boolean)',
532
+ 'agent_board.set_fleet_receive_policy(uuid,text,text,boolean,uuid,uuid,jsonb)',
533
+ 'agent_board.subscribe_callback_endpoint(uuid,uuid,text,text,uuid,jsonb)',
534
+ 'agent_board.unblock_board_inviter(uuid,uuid,uuid)',
535
+ 'agent_board.unsubscribe_callback_endpoint(uuid,uuid,text,text,uuid,jsonb)',
536
+ 'agent_board.update_board_membership(uuid,uuid,text,jsonb)',
537
+ 'agent_board.update_inbox_item_state(uuid,uuid,text,jsonb)',
538
+ 'agent_board.upsert_delegation_policy(uuid,text,jsonb,integer,interval,boolean)',
539
+ 'agent_board.verify_callback_endpoint(uuid,uuid,jsonb)'
540
+ ]::text[],
541
+ '#325: the effect-derived class is exactly these 59 authenticated-executable writer OVERLOADS — per-OID, and TRANSITIVE, so a delegating wrapper whose own body holds no DML is a member rather than a blind spot');
542
+
543
+ -- WEC-4. THE DELEGATION-CHAIN LIVENESS PIN, and the mitigation for the fail-open inversion the
544
+ -- header describes. Everything above depends on `fn_call_closure()` actually resolving edges; a
545
+ -- closure that silently stopped doing so would EMPTY the population and make WEC-5 and WEC-9
546
+ -- pass over nothing. This asserts one specific chain still exists, end to end and by name:
547
+ -- `send_direct_message` reaches `post_message`, and `post_message` writes `messages`. It is
548
+ -- EC-4's sink-name shape from agent_board_consent_gating.test.sql, aimed at the exact edge class
549
+ -- this revision exists for — the send family, which is where #324's direct-body scan went wrong.
550
+ select is(
551
+ (select (select count(*) from _wec_closure cl join pg_proc rp on rp.oid = cl.root_oid
552
+ where rp.pronamespace = 'agent_board'::regnamespace
553
+ and rp.proname = 'send_direct_message' and cl.reached_name = 'post_message')::text
554
+ || '/' ||
555
+ (select count(*) from _wec_direct d join pg_proc p on p.oid = d.fn_oid
556
+ where p.proname = 'post_message' and d.relname = 'messages')::text),
557
+ '2/1',
558
+ '#325 instrument liveness: BOTH send_direct_message overloads reach post_message, and post_message writes messages — the delegation chain is real, so a closure that stopped resolving edges reds HERE instead of silently emptying the class');
559
+
560
+ -- ===========================================================================================
561
+ -- HALF A — every writer derives its actor FROM THE SESSION.
562
+ -- ===========================================================================================
563
+ -- FOUR shapes. Scoring against only `_resolved_actor_agent_id` and `current_agent_id` produced
564
+ -- three false "blocking adjudications" during planning, one flagged as a possible weakly-gated
565
+ -- destructive verb; all three were correctly gated by shapes the predicate did not know. A class
566
+ -- assertion is only as complete as its list of what COUNTS as satisfying it — the mirror image of
567
+ -- rule 1, and the same failure. WEC-6 is the catalog query that keeps that list honest.
568
+ --
569
+ -- `auth.uid()` is matched in the writer's OWN body, and that is the difference between an
570
+ -- assertion and a tautology: it lives in the `auth` schema so it forms no closure edge of its
571
+ -- own, but two agent_board functions call it, so "something in my closure mentions auth.uid()"
572
+ -- matches 59 of 59. Scored on the own body it matches exactly one — WEC-7 pins that.
573
+ -- WEC-5.
574
+ select is(
575
+ (select coalesce(array_agg(p.sig order by p.sig), array[]::text[])
576
+ from _wec_pop p
577
+ where not exists (
578
+ select 1 from _wec_closure cl
579
+ where cl.root_oid = p.oid
580
+ and cl.reached_name in ('_resolved_actor_agent_id', 'current_agent_id',
581
+ '_require_platform_admin_user_id'))
582
+ and lower(agent_board_test.stripped_body(p.oid, true)) !~ 'auth\s*\.\s*uid\s*\('),
583
+ array[]::text[],
584
+ '#325 HALF A: EVERY authenticated-executable writer derives its actor from the SESSION — see the header on why this spelling is fail-OPEN against a dead closure and which assertions cover that');
585
+
586
+ -- WEC-6. The derivation-shape set, from the catalog. Two of the three are also named by hand in
587
+ -- WEC-5, so a rename or a pattern drift would silently SHRINK what counts as satisfying Half A
588
+ -- and make it easier to pass. This makes that drift loud.
589
+ select is(
590
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
591
+ from pg_proc p
592
+ where p.pronamespace = 'agent_board'::regnamespace
593
+ and p.proname ~ 'current_agent_id|resolved_actor|require_platform_admin'),
594
+ array['_require_platform_admin_user_id', '_resolved_actor_agent_id', 'current_agent_id']::text[],
595
+ '#325 canary: the agent_board session-derivation shapes are exactly these three — a fourth, or a rename, fails here and names itself instead of quietly leaving Half A short');
596
+
597
+ -- WEC-7. The anti-vacuity pin for Half A's fourth shape.
598
+ select is(
599
+ (select coalesce(array_agg(p.sig order by p.sig), array[]::text[])
600
+ from _wec_pop p
601
+ where lower(agent_board_test.stripped_body(p.oid, true)) ~ 'auth\s*\.\s*uid\s*\('),
602
+ array['agent_board.self_enroll_human(text)']::text[],
603
+ '#325: exactly ONE writer derives from raw auth.uid() — self_enroll_human CREATES the identity a session would resolve to, so it cannot use the resolver; scored on the own body because the closure form matches all 59 and would make Half A vacuous');
604
+
605
+ -- ===========================================================================================
606
+ -- HALF B — every writer REFUSES SOMEBODY. Labelled honestly: it pins that a refusal exists on the
607
+ -- path, NOT that the refusal admits the right people. Only WEC-14..17 speak to that, for one
608
+ -- member.
609
+ -- ===========================================================================================
610
+ -- WEC-8. THE SESSION-DERIVATION-ONLY CATEGORY, and the resolver each member relies on, in ONE
611
+ -- assertion carrying both readings. These are the writers that reach NO shared authz predicate
612
+ -- and raise no 42501 of their own; the plan predicts the set is EMPTY and measured it has 16
613
+ -- members. Their authority check is not absent, it is the RESOLVER — which REFUSES an absent
614
+ -- session and a mismatched actor id with 42501.
615
+ --
616
+ -- IT IS ONE ASSERTION BECAUSE TWO WOULD NOT HAVE BEEN TWO PINS. A previous revision put a
617
+ -- separate "here are the 16" exact set beside this one; a review seat showed the pair set
618
+ -- STRICTLY SUBSUMES it — every membership change alters a pair, so the membership assertion
619
+ -- could never red alone and added no detection power. Merged rather than kept for symmetry, and
620
+ -- NOT repaired by restricting either one to the 16 literal names, which would substitute a hand
621
+ -- list for a derived set: rule 1's own anti-pattern. The pair array below still NAMES every
622
+ -- member, so a new ungated writer joins it and names itself.
623
+ --
624
+ -- What the pairs buy over bare membership: a member that LOSES its resolver renders
625
+ -- `(NO RAISING RESOLVER)` and reds, and a member that silently SWITCHES which resolver it trusts
626
+ -- reds too. Measured distinct from WEC-9 in both directions — mutation c reds this one and
627
+ -- leaves WEC-9 green; mutation d reds both.
628
+ --
629
+ -- `current_agent_id` is deliberately absent from the pair vocabulary: it RETURNS NULL rather than
630
+ -- raising (pinned by WEC-13), so counting it would let a writer whose only derivation is a
631
+ -- nullable lookup read as refusing.
632
+ select is( -- pgtap-allow-raise-checked
633
+ (select coalesce(array_agg(x.pair order by x.pair), array[]::text[])
634
+ from (select p.sig || ' -> ' ||
635
+ coalesce((select string_agg(distinct cl.reached_name, '+' order by cl.reached_name)
636
+ from _wec_closure cl
637
+ where cl.root_oid = p.oid
638
+ and cl.reached_name in ('_resolved_actor_agent_id',
639
+ '_require_platform_admin_user_id')),
640
+ '(NO RAISING RESOLVER)') as pair
641
+ from _wec_pop p
642
+ where not exists (
643
+ select 1 from _wec_closure cl
644
+ join pg_proc ap on ap.oid = cl.reached_oid
645
+ where cl.root_oid = p.oid
646
+ and ap.pronamespace = 'agent_board'::regnamespace
647
+ and ap.prorettype = 'boolean'::regtype
648
+ and (ap.proname ~ '^_(current_)?agent_(can|has)_'
649
+ or ap.proname ~ '^_grant_within_'
650
+ or ap.proname in ('_invite_permitted', '_membership_consent_present')))
651
+ and lower(agent_board_test.stripped_body(p.oid, true)) !~ (select pat from _wec_raise_pat)) x),
652
+ array[
653
+ 'agent_board.block_board_inviter(uuid,uuid,uuid,jsonb) -> _resolved_actor_agent_id',
654
+ 'agent_board.create_or_find_thread(uuid,text,text,text,jsonb) -> _resolved_actor_agent_id',
655
+ 'agent_board.create_thread(uuid,text,text,jsonb) -> _resolved_actor_agent_id',
656
+ 'agent_board.delete_callback_endpoint(uuid,uuid) -> _resolved_actor_agent_id',
657
+ 'agent_board.disable_callback_endpoint(uuid,uuid,text,jsonb) -> _resolved_actor_agent_id',
658
+ 'agent_board.issue_signing_key_challenge(uuid,text,interval) -> _resolved_actor_agent_id',
659
+ 'agent_board.provision_agent(text,text,jsonb,uuid) -> _require_platform_admin_user_id',
660
+ 'agent_board.register_callback_endpoint(uuid,text,jsonb) -> _resolved_actor_agent_id',
661
+ 'agent_board.revoke_agent(uuid,text) -> _require_platform_admin_user_id',
662
+ 'agent_board.rotate_callback_signing_key(uuid,uuid,jsonb) -> _resolved_actor_agent_id',
663
+ 'agent_board.subscribe_callback_endpoint(uuid,uuid,text,text,uuid,jsonb) -> _resolved_actor_agent_id',
664
+ 'agent_board.unblock_board_inviter(uuid,uuid,uuid) -> _resolved_actor_agent_id',
665
+ 'agent_board.unsubscribe_callback_endpoint(uuid,uuid,text,text,uuid,jsonb) -> _resolved_actor_agent_id',
666
+ 'agent_board.update_inbox_item_state(uuid,uuid,text,jsonb) -> _resolved_actor_agent_id',
667
+ 'agent_board.upsert_delegation_policy(uuid,text,jsonb,integer,interval,boolean) -> _resolved_actor_agent_id',
668
+ 'agent_board.verify_callback_endpoint(uuid,uuid,jsonb) -> _resolved_actor_agent_id'
669
+ ]::text[],
670
+ '#325 HALF B, part 1: exactly these 16 writers reach NO shared authz predicate and raise no 42501 of their own, and each NAMES the raising resolver it relies on — 14 the session resolver, 2 the platform-admin one; a new ungated writer joins this set, and a member that loses or switches its resolver reds here');
671
+
672
+ -- WEC-9. The union, and the only assertion here that is a claim about the WHOLE class rather than
673
+ -- about a category. Not spelled over the CLOSURE for 42501: that form is satisfied by 59 of 59
674
+ -- through the resolver alone, so it would restate Half A and no mutation could red it.
675
+ --
676
+ -- ITS DETECTION IS SUBSUMED BY WEC-8, and saying so is the point rather than a caveat. Any member
677
+ -- that lands in this violating set is by construction in WEC-8's category with the pair
678
+ -- `(NO RAISING RESOLVER)`, so WEC-9 cannot red without WEC-8 redding too. Measured across the
679
+ -- whole battery: no row reds 9 without 8, while `e` and `p1415` red 8 without 9.
680
+ --
681
+ -- It is KEPT anyway, and this is a deliberate exception to "a duplicate is not a second pin" —
682
+ -- the same reasoning that MERGED the old membership assertion into WEC-8 would delete this one
683
+ -- too. The difference is what the two assert. WEC-8 is a statement about sixteen named functions;
684
+ -- this is the statement of the file's HEADLINE PROPERTY over all 59, and a class-enforcement file
685
+ -- whose class-wide claim exists only as an inference from a category listing is the "comment
686
+ -- claims what no assertion does" shape. So it is retained as a STATEMENT with no independent
687
+ -- detection power, labelled as such, rather than presented as a third pin.
688
+ select is(
689
+ (select coalesce(array_agg(p.sig order by p.sig), array[]::text[])
690
+ from _wec_pop p
691
+ where not exists (
692
+ select 1 from _wec_closure cl
693
+ join pg_proc ap on ap.oid = cl.reached_oid
694
+ where cl.root_oid = p.oid
695
+ and ap.pronamespace = 'agent_board'::regnamespace
696
+ and ap.prorettype = 'boolean'::regtype
697
+ and (ap.proname ~ '^_(current_)?agent_(can|has)_'
698
+ or ap.proname ~ '^_grant_within_'
699
+ or ap.proname in ('_invite_permitted', '_membership_consent_present')))
700
+ and lower(agent_board_test.stripped_body(p.oid, true)) !~ (select pat from _wec_raise_pat)
701
+ and not exists (
702
+ select 1 from _wec_closure cl
703
+ where cl.root_oid = p.oid
704
+ and cl.reached_name in ('_resolved_actor_agent_id',
705
+ '_require_platform_admin_user_id'))),
706
+ array[]::text[],
707
+ '#325 HALF B, part 2: EVERY writer reaches a shared predicate, OR raises 42501 in its own text, OR reaches a raising resolver — it pins that a refusal exists on the path, NOT that the refusal admits the right people');
708
+
709
+ -- WEC-10. The authorization-predicate set, from the catalog, shared with #310 deliberately —
710
+ -- two files disagreeing about what counts as gated is worse than either being wrong.
711
+ -- `_is_service_role` is excluded BY PATTERN, not by name: it discriminates a role, it does not
712
+ -- authorize, and counting it would let a function that only checks it read as gated.
713
+ select is(
714
+ (select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
715
+ from pg_proc p
716
+ where p.pronamespace = 'agent_board'::regnamespace
717
+ and p.prorettype = 'boolean'::regtype
718
+ and (p.proname ~ '^_(current_)?agent_(can|has)_'
719
+ or p.proname ~ '^_grant_within_'
720
+ or p.proname in ('_invite_permitted', '_membership_consent_present'))),
721
+ array['_agent_can_access_thread', '_agent_has_board_access', '_current_agent_can_access_task',
722
+ '_current_agent_can_access_thread', '_current_agent_can_mutate_task',
723
+ '_current_agent_can_post_to_thread', '_current_agent_can_send_to_board',
724
+ '_current_agent_has_board_access', '_grant_within_ceiling', '_invite_permitted',
725
+ '_membership_consent_present']::text[],
726
+ '#325 canary: the shared authorization-predicate set is exactly these eleven — over the 59 this arm is LOAD-BEARING (5 writers reach a predicate and raise no 42501 of their own), which was not true of the narrower class');
727
+
728
+ -- WEC-11 and WEC-12. The two facts WEC-8 rests on, asserted rather than assumed. Without them a
729
+ -- resolver that stopped raising would leave WEC-8 green while its meaning evaporated: the 16
730
+ -- would still REACH it, and it would no longer refuse anybody. Measured — mutation k does exactly
731
+ -- that and WEC-8 stays GREEN, so these are the pins, not a restatement.
732
+ -- keep_literals = true is mandatory: `42501` is a string literal in `using errcode = '42501'`.
733
+ -- WEC-11.
734
+ select ok(
735
+ lower(agent_board_test.stripped_body(
736
+ (select oid from pg_proc where pronamespace = 'agent_board'::regnamespace
737
+ and proname = '_resolved_actor_agent_id'), true)) ~ '42501',
738
+ '#325: _resolved_actor_agent_id RAISES 42501 — the fact WEC-8 rests on, so the exemption category cannot go hollow while its assertion stays green');
739
+
740
+ -- WEC-12.
741
+ select ok(
742
+ lower(agent_board_test.stripped_body(
743
+ (select oid from pg_proc where pronamespace = 'agent_board'::regnamespace
744
+ and proname = '_require_platform_admin_user_id'), true)) ~ '42501',
745
+ '#325: _require_platform_admin_user_id RAISES 42501 — the same fact for the platform-admin arm of WEC-8');
746
+
747
+ -- WEC-13. The negative half of the pair, and the reason `current_agent_id` is excluded from
748
+ -- "raising". If it ever starts raising, this reds and the exclusion should be revisited — the
749
+ -- exclusion is currently the STRICTER choice, so a failure here means WEC-8 is being harder than
750
+ -- it needs to be, not that it has gone soft.
751
+ select ok(
752
+ lower(agent_board_test.stripped_body(
753
+ (select oid from pg_proc where pronamespace = 'agent_board'::regnamespace
754
+ and proname = 'current_agent_id'), true)) !~ (select pat from _wec_raise_pat),
755
+ '#325: current_agent_id does NOT raise — it returns NULL on an unresolved principal, which is why it is not counted as a refusal in WEC-8');
756
+
757
+ -- ===========================================================================================
758
+ -- THE BEHAVIOURAL PROBE. WEC-5..10 are SOURCE-SHAPE claims: they see that a refusal is reachable,
759
+ -- never that it fires. These three make the claim evidence, for one representative of the 16.
760
+ -- `create_thread` is chosen because it is the WEAKEST case in the class — nothing but the
761
+ -- resolver stands between a caller and a `threads` row — so if the resolver refuses here, the
762
+ -- category WEC-8 describes is real.
763
+ -- ===========================================================================================
764
+ create temp table _wec_agent (
765
+ label text primary key,
766
+ agent_id uuid,
767
+ user_id uuid
768
+ ) on commit drop;
769
+
770
+ create temp table _wec_thread (
771
+ label text primary key,
772
+ thread_id uuid,
773
+ subject text
774
+ ) on commit drop;
775
+
776
+ do $$
777
+ declare
778
+ v_sfx text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
779
+ v_u_a uuid;
780
+ v_u_b uuid;
781
+ v_a uuid := gen_random_uuid();
782
+ v_b uuid := gen_random_uuid();
783
+ begin
784
+ v_u_a := public.ensure_github_user('wec-a-' || v_sfx, 'wec-a-' || v_sfx || '@example.test', 'wec-a-' || v_sfx, 'WEC A');
785
+ v_u_b := public.ensure_github_user('wec-b-' || v_sfx, 'wec-b-' || v_sfx || '@example.test', 'wec-b-' || v_sfx, 'WEC B');
786
+
787
+ insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
788
+ values (v_a, 'agent', 'wec-a-' || v_sfx, 'WEC A', 'active', v_u_a, v_u_a),
789
+ (v_b, 'agent', 'wec-b-' || v_sfx, 'WEC B', 'active', v_u_b, v_u_b);
790
+
791
+ insert into _wec_agent values ('a', v_a, v_u_a), ('b', v_b, v_u_b);
792
+ end
793
+ $$;
794
+
795
+ select set_config('request.jwt.claim.role', 'authenticated', true);
796
+ select set_config('request.jwt.claim.sub', (select user_id from _wec_agent where label = 'a')::text, true);
797
+
798
+ -- The ALLOW side runs first and is not decoration: a gate that refused EVERYBODY would satisfy
799
+ -- the deny assertion below on its own, and the file would claim enforcement while describing a
800
+ -- dead RPC. Measured — mutation p1415 makes create_thread refuse unconditionally and the deny
801
+ -- assertion stays GREEN; only this pair catches it. The capture table exists BEFORE the call
802
+ -- (#113) so a raise surfaces as a named `not ok` instead of aborting with no TAP output.
803
+ -- WEC-14.
804
+ -- p_created_by_agent_id is passed as NULL ON PURPOSE, and that is an upgrade a review seat asked
805
+ -- for. Passing A's own id under A's session makes supplied == resolved, so the follow-up could
806
+ -- not tell "the RESOLVER's return was written" from "the ARGUMENT was written" — it would have
807
+ -- read as an actor-binding proof while proving only that the two agreed. With NULL supplied there
808
+ -- is no argument to write, so the author column can only have come from the session. This is the
809
+ -- one place in the file that speaks to limit 3 (reachability is not data flow), and it now
810
+ -- speaks to it for real, for one member.
811
+ select lives_ok(
812
+ $$ insert into _wec_thread
813
+ select 'self', t.thread_id, t.subject
814
+ from agent_board.create_thread(null::uuid, 'WEC self thread') t $$,
815
+ '#325 allow side: an authenticated agent creates a thread with NO supplied actor id [call succeeds]');
816
+
817
+ -- WEC-15.
818
+ select is(
819
+ (select created_by_agent_id from agent_board.threads
820
+ where id = (select thread_id from _wec_thread where label = 'self')),
821
+ (select agent_id from _wec_agent where label = 'a'),
822
+ '#325 allow side / ACTOR BINDING: the row was written and its author is agent A, which with a NULL supplied id can only have come from the SESSION — the write took effect, so the deny below is a refusal rather than a broken RPC');
823
+
824
+ -- THE DENY SIDE USES THE NEAREST PRINCIPAL, not a stranger, and #310 is where that lesson was
825
+ -- bought: a stranger is refused by any gate that checks anything at all, so it cannot tell
826
+ -- "binds the actor to the session" apart from "checks that some agent exists". Agent B is a REAL,
827
+ -- ACTIVE agent with its own live session identity. The only thing wrong with the call is that A
828
+ -- is making it.
829
+ -- WEC-16.
830
+ select throws_ok(
831
+ $$ select * from agent_board.create_thread(
832
+ (select agent_id from _wec_agent where label = 'b'), 'WEC impersonation attempt') $$,
833
+ '42501',
834
+ 'p_created_by_agent_id must match current_agent_id()',
835
+ '#325 deny side: agent A cannot create a thread AS agent B — the resolver refuses a real, active, NEAREST principal, which is the behaviour WEC-8 asserts only structurally');
836
+
837
+ select set_config('request.jwt.claim.role', 'service_role', true);
838
+ select set_config('request.jwt.claim.sub', '', true);
839
+
840
+ -- ===========================================================================================
841
+ -- THE STRUCTURAL BOUND — three arms, none of which subsumes the others (mutations i, j).
842
+ -- ===========================================================================================
843
+ create temp table _wec_class_tables on commit drop as
844
+ select distinct d.relname, ('agent_board.' || quote_ident(d.relname))::regclass as reloid
845
+ from _wec_direct d
846
+ where exists (select 1 from _wec_closure cl join _wec_pop p on p.oid = cl.root_oid
847
+ where cl.reached_oid = d.fn_oid);
848
+
849
+ -- WEC-17. The arm that matters for THIS class, and it HOLDS. Its MESSAGE is a correction: the
850
+ -- earlier wording said this is "what makes the body scan a complete surface", and that conclusion
851
+ -- is false — a write can go through a SECURITY DEFINER wrapper whose own body contains no DML,
852
+ -- which is the entire defect this revision fixes. What the absence of a direct grant establishes
853
+ -- is narrower and is all that is claimed now: authenticated and anon cannot write these relations
854
+ -- WITHOUT going through some agent_board function. Which functions those are is WEC-3's job.
855
+ select is(
856
+ (select coalesce(array_agg(r || ':' || pv || ':' || t.relname
857
+ order by r || ':' || pv || ':' || t.relname), array[]::text[])
858
+ from _wec_class_tables t
859
+ cross join unnest(array['authenticated', 'anon']) as r
860
+ cross join unnest(array['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE']) as pv
861
+ where has_table_privilege(r, t.reloid, pv)),
862
+ array[]::text[],
863
+ '#325: NEITHER authenticated NOR anon holds a direct write privilege on ANY of the 37 class relations — so for those two roles every write goes through some agent_board function; WEC-3 is what says which');
864
+
865
+ -- WEC-18. The arm that DOES NOT hold, pinned as an exact set rather than omitted. service_role
866
+ -- writes thirteen of these relations directly, with no function and no body — the structural
867
+ -- bound simply does not cover that route. Spelled per relation:privilege so a DELETE appearing on
868
+ -- a relation that had only INSERT names itself.
869
+ select is(
870
+ (select coalesce(array_agg(pv || ':' || t.relname order by pv || ':' || t.relname), array[]::text[])
871
+ from _wec_class_tables t
872
+ cross join unnest(array['INSERT', 'UPDATE', 'DELETE', 'TRUNCATE']) as pv
873
+ where has_table_privilege('service_role', t.reloid, pv)),
874
+ array[
875
+ 'DELETE:board_invite_blocks',
876
+ 'DELETE:fleet_harness_registrations',
877
+ 'DELETE:fleet_machine_agents',
878
+ 'DELETE:fleet_machines',
879
+ 'DELETE:fleet_receive_policies',
880
+ 'DELETE:fleet_repo_scopes',
881
+ 'DELETE:fleet_revocation_events',
882
+ 'DELETE:fleet_trust_domains',
883
+ 'INSERT:board_authority_ceilings',
884
+ 'INSERT:board_invite_blocks',
885
+ 'INSERT:board_invite_redemptions',
886
+ 'INSERT:board_invite_tokens',
887
+ 'INSERT:board_membership_events',
888
+ 'INSERT:fleet_harness_registrations',
889
+ 'INSERT:fleet_machine_agents',
890
+ 'INSERT:fleet_machines',
891
+ 'INSERT:fleet_receive_policies',
892
+ 'INSERT:fleet_repo_scopes',
893
+ 'INSERT:fleet_revocation_events',
894
+ 'INSERT:fleet_trust_domains',
895
+ 'INSERT:send_authz_telemetry',
896
+ 'UPDATE:board_authority_ceilings',
897
+ 'UPDATE:board_invite_redemptions',
898
+ 'UPDATE:board_invite_tokens',
899
+ 'UPDATE:fleet_harness_registrations',
900
+ 'UPDATE:fleet_machine_agents',
901
+ 'UPDATE:fleet_machines',
902
+ 'UPDATE:fleet_receive_policies',
903
+ 'UPDATE:fleet_repo_scopes',
904
+ 'UPDATE:fleet_revocation_events',
905
+ 'UPDATE:fleet_trust_domains'
906
+ ]::text[],
907
+ '#325: service_role holds direct DML on THIRTEEN of the 37 class relations — the plan asserts this bound holds and it DOES NOT; pinned as an exact set so the hole is bounded and a widening names itself');
908
+
909
+ -- WEC-19. The rule-1-correct arm. WEC-17 and WEC-18 are hand lists of three role names between
910
+ -- them, so an INSERT granted to a brand-new role self-names in NEITHER. This reads the ACL itself.
911
+ -- aclexplode over pg_class.relacl rather than information_schema.role_table_grants: that view is
912
+ -- filtered to roles the CURRENT user can see, and a privilege-dependent visibility filter has no
913
+ -- place inside a completeness claim (#302).
914
+ select is(
915
+ (select coalesce(array_agg(distinct coalesce(g.rolname, 'PUBLIC') || ':' || a.privilege_type
916
+ || ':' || t.relname
917
+ order by coalesce(g.rolname, 'PUBLIC') || ':' || a.privilege_type
918
+ || ':' || t.relname), array[]::text[])
919
+ from _wec_class_tables t
920
+ join pg_class c on c.oid = t.reloid
921
+ cross join aclexplode(c.relacl) a
922
+ left join pg_roles g on g.oid = a.grantee
923
+ where a.privilege_type <> 'SELECT'
924
+ and coalesce(g.rolname, 'PUBLIC') not in ('postgres', 'service_role')),
925
+ array[]::text[],
926
+ '#325: NO grantee other than the owner and service_role holds a non-SELECT privilege on any class relation — a grant to PUBLIC or to a role no assertion here names fails HERE and names itself');
927
+
928
+ -- WEC-20. The owner exclusion from WEC-19, asserted positively. An exclusion nobody checks is a
929
+ -- place to hide a grantee, which is this repo's named soft spot.
930
+ -- The OWNER claim is checked as ownership, over EVERY class relation, not inferred from two
931
+ -- privileges on one table. The earlier form asserted INSERT and DELETE on `threads` and called
932
+ -- that "is the table OWNER"; there was no `relowner` check anywhere in the file, so the label
933
+ -- claimed more than the query. Both halves are asserted now: postgres owns all 37, and holds the
934
+ -- write privileges the exclusion assumes.
935
+ select ok(
936
+ (select count(*) from _wec_class_tables t join pg_class c on c.oid = t.reloid
937
+ where c.relowner <> 'postgres'::regrole) = 0
938
+ and has_table_privilege('postgres', 'agent_board.threads', 'INSERT')
939
+ and has_table_privilege('postgres', 'agent_board.threads', 'DELETE'),
940
+ '#325: postgres OWNS every one of the 37 class relations (pg_class.relowner, not inferred from a privilege) and holds writes on them — so the WEC-19 exclusion is a fact about ownership, not a carve-out');
941
+
942
+ -- WEC-21. The over-revoke guard. A file that only ever asserts absence would call a schema with
943
+ -- every privilege revoked a success.
944
+ select ok(
945
+ has_table_privilege('authenticated', 'agent_board.threads', 'SELECT')
946
+ and has_table_privilege('service_role', 'agent_board.threads', 'SELECT'),
947
+ '#325: authenticated and service_role DO hold SELECT on a class relation — WEC-17 is a write revoke, not a dead schema');
948
+
949
+ -- ===========================================================================================
950
+ -- FUNCTION-LESS WRITE PATHS — referential actions that write a class relation with no privilege,
951
+ -- no function and no body. Nothing above gates them; these two BOUND them so growth is visible.
952
+ -- ===========================================================================================
953
+ -- `confdeltype in ('c','n','d')` — CASCADE, SET NULL and SET DEFAULT — and the two non-cascade
954
+ -- arms are a correction. An earlier revision filtered `= 'c'` while limit 5 claimed the
955
+ -- function-less path was "counted across the whole class"; SET NULL performs an ungated UPDATE of
956
+ -- a class relation and 14 such edges exist, including `agents_parent_agent_id_fkey`,
957
+ -- `agent_signing_keys_revoked_by_agent_id_fkey` and
958
+ -- `agent_credentials_replaced_by_credential_id_fkey`. The claim covered less than it said.
959
+ -- RESTRICT and NO ACTION (3 and 25 edges) are deliberately excluded: they REFUSE the parent
960
+ -- delete rather than writing the child. `confupdtype` is NO ACTION on all 116 foreign keys, so
961
+ -- the update-side of the same class is empty today and is not pinned; it would need its own arm
962
+ -- the day a referenced key becomes mutable.
963
+ --
964
+ -- Parents are rendered with their REAL schema, not a hardcoded `agent_board.` prefix, because one
965
+ -- of them is NOT in agent_board: `auth.users`. Deleting a Supabase auth user SET NULLs
966
+ -- `agents.auth_user_id`, which is a cross-schema function-less write into a class relation. The
967
+ -- hardcoded form mislabelled it `agent_board.users`, a relation that does not exist.
968
+ -- WEC-22. The parent relations, as an exact set. A NEW parent whose deletion cascades into a
969
+ -- class relation names itself here.
970
+ select is(
971
+ (select coalesce(array_agg(distinct pn.nspname || '.' || pc.relname
972
+ order by pn.nspname || '.' || pc.relname), array[]::text[])
973
+ from _wec_class_tables t
974
+ join pg_constraint con on con.conrelid = t.reloid
975
+ join pg_class pc on pc.oid = con.confrelid
976
+ join pg_namespace pn on pn.oid = pc.relnamespace
977
+ where con.contype = 'f' and con.confdeltype in ('c', 'n', 'd')),
978
+ array[
979
+ 'agent_board.agent_credentials',
980
+ 'agent_board.agent_signing_keys',
981
+ 'agent_board.agents',
982
+ 'agent_board.board_invite_tokens',
983
+ 'agent_board.boards',
984
+ 'agent_board.callback_endpoints',
985
+ 'agent_board.callback_subscriptions',
986
+ 'agent_board.delegations',
987
+ 'agent_board.delivery_recipients',
988
+ 'agent_board.fleet_harness_registrations',
989
+ 'agent_board.fleet_machines',
990
+ 'agent_board.fleet_repo_scopes',
991
+ 'agent_board.fleet_trust_domains',
992
+ 'agent_board.inbox_items',
993
+ 'agent_board.messages',
994
+ 'agent_board.task_events',
995
+ 'agent_board.tasks',
996
+ 'agent_board.threads',
997
+ 'auth.users'
998
+ ]::text[],
999
+ '#325: deleting a row in one of these 19 parents CASCADES or SET NULLs into a class relation — a write path with no privilege check and no function body, so no assertion in this file gates it; one parent is auth.users, i.e. outside agent_board entirely');
1000
+
1001
+ -- WEC-23. The edge count. This IS a bare count and the hazard is stated rather than left for a
1002
+ -- reviewer: the routine response to a red count is to bump it, which absorbs a new cascade edge
1003
+ -- silently. It is safe here ONLY because WEC-22 names the parents independently — a new edge from
1004
+ -- a NEW parent reds both; a new edge from an EXISTING parent reds only this one and must be read
1005
+ -- as a real change to the ungated surface, never bumped without checking it.
1006
+ select is(
1007
+ (select count(*)::text
1008
+ from _wec_class_tables t
1009
+ join pg_constraint con on con.conrelid = t.reloid
1010
+ where con.contype = 'f' and con.confdeltype in ('c', 'n', 'd')),
1011
+ '88',
1012
+ '#325: 88 writing referential actions (74 CASCADE + 14 SET NULL) reach the 37 class relations — bump this number only after confirming the new edge is intended; see WEC-22, which names parents independently');
1013
+
1014
+ select * from finish();
1015
+
1016
+ rollback;