@consiliency/agent-board-schema 1.35.0 → 1.40.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.
- package/fixtures/already-applied.json +16 -0
- package/migrations/20260917000100_agent_board_invite_no_downgrade.sql +398 -0
- package/migrations/20260918000100_agent_board_signed_write_enforced_repoint.sql +139 -0
- package/migrations/20260919000100_agent_board_signed_write_model_key.sql +107 -0
- package/migrations/20260920000100_agent_board_realtime_credential_claim.sql +119 -0
- package/package.json +1 -1
- package/schema-manifest.json +39 -3
- package/src/deployment-drift-script.test.mjs +424 -0
- package/src/deployment-drift.test.mjs +301 -0
- package/src/tooling.test.mjs +49 -2
- package/tests/agent_board_bundle_endpoint_compat.test.sql +73 -18
- package/tests/agent_board_consent_substrate.test.sql +8 -1
- package/tests/agent_board_invite_no_downgrade.test.sql +689 -0
- package/tests/agent_board_realtime_credential_claim.test.sql +301 -0
- package/tests/agent_board_signed_write_enforced.test.sql +366 -0
- package/tests/agent_board_source_strip_helper.test.sql +100 -1
- package/tests/agent_board_write_enforcement_class.test.sql +1016 -0
- package/tests/fixtures/isolation-substrate.sql +1 -1
- package/tests/fixtures/test-prereqs.sql +22 -0
|
@@ -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;
|