@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,366 @@
|
|
|
1
|
+
-- #310 — `signed_write_enforced` must describe the model this project actually ships.
|
|
2
|
+
--
|
|
3
|
+
-- The flag reported `false` on a system that was enforcing, because its predicate still encoded
|
|
4
|
+
-- the abandoned "authenticated cannot execute the send RPCs" model. `20260918000100` re-points it
|
|
5
|
+
-- at what enforcement means now. This file pins BOTH halves of that meaning, and the split is
|
|
6
|
+
-- deliberate rather than convenient:
|
|
7
|
+
--
|
|
8
|
+
-- * the GRANT half is in the flag, because privileges are catalog facts a shipped function can
|
|
9
|
+
-- read soundly at runtime;
|
|
10
|
+
-- * the BODY half is here, because deciding "this writer derives its actor from the session and
|
|
11
|
+
-- gates" means reading function source, and a shipped runtime scan of `prosrc` is satisfied by
|
|
12
|
+
-- a COMMENT (#196/#199). The strip helpers that make such a scan sound are test-only, and
|
|
13
|
+
-- shipping a second copy into `agent_board` is the two-copies-drift failure #199 names.
|
|
14
|
+
--
|
|
15
|
+
-- CONSEQUENCE, with the qualifier that makes it true. A new ungated writer THAT IS THE
|
|
16
|
+
-- COUNTERPART OF A SIGNED GATEWAY turns an assertion in this file red rather than the flag. A new
|
|
17
|
+
-- ungated writer with NO signed twin turns NEITHER red.
|
|
18
|
+
--
|
|
19
|
+
-- That is by construction, not by accident: every class-scoping predicate below bounds the class
|
|
20
|
+
-- with `signed\_%` and derives the counterpart by stripping the prefix, so a writer without a
|
|
21
|
+
-- signed twin is outside the class before any assertion runs. Measured, because a claim about
|
|
22
|
+
-- one's own blind spot deserves evidence too: an `authenticated`-executable SECURITY DEFINER
|
|
23
|
+
-- writer that derives nothing from the session left the flag `true` and all 19 assertions green.
|
|
24
|
+
--
|
|
25
|
+
-- The repo has many such writers already -- `create_board`, `join_board`, `invite_board_room`-
|
|
26
|
+
-- style admission RPCs, `acquire_claim`, `delegate_task` -- none of which has a signed gateway.
|
|
27
|
+
-- This file makes NO claim about them. Closing that properly needs an EFFECT-derived class
|
|
28
|
+
-- ("every authenticated-executable function that writes a message/task/authority row") on the
|
|
29
|
+
-- shape `agent_board_consent_gating.test.sql` EC-4 already uses, which is a bigger class than
|
|
30
|
+
-- #310's subject; filed as a follow-up rather than smuggled in here.
|
|
31
|
+
--
|
|
32
|
+
-- An earlier draft of this note said "a new UNGATED writer" flatly. A review seat read the
|
|
33
|
+
-- scoping predicates and showed the claim was broader than the class. Recording that here rather
|
|
34
|
+
-- than only fixing the wording, because the unqualified sentence is the kind that gets copied.
|
|
35
|
+
--
|
|
36
|
+
-- THE CALL CLOSURE IS LOAD-BEARING, not a flourish. Only 3 of the 7 unsigned counterparts name
|
|
37
|
+
-- `_resolved_actor_agent_id` in their own body; the send family DELEGATES to `post_message`. A
|
|
38
|
+
-- direct body scan would report four of seven ungated and be WRONG.
|
|
39
|
+
--
|
|
40
|
+
-- WHAT MAKES A CLOSURE ASSERTION FAIL-CLOSED is where its POPULATION comes from, not which of the
|
|
41
|
+
-- two spellings it uses -- an earlier version of this note said the spelling was the thing, and
|
|
42
|
+
-- the rewrite below falsifies that. "The set that does NOT reach X is empty" is fail-OPEN when the
|
|
43
|
+
-- population is drawn from the closure itself, because an empty closure empties the population
|
|
44
|
+
-- too. It is fail-CLOSED when the population comes from `pg_proc`, because then an empty closure
|
|
45
|
+
-- puts EVERY member in the non-reaching set. The assertions below take the population from
|
|
46
|
+
-- pg_proc and pin its size with a canary, so both directions are covered. Proven by shimming the
|
|
47
|
+
-- helper to return zero rows, not assumed.
|
|
48
|
+
|
|
49
|
+
begin;
|
|
50
|
+
select plan(22);
|
|
51
|
+
|
|
52
|
+
-- ---------------------------------------------------------------------------
|
|
53
|
+
-- THE DEFECT: the flag now describes a system that is enforcing.
|
|
54
|
+
-- ---------------------------------------------------------------------------
|
|
55
|
+
select is(
|
|
56
|
+
(agent_board.get_bundle_compatibility()->>'signed_write_enforced'),
|
|
57
|
+
'true',
|
|
58
|
+
'#310: signed_write_enforced is TRUE on a system that is enforcing — the whole point of the issue');
|
|
59
|
+
|
|
60
|
+
select is(
|
|
61
|
+
(select count(*)::text from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
62
|
+
where n.nspname = 'agent_board' and p.proname = 'signed_send_direct_message'),
|
|
63
|
+
'1',
|
|
64
|
+
'#310 anchor: the signed gateway exists — without it the grant clause below is vacuously true, so this is what stops "enforced" meaning "nothing to enforce"');
|
|
65
|
+
|
|
66
|
+
-- ---------------------------------------------------------------------------
|
|
67
|
+
-- THE GRANT HALF. The old predicate carried a FOUR-name hand list; there are SEVEN
|
|
68
|
+
-- gateways, which is rule 1's failure in its plainest form. This set comes from pg_proc.
|
|
69
|
+
-- ---------------------------------------------------------------------------
|
|
70
|
+
select is(
|
|
71
|
+
(select coalesce(array_agg(distinct p.proname::text order by p.proname::text), array[]::text[])
|
|
72
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
73
|
+
where n.nspname = 'agent_board' and p.proname like 'signed\_%'),
|
|
74
|
+
array['signed_append_task_event', 'signed_create_tasking_delivery', 'signed_grant_board_role',
|
|
75
|
+
'signed_post_board_message', 'signed_send_direct_message', 'signed_send_multicast_message',
|
|
76
|
+
'signed_set_board_authority_ceiling']::text[],
|
|
77
|
+
'#310: there are SEVEN signed gateways — the retired predicate named four, and a new gateway fails here NAMING itself');
|
|
78
|
+
|
|
79
|
+
select is(
|
|
80
|
+
(select coalesce(array_agg(r.rolname || ':' || p.proname order by r.rolname, p.proname), array[]::text[])
|
|
81
|
+
from pg_proc p
|
|
82
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
83
|
+
cross join (values ('authenticated'), ('anon')) as r(rolname)
|
|
84
|
+
where n.nspname = 'agent_board' and p.proname like 'signed\_%'
|
|
85
|
+
and has_function_privilege(r.rolname, p.oid, 'EXECUTE')),
|
|
86
|
+
array[]::text[],
|
|
87
|
+
'#310: no signed_* gateway is EXECUTE-able by authenticated or anon — via has_function_privilege, which resolves inherited and PUBLIC-default grants that role_routine_grants cannot see');
|
|
88
|
+
|
|
89
|
+
-- The positive complement: the operator path must EXIST, or "nobody can call it" would satisfy
|
|
90
|
+
-- the assertion above while meaning the gateway is dead rather than protected.
|
|
91
|
+
select is(
|
|
92
|
+
(select coalesce(array_agg(distinct p.proname::text order by p.proname::text), array[]::text[])
|
|
93
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
94
|
+
where n.nspname = 'agent_board' and p.proname like 'signed\_%'
|
|
95
|
+
and has_function_privilege('service_role', p.oid, 'EXECUTE')),
|
|
96
|
+
array['signed_append_task_event', 'signed_create_tasking_delivery', 'signed_grant_board_role',
|
|
97
|
+
'signed_post_board_message', 'signed_send_direct_message', 'signed_send_multicast_message',
|
|
98
|
+
'signed_set_board_authority_ceiling']::text[],
|
|
99
|
+
'#310: every signed_* IS executable by service_role — the operator path is protected, not dead');
|
|
100
|
+
|
|
101
|
+
-- ---------------------------------------------------------------------------
|
|
102
|
+
-- THE PAIRING, by NAME rather than by a list: each `signed_X` has an unsigned `X`. This is what
|
|
103
|
+
-- makes "the enforcement class" catalog-derived — the class is not "the send RPCs someone
|
|
104
|
+
-- remembered", it is the unsigned counterpart of every gateway that exists.
|
|
105
|
+
-- ---------------------------------------------------------------------------
|
|
106
|
+
-- Spelled as the MISSING set, not the present set. The present-set form was filtered by
|
|
107
|
+
-- `where exists (counterpart)`, so a gateway with NO counterpart was filtered OUT of its own
|
|
108
|
+
-- assertion and the aggregate still matched -- the description claimed "a gateway without one
|
|
109
|
+
-- fails here" and the author's own mutation table refuted it: an orphan gateway reddened
|
|
110
|
+
-- other assertions and never this one. What that form actually detected was counterpart
|
|
111
|
+
-- REMOVAL. This form detects both, and names the orphan.
|
|
112
|
+
select is(
|
|
113
|
+
(select coalesce(array_agg(s.counterpart order by s.counterpart), array[]::text[])
|
|
114
|
+
from (select regexp_replace(p.proname, '^signed_', '') as counterpart
|
|
115
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
116
|
+
where n.nspname = 'agent_board' and p.proname like 'signed\_%') s
|
|
117
|
+
where not exists (select 1 from pg_proc p2 join pg_namespace n2 on n2.oid = p2.pronamespace
|
|
118
|
+
where n2.nspname = 'agent_board' and p2.proname = s.counterpart)),
|
|
119
|
+
array[]::text[],
|
|
120
|
+
'#310: every signed gateway HAS an unsigned counterpart — a gateway whose counterpart is missing or was never created fails here and names itself');
|
|
121
|
+
|
|
122
|
+
select is(
|
|
123
|
+
(select coalesce(array_agg(distinct s.counterpart order by s.counterpart), array[]::text[])
|
|
124
|
+
from (select regexp_replace(p.proname, '^signed_', '') as counterpart
|
|
125
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
126
|
+
where n.nspname = 'agent_board' and p.proname like 'signed\_%') s
|
|
127
|
+
where exists (select 1 from pg_proc p2 join pg_namespace n2 on n2.oid = p2.pronamespace
|
|
128
|
+
where n2.nspname = 'agent_board' and p2.proname = s.counterpart
|
|
129
|
+
and has_function_privilege('authenticated', p2.oid, 'EXECUTE'))),
|
|
130
|
+
array['append_task_event', 'create_tasking_delivery', 'grant_board_role', 'post_board_message',
|
|
131
|
+
'send_direct_message', 'send_multicast_message', 'set_board_authority_ceiling']::text[],
|
|
132
|
+
'#310: every unsigned counterpart IS authenticated-executable — that is the shipped model (the gate is inside the function), and this pins it rather than leaving it implied');
|
|
133
|
+
|
|
134
|
+
-- ---------------------------------------------------------------------------
|
|
135
|
+
-- THE BODY HALF, over the CALL CLOSURE.
|
|
136
|
+
-- ---------------------------------------------------------------------------
|
|
137
|
+
-- PER-OID, not per-name, and that is a correction rather than a refinement. An earlier draft
|
|
138
|
+
-- aggregated `distinct root_name`, so adding an authenticated, UNGATED OVERLOAD of an existing
|
|
139
|
+
-- counterpart left every expected name present -- another overload was still gated and supplied
|
|
140
|
+
-- the name. Verified: the whole file stayed green with such an overload installed. Overloads are
|
|
141
|
+
-- separate functions with separate grants, so the population is separate functions too.
|
|
142
|
+
--
|
|
143
|
+
-- Spelled as "the set that does NOT reach it is empty", which is fail-CLOSED here precisely
|
|
144
|
+
-- because the population comes from pg_proc and not from the closure: if the closure helper ever
|
|
145
|
+
-- returns nothing, every member lands in the non-reaching set and this goes red. The canary
|
|
146
|
+
-- below pins the population size so the set cannot go empty by the population going empty.
|
|
147
|
+
--
|
|
148
|
+
-- WHAT "REACHES" PROVES, and what it does not. The closure establishes textual REACHABILITY,
|
|
149
|
+
-- not data flow: a function could call the resolver and ignore the result, or call it on a
|
|
150
|
+
-- branch that never executes. Two things narrow that gap without closing it. The resolver is
|
|
151
|
+
-- SIDE-EFFECTFUL on the live path -- for `authenticated` it raises 42501 on a missing session or
|
|
152
|
+
-- an id mismatch -- so even a result-ignoring call enforces session PRESENCE; and the
|
|
153
|
+
-- drop-the-resolver mutation pins regressions in the existing members. What it does NOT enforce
|
|
154
|
+
-- is actor BINDING: a function could resolve an actor and then write a caller-supplied id
|
|
155
|
+
-- anyway. For the eleven current overloads the stronger property holds by reading -- the send
|
|
156
|
+
-- family delegates to post_message, and set_board_authority_ceiling feeds v_actor into its owner
|
|
157
|
+
-- gate on every path -- but that is a reading, not an assertion, and it is stated as one.
|
|
158
|
+
select is(
|
|
159
|
+
(select coalesce(array_agg(pop.oid::regprocedure::text order by pop.oid::regprocedure::text), array[]::text[])
|
|
160
|
+
from (select p.oid
|
|
161
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
162
|
+
where n.nspname = 'agent_board'
|
|
163
|
+
and p.proname in (select regexp_replace(sp.proname, '^signed_', '')
|
|
164
|
+
from pg_proc sp join pg_namespace sn on sn.oid = sp.pronamespace
|
|
165
|
+
where sn.nspname = 'agent_board' and sp.proname like 'signed\_%')) pop
|
|
166
|
+
where not exists (select 1 from agent_board_test.fn_call_closure() cl
|
|
167
|
+
where cl.root_oid = pop.oid and cl.reached_name = '_resolved_actor_agent_id')),
|
|
168
|
+
array[]::text[],
|
|
169
|
+
'#310: EVERY unsigned counterpart OVERLOAD REACHES the session resolver — per-OID, because a single ungated overload is a whole door and its name is already spoken for by the gated one');
|
|
170
|
+
|
|
171
|
+
select is(
|
|
172
|
+
(select count(*)::text
|
|
173
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
174
|
+
where n.nspname = 'agent_board'
|
|
175
|
+
and p.proname in (select regexp_replace(sp.proname, '^signed_', '')
|
|
176
|
+
from pg_proc sp join pg_namespace sn on sn.oid = sp.pronamespace
|
|
177
|
+
where sn.nspname = 'agent_board' and sp.proname like 'signed\_%')),
|
|
178
|
+
'11',
|
|
179
|
+
'#310 canary: there are ELEVEN counterpart overloads across the seven names — pins the population of the assertion above, so an empty non-reaching set cannot mean an empty population');
|
|
180
|
+
|
|
181
|
+
-- THE RELAY DOOR. The flag asks whether a `signed_%`-NAMED function is executable by a
|
|
182
|
+
-- low-privilege role. That is not the same question as "can a low-privilege role reach the
|
|
183
|
+
-- signed path", and the gap is reachable: an `authenticated`-executable SECURITY DEFINER
|
|
184
|
+
-- function with ANY OTHER NAME can call a gateway through its owner's privileges. Verified --
|
|
185
|
+
-- such a relay left the flag `true` and every assertion in this file green.
|
|
186
|
+
--
|
|
187
|
+
-- The flag cannot close this (it would have to read bodies). The closure can, and does -- but
|
|
188
|
+
-- only within the bounds below, and those bounds point the DANGEROUS way for this one assertion.
|
|
189
|
+
--
|
|
190
|
+
-- WHY THE DIRECTION MATTERS HERE AND NOWHERE ELSE IN THIS FILE. Every other closure assertion is
|
|
191
|
+
-- MUST-REACH: a call edge the closure misses puts a member in the non-reaching set and turns it
|
|
192
|
+
-- RED. This is the file's only MUST-NOT-REACH assertion, so a missed edge turns it GREEN. Same
|
|
193
|
+
-- instrument, opposite failure direction, which is why its limits are stated here rather than
|
|
194
|
+
-- left implied:
|
|
195
|
+
--
|
|
196
|
+
-- * SCHEMA SCOPE. fn_call_closure's population is `agent_board` only. A SECURITY DEFINER relay
|
|
197
|
+
-- in ANOTHER schema -- `public`, say -- calling agent_board.signed_post_board_message is
|
|
198
|
+
-- invisible to this assertion AND to the flag. Reaching that state requires a migration,
|
|
199
|
+
-- not grant drift, which is why it is disclosed rather than treated as live.
|
|
200
|
+
-- * DYNAMIC SQL. A gateway named inside an `EXECUTE '...'` literal forms no call edge, because
|
|
201
|
+
-- stripped_body removes literals in its default mode. agent_board_fk_lock_edges.test.sql
|
|
202
|
+
-- carries an FKL-11 dynamic-SQL tripwire for exactly this; fn_call_closure has none.
|
|
203
|
+
--
|
|
204
|
+
-- Both are recorded rather than closed, and neither is reachable by granting alone.
|
|
205
|
+
select is(
|
|
206
|
+
(select coalesce(array_agg(distinct cl.root_oid::regprocedure::text order by cl.root_oid::regprocedure::text), array[]::text[])
|
|
207
|
+
from agent_board_test.fn_call_closure() cl
|
|
208
|
+
join pg_proc rp on rp.oid = cl.root_oid
|
|
209
|
+
join pg_namespace rn on rn.oid = rp.pronamespace
|
|
210
|
+
join pg_proc gp on gp.proname = cl.reached_name
|
|
211
|
+
join pg_namespace gn on gn.oid = gp.pronamespace
|
|
212
|
+
where rn.nspname = 'agent_board' and gn.nspname = 'agent_board'
|
|
213
|
+
and gp.proname like 'signed\_%'
|
|
214
|
+
and (has_function_privilege('authenticated', rp.oid, 'EXECUTE')
|
|
215
|
+
or has_function_privilege('anon', rp.oid, 'EXECUTE'))),
|
|
216
|
+
array[]::text[],
|
|
217
|
+
'#310: nothing IN agent_board executable by authenticated or anon reaches a signed_* gateway through its STATIC call closure — closes the in-schema SECURITY DEFINER relay; see the scope note above for the two doors it does not see');
|
|
218
|
+
|
|
219
|
+
-- CANARY for the predicate set the coverage assertion below depends on. Two of its members are
|
|
220
|
+
-- named rather than matched by pattern, so pattern drift would silently shrink the set and make
|
|
221
|
+
-- the coverage claim easier to satisfy. Pinning the membership makes that drift loud.
|
|
222
|
+
select is(
|
|
223
|
+
(select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
|
|
224
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
225
|
+
where n.nspname = 'agent_board' and p.prorettype = 'boolean'::regtype
|
|
226
|
+
and (p.proname ~ '^_(current_)?agent_(can|has)_'
|
|
227
|
+
or p.proname ~ '^_grant_within_'
|
|
228
|
+
or p.proname in ('_invite_permitted', '_membership_consent_present'))),
|
|
229
|
+
array['_agent_can_access_thread', '_agent_has_board_access', '_current_agent_can_access_task',
|
|
230
|
+
'_current_agent_can_access_thread', '_current_agent_can_mutate_task',
|
|
231
|
+
'_current_agent_can_post_to_thread', '_current_agent_can_send_to_board',
|
|
232
|
+
'_current_agent_has_board_access', '_grant_within_ceiling', '_invite_permitted',
|
|
233
|
+
'_membership_consent_present']::text[],
|
|
234
|
+
'#310 canary: the authorization-predicate set is exactly these eleven — note `_is_service_role` is excluded BY PATTERN, not by name: it discriminates a role, it does not authorize, and counting it would let a function that only checks it read as gated');
|
|
235
|
+
|
|
236
|
+
-- PER-OID, for the same reason the resolver assertion above is: this was per-NAME, so an
|
|
237
|
+
-- authenticated overload that reaches the resolver but NO authorization predicate was absorbed
|
|
238
|
+
-- -- the gated sibling overload supplied the `exists` for the name. Measured: such an overload
|
|
239
|
+
-- reddened ONLY the population canary above. That is a real tripwire, but the routine response
|
|
240
|
+
-- to a count canary is to bump the count, and that edit would have silently absorbed the hole.
|
|
241
|
+
-- A canary is not a substitute for the assertion it guards.
|
|
242
|
+
select is(
|
|
243
|
+
(select coalesce(array_agg(pop.oid::regprocedure::text order by pop.oid::regprocedure::text), array[]::text[])
|
|
244
|
+
from (select p.oid
|
|
245
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
246
|
+
where n.nspname = 'agent_board'
|
|
247
|
+
and p.proname in (select regexp_replace(sp.proname, '^signed_', '')
|
|
248
|
+
from pg_proc sp join pg_namespace sn on sn.oid = sp.pronamespace
|
|
249
|
+
where sn.nspname = 'agent_board' and sp.proname like 'signed\_%')) pop
|
|
250
|
+
where not exists (
|
|
251
|
+
select 1 from agent_board_test.fn_call_closure() cl
|
|
252
|
+
join pg_proc ap on ap.proname = cl.reached_name
|
|
253
|
+
join pg_namespace an on an.oid = ap.pronamespace
|
|
254
|
+
where cl.root_oid = pop.oid
|
|
255
|
+
and an.nspname = 'agent_board' and ap.prorettype = 'boolean'::regtype
|
|
256
|
+
and (ap.proname ~ '^_(current_)?agent_(can|has)_'
|
|
257
|
+
or ap.proname ~ '^_grant_within_'
|
|
258
|
+
or ap.proname in ('_invite_permitted', '_membership_consent_present')))),
|
|
259
|
+
array['agent_board.set_board_authority_ceiling(uuid,text,uuid)']::text[],
|
|
260
|
+
'#310: exactly ONE counterpart OVERLOAD authorizes without reaching a shared predicate — per-OID, so an ungated overload cannot hide behind a gated sibling of the same name');
|
|
261
|
+
|
|
262
|
+
-- ---------------------------------------------------------------------------
|
|
263
|
+
-- THE EXEMPTION, asserted positively and BEHAVIOURALLY. `set_board_authority_ceiling` gates
|
|
264
|
+
-- inline on the actor's board role instead of through a shared predicate, so the closure cannot
|
|
265
|
+
-- see it. An exemption claimed by absence is this repo's named soft spot; these three assertions
|
|
266
|
+
-- make the claim evidence.
|
|
267
|
+
-- ---------------------------------------------------------------------------
|
|
268
|
+
select agent_board.register_agent('agent', 'swe-owner', 'SWE Owner');
|
|
269
|
+
select agent_board.register_agent('agent', 'swe-other', 'SWE Other');
|
|
270
|
+
|
|
271
|
+
create temp table _swe on commit drop as
|
|
272
|
+
select (select id from agent_board.agents where handle = 'swe-owner') as owner_id,
|
|
273
|
+
(select id from agent_board.agents where handle = 'swe-other') as other_id;
|
|
274
|
+
|
|
275
|
+
select lives_ok(
|
|
276
|
+
$$ select agent_board.create_board((select owner_id from _swe), 'swe-board', 'SWE Board') $$,
|
|
277
|
+
'the owner creates a board');
|
|
278
|
+
|
|
279
|
+
create temp table _sweb on commit drop as
|
|
280
|
+
select id as board_id from agent_board.boards where slug = 'swe-board';
|
|
281
|
+
|
|
282
|
+
select lives_ok(
|
|
283
|
+
$$ create temp table _swecap as select * from agent_board.set_board_authority_ceiling(
|
|
284
|
+
(select board_id from _sweb), 'admin', (select owner_id from _swe)) $$,
|
|
285
|
+
'#310 exemption, allow side: the board OWNER can set the authority ceiling');
|
|
286
|
+
|
|
287
|
+
select is(
|
|
288
|
+
(select max_grantable_role from agent_board.board_authority_ceilings
|
|
289
|
+
where board_id = (select board_id from _sweb)),
|
|
290
|
+
'admin',
|
|
291
|
+
'#310 exemption: the owner call actually took effect — a refusal that also refused the owner would satisfy the deny assertion below on its own');
|
|
292
|
+
|
|
293
|
+
select throws_ok(
|
|
294
|
+
$$ select agent_board.set_board_authority_ceiling(
|
|
295
|
+
(select board_id from _sweb), 'owner', (select other_id from _swe)) $$,
|
|
296
|
+
'42501',
|
|
297
|
+
null,
|
|
298
|
+
'#310 exemption, deny side (stranger): an agent with no membership at all is refused 42501');
|
|
299
|
+
|
|
300
|
+
-- THE DENY SIDE ABOVE IS NOT ENOUGH ON ITS OWN, and this pair is why. A stranger is refused by
|
|
301
|
+
-- any gate that checks membership AT ALL, so it cannot distinguish "owner-only" from
|
|
302
|
+
-- "member-or-better". Measured: widening the inline gate from `bm.role = 'owner'` to
|
|
303
|
+
-- `bm.role in ('owner','admin','member')` left BOTH assertions above green. The subject that
|
|
304
|
+
-- separates the two claims is an ACTIVE NON-OWNER MEMBER, so that is what this seats.
|
|
305
|
+
select lives_ok(
|
|
306
|
+
$$ select agent_board._upsert_board_membership_state(
|
|
307
|
+
(select board_id from _sweb), (select other_id from _swe), 'joined', (select owner_id from _swe)) $$,
|
|
308
|
+
'the other agent becomes an ACTIVE member of the board');
|
|
309
|
+
|
|
310
|
+
select is(
|
|
311
|
+
(select role from agent_board.board_memberships
|
|
312
|
+
where board_id = (select board_id from _sweb) and agent_id = (select other_id from _swe)
|
|
313
|
+
order by case when membership_state in ('joined','subscribed') then 0 else 1 end, created_at desc, id desc
|
|
314
|
+
limit 1),
|
|
315
|
+
'member',
|
|
316
|
+
'#310 fixture: that membership is a plain member, not an owner — the precondition the assertion below needs');
|
|
317
|
+
|
|
318
|
+
select throws_ok(
|
|
319
|
+
$$ select agent_board.set_board_authority_ceiling(
|
|
320
|
+
(select board_id from _sweb), 'owner', (select other_id from _swe)) $$,
|
|
321
|
+
'42501',
|
|
322
|
+
null,
|
|
323
|
+
'#310 exemption, deny side (non-owner MEMBER): the inline gate is OWNER-only, not merely membership-checking — this is the assertion the stranger case cannot make');
|
|
324
|
+
|
|
325
|
+
-- ---------------------------------------------------------------------------
|
|
326
|
+
-- THE INSTRUMENT. Both of these are #310's own acceptance criteria.
|
|
327
|
+
-- ---------------------------------------------------------------------------
|
|
328
|
+
select is(
|
|
329
|
+
(select coalesce(array_agg(p.proname::text order by p.proname::text), array[]::text[])
|
|
330
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
331
|
+
where n.nspname = 'agent_board'
|
|
332
|
+
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
333
|
+
and agent_board_test.stripped_body(p.oid, true) ~* 'role_routine_grants'),
|
|
334
|
+
array[]::text[],
|
|
335
|
+
'#310: NO shipped function reads information_schema.role_routine_grants — it shows direct grants only and is filtered to visible roles, the fail-open shape #302 corrected');
|
|
336
|
+
|
|
337
|
+
-- The #118 class: recreating a SECURITY DEFINER function drops its ACL and COMMENT, and a
|
|
338
|
+
-- CREATE OR REPLACE that changed the signature would silently re-grant EXECUTE to PUBLIC.
|
|
339
|
+
-- Measured before the migration was written: authenticated and service_role held EXECUTE, anon
|
|
340
|
+
-- did not, PUBLIC held nothing. This pins that it still holds.
|
|
341
|
+
-- Aggregated over pg_roles, NOT a four-name candidate list. The list form was the
|
|
342
|
+
-- exemption-list soft spot rule 1 names: a widening grant to any OTHER existing role -- a portal
|
|
343
|
+
-- role, `dashboard_user` -- changes the real executor set and passes a membership test over four
|
|
344
|
+
-- names. Superusers and the function's OWNER are excluded because their access is implicit
|
|
345
|
+
-- rather than granted, and excluding the owner BY OID rather than by name keeps this portable to
|
|
346
|
+
-- a cluster where the bundle is owned by something other than `postgres`.
|
|
347
|
+
select is(
|
|
348
|
+
(select coalesce(array_agg(r.rolname::text order by r.rolname::text), array[]::text[])
|
|
349
|
+
from pg_roles r
|
|
350
|
+
cross join (select p.oid, p.proowner from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
351
|
+
where n.nspname = 'agent_board' and p.proname = 'get_bundle_compatibility') f
|
|
352
|
+
where not r.rolsuper
|
|
353
|
+
and r.oid <> f.proowner
|
|
354
|
+
and has_function_privilege(r.rolname, f.oid, 'EXECUTE')),
|
|
355
|
+
array['authenticated', 'service_role']::text[],
|
|
356
|
+
'#310 / #118 class: exactly authenticated and service_role hold granted EXECUTE on the health RPC — over ALL non-superuser roles, so a grant to any role at all fails here');
|
|
357
|
+
|
|
358
|
+
select isnt(
|
|
359
|
+
(select obj_description(
|
|
360
|
+
(select p.oid from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
361
|
+
where n.nspname = 'agent_board' and p.proname = 'get_bundle_compatibility'), 'pg_proc')),
|
|
362
|
+
null,
|
|
363
|
+
'#310 / #118 class: the COMMENT survived the recreate — DROP FUNCTION discards it and a recreate that omits it loses the only in-database statement of what the flag means');
|
|
364
|
+
|
|
365
|
+
select * from finish();
|
|
366
|
+
rollback;
|
|
@@ -29,7 +29,7 @@ set search_path = extensions, public, agent_board;
|
|
|
29
29
|
|
|
30
30
|
begin;
|
|
31
31
|
|
|
32
|
-
select plan(
|
|
32
|
+
select plan(57);
|
|
33
33
|
|
|
34
34
|
-- ---------------------------------------------------------------------------
|
|
35
35
|
-- The four demonstrated masking constructs.
|
|
@@ -361,6 +361,105 @@ select throws_ok(
|
|
|
361
361
|
null, null,
|
|
362
362
|
'SQLBODY-15: stripped_def is LOUD on a missing oid too -- it used to return NULL');
|
|
363
363
|
|
|
364
|
+
-- ===========================================================================
|
|
365
|
+
-- AMBIGUOUS SPLICE (message-board#304)
|
|
366
|
+
--
|
|
367
|
+
-- stripped_def locates the body inside pg_get_functiondef by FIRST textual
|
|
368
|
+
-- occurrence. The generated definition carries author-controlled text before the
|
|
369
|
+
-- body — the function name, every argument name, the SET clauses — so a body
|
|
370
|
+
-- that also occurs there sends the splice into the HEADER: the header is
|
|
371
|
+
-- corrupted and the REAL body comes back unstripped, which satisfies every
|
|
372
|
+
-- negative assertion and defeats every positive one over the result.
|
|
373
|
+
--
|
|
374
|
+
-- The trigger is built the way a real one arrives: a quoted ARGUMENT NAME that
|
|
375
|
+
-- happens to spell the body. That is the same surface a mint_harden assertion
|
|
376
|
+
-- was already defeated through (an argument named p_raw_key_fingerprint, the
|
|
377
|
+
-- #196 lint's rule 3), so these fixtures are a shrunk copy of a defect that has
|
|
378
|
+
-- happened, not an invented one.
|
|
379
|
+
--
|
|
380
|
+
-- Both arms are covered. The BEGIN ATOMIC arm added in #303 is the MORE exposed
|
|
381
|
+
-- one: a deparsed body is short and normalized, so coincidental collision is
|
|
382
|
+
-- likelier there than in a hand-written prosrc body.
|
|
383
|
+
--
|
|
384
|
+
-- The fixtures live in pg_temp, NOT agent_board: stripped_def now RAISES on
|
|
385
|
+
-- them, and the catalog-derived corpus scans below sweep agent_board.
|
|
386
|
+
-- ===========================================================================
|
|
387
|
+
create function pg_temp._mb304_prosrc_collision("select 'mb304-collision-marker'" int)
|
|
388
|
+
returns text language sql as $mb$select 'mb304-collision-marker'$mb$;
|
|
389
|
+
|
|
390
|
+
create function pg_temp._mb304_atomic_collision(p int, "RETURN 'mb304-collision-marker'::text" int)
|
|
391
|
+
returns text language sql return 'mb304-collision-marker';
|
|
392
|
+
|
|
393
|
+
-- The non-colliding twins. Same bodies, ordinary argument names. They prove the
|
|
394
|
+
-- raises below are caused by the AMBIGUITY and not by the fixture shape, and
|
|
395
|
+
-- that the fix did not turn stripped_def into a function that refuses everything.
|
|
396
|
+
create function pg_temp._mb304_prosrc_control(p int)
|
|
397
|
+
returns text language sql as $mb$select 'mb304-collision-marker'$mb$;
|
|
398
|
+
|
|
399
|
+
create function pg_temp._mb304_atomic_control(p int)
|
|
400
|
+
returns text language sql return 'mb304-collision-marker';
|
|
401
|
+
|
|
402
|
+
-- PREMISE. Everything below is worthless if the fixtures are not actually
|
|
403
|
+
-- ambiguous, so the ambiguity is MEASURED with the same occurrence count the
|
|
404
|
+
-- fixed helper uses — not assumed from how the argument was named.
|
|
405
|
+
select is(
|
|
406
|
+
(select ((length(pg_get_functiondef(p.oid)) - length(replace(pg_get_functiondef(p.oid), p.prosrc, '')))
|
|
407
|
+
/ length(p.prosrc))::int
|
|
408
|
+
from pg_proc p where p.oid = 'pg_temp._mb304_prosrc_collision(int)'::regprocedure),
|
|
409
|
+
2,
|
|
410
|
+
'MB304 PREMISE-A: the prosrc fixture really does have its body occurring TWICE in its own generated definition');
|
|
411
|
+
|
|
412
|
+
select is(
|
|
413
|
+
(select ((length(pg_get_functiondef(p.oid))
|
|
414
|
+
- length(replace(pg_get_functiondef(p.oid), pg_get_function_sqlbody(p.oid), '')))
|
|
415
|
+
/ length(pg_get_function_sqlbody(p.oid)))::int
|
|
416
|
+
from pg_proc p where p.oid = 'pg_temp._mb304_atomic_collision(int,int)'::regprocedure),
|
|
417
|
+
2,
|
|
418
|
+
'MB304 PREMISE-B: the BEGIN ATOMIC fixture really does have its DEPARSED body occurring TWICE in its own generated definition');
|
|
419
|
+
|
|
420
|
+
select throws_ok(
|
|
421
|
+
$$ select agent_board_test.stripped_def('pg_temp._mb304_prosrc_collision(int)'::regprocedure) $$,
|
|
422
|
+
null, null,
|
|
423
|
+
'MB304-1: an ambiguous PROSRC body RAISES rather than splicing into the header and returning the body unstripped');
|
|
424
|
+
|
|
425
|
+
select throws_ok(
|
|
426
|
+
$$ select agent_board_test.stripped_def('pg_temp._mb304_atomic_collision(int,int)'::regprocedure) $$,
|
|
427
|
+
null, null,
|
|
428
|
+
'MB304-2: the BEGIN ATOMIC arm added in #303 refuses the same way — the deparsed body is where a coincidental collision is MOST likely');
|
|
429
|
+
|
|
430
|
+
-- The controls. Unambiguous bodies must still splice, so the refusals above are
|
|
431
|
+
-- about AMBIGUITY and stripped_def has not become uniformly loud. lives_ok first
|
|
432
|
+
-- and the state check separately: a bare `ok(stripped_def(...) !~ …)` on a
|
|
433
|
+
-- raising call ABORTS the transaction, which pgTAP reports as no `not ok` at all
|
|
434
|
+
-- and silently drops every assertion after it.
|
|
435
|
+
select lives_ok(
|
|
436
|
+
$$ select agent_board_test.stripped_def('pg_temp._mb304_prosrc_control(int)'::regprocedure) $$,
|
|
437
|
+
'MB304-3: CONTROL — the same prosrc body with an ordinary argument name is UNambiguous and still splices');
|
|
438
|
+
|
|
439
|
+
select ok(
|
|
440
|
+
agent_board_test.stripped_def('pg_temp._mb304_prosrc_control(int)'::regprocedure)
|
|
441
|
+
!~ 'mb304-collision-marker',
|
|
442
|
+
'MB304-4: ...and that spliced result really is stripped');
|
|
443
|
+
|
|
444
|
+
select ok(
|
|
445
|
+
agent_board_test.stripped_def('pg_temp._mb304_prosrc_control(int)'::regprocedure, true)
|
|
446
|
+
~ 'mb304-collision-marker',
|
|
447
|
+
'MB304-5: ...and keep-literals mode keeps the marker, so MB304-4 is not passing because the definition collapsed');
|
|
448
|
+
|
|
449
|
+
select lives_ok(
|
|
450
|
+
$$ select agent_board_test.stripped_def('pg_temp._mb304_atomic_control(int)'::regprocedure) $$,
|
|
451
|
+
'MB304-6: CONTROL — the same deparsed body with an ordinary argument name is UNambiguous and still splices');
|
|
452
|
+
|
|
453
|
+
select ok(
|
|
454
|
+
agent_board_test.stripped_def('pg_temp._mb304_atomic_control(int)'::regprocedure)
|
|
455
|
+
!~ 'mb304-collision-marker',
|
|
456
|
+
'MB304-7: ...and that spliced result really is stripped');
|
|
457
|
+
|
|
458
|
+
select ok(
|
|
459
|
+
agent_board_test.stripped_def('pg_temp._mb304_atomic_control(int)'::regprocedure, true)
|
|
460
|
+
~ 'mb304-collision-marker',
|
|
461
|
+
'MB304-8: ...and keep-literals mode keeps the marker, so MB304-7 is not passing because the definition collapsed');
|
|
462
|
+
|
|
364
463
|
-- ---------------------------------------------------------------------------
|
|
365
464
|
-- CORPUS (catalog-derived). The vectors above are written by hand and can only
|
|
366
465
|
-- prove what their author thought of. These run the scanner over every live
|