@consiliency/agent-board-schema 1.51.0 → 1.52.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/package.json +1 -1
- package/schema-manifest.json +1 -1
- package/tests/agent_board_agent_liveness.test.sql +3 -1
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +33 -36
- package/tests/agent_board_authbound_boundary.test.sql +4 -3
- package/tests/agent_board_board_claim_policy.test.sql +17 -6
- package/tests/agent_board_claim_subject_gate.test.sql +7 -8
- package/tests/agent_board_claimkey.test.sql +9 -6
- package/tests/agent_board_claimpolicy_race.test.sql +1466 -342
- package/tests/agent_board_consent_emission.test.sql +5 -4
- package/tests/agent_board_consent_gating.test.sql +20 -9
- package/tests/agent_board_enroll_resumption.test.sql +1 -1
- package/tests/agent_board_enrollment_audit_content.test.sql +5 -4
- package/tests/agent_board_federated_identity_injective_slug.test.sql +3 -1
- package/tests/agent_board_federated_lock_order_class.test.sql +69 -52
- package/tests/agent_board_fedsession.test.sql +3 -1
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +9 -4
- package/tests/agent_board_fedsession_stored_identity_gate.test.sql +18 -6
- package/tests/agent_board_fk_lock_edges.test.sql +64 -5
- package/tests/agent_board_fleet_domain_reclaim.test.sql +2 -4
- package/tests/agent_board_fleet_machine_hijack.test.sql +2 -4
- package/tests/agent_board_fleet_machine_reactivation.test.sql +5 -1
- package/tests/agent_board_fleet_machine_reclaim.test.sql +2 -4
- package/tests/agent_board_invite_no_downgrade.test.sql +24 -18
- package/tests/agent_board_ledger_authority.test.sql +29 -21
- package/tests/agent_board_mint_harden.test.sql +5 -5
- package/tests/agent_board_outbox_wake_ordering.test.sql +4 -3
- package/tests/agent_board_ownerless_renew_quota.test.sql +7 -2
- package/tests/agent_board_portal_offline_invariant.test.sql +5 -4
- package/tests/agent_board_receipt_subject_gate.test.sql +7 -8
- package/tests/agent_board_rolereset.test.sql +5 -3
- package/tests/agent_board_rowid.test.sql +34 -15
- package/tests/agent_board_signer_lifecycle.test.sql +24 -13
- package/tests/agent_board_source_strip_helper.test.sql +2 -1
- package/tests/agent_board_task_event_origin_posture.test.sql +21 -9
- package/tests/agent_board_wake_recovery.test.sql +53 -26
- package/tests/agent_board_write_enforcement_class.test.sql +130 -12
- package/tests/catalog/function_execute_privileges.json +1923 -0
- package/tests/fixtures/test-prereqs.sql +644 -237
|
@@ -50,7 +50,7 @@ select p.oid, p.proname
|
|
|
50
50
|
from pg_proc p
|
|
51
51
|
join pg_namespace n on n.oid = p.pronamespace
|
|
52
52
|
where n.nspname = 'agent_board'
|
|
53
|
-
and agent_board_test.stripped_body(p.oid)
|
|
53
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into', 'task_events');
|
|
54
54
|
|
|
55
55
|
create temp table _op_class on commit drop as
|
|
56
56
|
with recursive walk as (
|
|
@@ -60,8 +60,7 @@ with recursive walk as (
|
|
|
60
60
|
from pg_proc p
|
|
61
61
|
join pg_namespace n on n.oid = p.pronamespace
|
|
62
62
|
join walk w
|
|
63
|
-
on agent_board_test.stripped_body(p.oid)
|
|
64
|
-
~* ('(^|[^a-z0-9_.])(agent_board\.)?' || w.proname || '\s*\(')
|
|
63
|
+
on agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re(w.proname)
|
|
65
64
|
where n.nspname = 'agent_board' and p.oid <> w.oid and w.depth < 5
|
|
66
65
|
)
|
|
67
66
|
select oid, proname, min(depth) as depth from walk group by oid, proname;
|
|
@@ -106,11 +105,12 @@ select is(
|
|
|
106
105
|
(select coalesce(string_agg(c.proname, ', ' order by c.proname), '')
|
|
107
106
|
from _op_class c
|
|
108
107
|
where (select count(*) from regexp_matches(
|
|
109
|
-
agent_board_test.stripped_body(c.oid, true), '_append_task_event
|
|
108
|
+
agent_board_test.stripped_body(c.oid, true), agent_board_test.call_re('_append_task_event'), 'g')) <> 0
|
|
110
109
|
and (select count(*) from regexp_matches(
|
|
111
|
-
agent_board_test.stripped_body(c.oid, true), '_append_task_event
|
|
110
|
+
agent_board_test.stripped_body(c.oid, true), agent_board_test.call_re('_append_task_event'), 'g'))
|
|
112
111
|
<> (select count(*) from regexp_matches(
|
|
113
|
-
agent_board_test.stripped_body(c.oid, true),
|
|
112
|
+
agent_board_test.stripped_body(c.oid, true),
|
|
113
|
+
agent_board_test.re(agent_board_test.call_frag('_append_task_event') || '\s*'''), 'g'))),
|
|
114
114
|
'',
|
|
115
115
|
'OP-4 CLASS: every funnel caller passes an explicit literal posture -- a caller that does not is NAMED here');
|
|
116
116
|
|
|
@@ -120,8 +120,13 @@ select is(
|
|
|
120
120
|
select is(
|
|
121
121
|
(select coalesce(string_agg(d.proname, ', ' order by d.proname), '')
|
|
122
122
|
from _op_direct d
|
|
123
|
+
-- The INSERT column-list arm ONLY (col_write_frag's arm 4), not col_write_re: that union also
|
|
124
|
+
-- accepts an INSERT with NO column list as a write of every column, and here a positional
|
|
125
|
+
-- insert that leaves origin_posture to its default is exactly the offender to name.
|
|
123
126
|
where agent_board_test.stripped_body(d.oid)
|
|
124
|
-
|
|
127
|
+
!~ agent_board_test.re(agent_board_test.stmt_frag('insert into', 'task_events')
|
|
128
|
+
|| '(?:\s+(?:' || agent_board_test.ci_re('as') || '\s+)?' || agent_board_test.ident_any_frag() || ')?'
|
|
129
|
+
|| '\s*\([^)]*' || agent_board_test.col_frag('origin_posture') || '[^)]*\)')),
|
|
125
130
|
'',
|
|
126
131
|
'OP-5 CLASS: every DIRECT task_events INSERT names origin_posture -- one that does not is NAMED here');
|
|
127
132
|
|
|
@@ -314,7 +319,12 @@ select is(
|
|
|
314
319
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
315
320
|
where n.nspname = 'agent_board'
|
|
316
321
|
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
317
|
-
|
|
322
|
+
-- The relations that OWN the column come from the catalog, not a hand list: an
|
|
323
|
+
-- origin_posture column added to a second table joins this scan by itself.
|
|
324
|
+
and exists (select 1 from information_schema.columns c
|
|
325
|
+
where c.table_schema = 'agent_board' and c.column_name = 'origin_posture'
|
|
326
|
+
and agent_board_test.stripped_body(p.oid, true)
|
|
327
|
+
~ agent_board_test.re(agent_board_test.col_assign_frag(c.table_name, 'origin_posture', agent_board_test.bare_ok(p.oid))))),
|
|
318
328
|
'',
|
|
319
329
|
'OP-19: nothing UPDATEs origin_posture -- including through a dynamic-SQL template, which keeping literals makes visible');
|
|
320
330
|
|
|
@@ -336,8 +346,10 @@ select is(
|
|
|
336
346
|
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
337
347
|
where n.nspname = 'agent_board'
|
|
338
348
|
and p.proname <> 'append_task_event'
|
|
349
|
+
-- bare_ok: a body that can reach the schema unqualified is scanned for the bare call too
|
|
350
|
+
-- (the hand-rolled form this replaced required the `agent_board.` qualifier).
|
|
339
351
|
and agent_board_test.stripped_body(p.oid, true)
|
|
340
|
-
~ '(
|
|
352
|
+
~ agent_board_test.call_re('append_task_event', agent_board_test.bare_ok(p.oid))),
|
|
341
353
|
'',
|
|
342
354
|
'OP-20: no in-schema function calls the PUBLIC append_task_event -- one that did would be stamped direct-rpc and named by nothing else');
|
|
343
355
|
|
|
@@ -200,22 +200,34 @@ cols as (
|
|
|
200
200
|
insert into _wr_class(kind, pattern)
|
|
201
201
|
select 'relations', string_agg(relname, '|' order by relname) from rels
|
|
202
202
|
union all
|
|
203
|
-
|
|
203
|
+
-- The column arm is a col_ref_frag alternation, QUALIFIED OR BARE (`i.created_at <` and
|
|
204
|
+
-- `created_at <` are both gates; a bare-only form would miss the alias spelling). The
|
|
205
|
+
-- window below is measured from the start of the reference, so a qualified reference
|
|
206
|
+
-- reaches `[^<>=]{0,40}` further back than the old `\m…\M` did -- a difference that can
|
|
207
|
+
-- only ADD a match, which makes the exact-set claims below red, never green (measured on
|
|
208
|
+
-- the 26 members: identical).
|
|
209
|
+
select 'columns', '(?:' || string_agg(agent_board_test.col_ref_frag(attname), '|' order by length(attname) desc, attname) || ')' from cols;
|
|
204
210
|
|
|
205
211
|
create or replace function pg_temp._wr_cursor_gates(p_text text) returns text
|
|
206
212
|
language sql stable as $fn$
|
|
207
213
|
select coalesce(
|
|
208
|
-
|
|
214
|
+
-- A qualifier on a column-first match is dropped from the report, so the strings
|
|
215
|
+
-- WR-8 pins stay `created_at <`, whichever alias the body spells. (This rewrites a
|
|
216
|
+
-- MATCH, not source text, so it is a plain regex rather than a family composition.)
|
|
217
|
+
(select string_agg(distinct regexp_replace(regexp_replace(m[1], '^[^.]*[.][[:space:]]*', ''),
|
|
218
|
+
'[[:space:]]+', ' ', 'g'), ' | ')
|
|
209
219
|
from _wr_class c,
|
|
210
220
|
regexp_matches(
|
|
211
221
|
-- literals KEPT: this file's class members put real predicates in
|
|
212
222
|
-- dynamic SQL (see the _wr_members header). Comments are removed.
|
|
213
223
|
agent_board_test.strip_sql($1, true),
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|| '
|
|
218
|
-
'
|
|
224
|
+
-- case-folding comes from the family (ci_re / ident_re), not from an `i` flag,
|
|
225
|
+
-- which the director's `(?c)` would override
|
|
226
|
+
agent_board_test.re('(' || c.pattern || '[^<>=]{0,40}(?:<=|>=|<|>)'
|
|
227
|
+
|| '|(?:<=|>=|<|>)[^<>=]{0,40}' || c.pattern
|
|
228
|
+
|| '|' || c.pattern || '[^<>=]{0,20}\m' || agent_board_test.ci_re('between') || '\M'
|
|
229
|
+
|| '|\m' || agent_board_test.ci_re('between') || '\M[^<>=]{0,40}' || c.pattern || ')'),
|
|
230
|
+
'g') m
|
|
219
231
|
where c.kind = 'columns'),
|
|
220
232
|
'')
|
|
221
233
|
$fn$;
|
|
@@ -270,7 +282,12 @@ select p.proname, 'function', pg_get_functiondef(p.oid), agent_board_test.stripp
|
|
|
270
282
|
join pg_namespace n on n.oid = p.pronamespace
|
|
271
283
|
where n.nspname = 'agent_board'
|
|
272
284
|
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
273
|
-
|
|
285
|
+
-- membership by RELATION REFERENCE in the stripped body (literals kept, because the
|
|
286
|
+
-- members this file exists for reference their relations inside dynamic SQL), rather
|
|
287
|
+
-- than a substring of the raw definition; measured: the same 15 functions.
|
|
288
|
+
and agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.re(
|
|
289
|
+
(select string_agg(agent_board_test.qname_frag(x, agent_board_test.bare_ok(p.oid)), '|')
|
|
290
|
+
from unnest(string_to_array((select pattern from _wr_class where kind = 'relations'), '|')) x));
|
|
274
291
|
|
|
275
292
|
-- ---------------------------------------------------------------------------
|
|
276
293
|
-- WR-1 EXISTENCE
|
|
@@ -353,7 +370,7 @@ select is(
|
|
|
353
370
|
|
|
354
371
|
select ok(
|
|
355
372
|
(select agent_board_test.stripped_viewdef(to_regclass('agent_board.v_agent_wake_recovery'), true)
|
|
356
|
-
|
|
373
|
+
!~ agent_board_test.re('\m' || agent_board_test.ci_re('order by') || '\M')),
|
|
357
374
|
'WR-4 the wake recovery view carries no ORDER BY -- the unhandled set is a SET, not a cursor stream');
|
|
358
375
|
|
|
359
376
|
-- ---------------------------------------------------------------------------
|
|
@@ -367,16 +384,20 @@ select is(
|
|
|
367
384
|
coalesce(
|
|
368
385
|
(select array_agg(btrim(btrim(split_part(e, '::', 1)), '''') order by btrim(btrim(split_part(e, '::', 1)), ''''))
|
|
369
386
|
from unnest(string_to_array(
|
|
370
|
-
|
|
371
|
-
|
|
387
|
+
-- literals KEPT: the state set being read IS a list of string literals
|
|
388
|
+
substring(agent_board_test.stripped_viewdef(to_regclass('agent_board.v_agent_wake_recovery'), true)
|
|
389
|
+
from agent_board_test.re(agent_board_test.col_ref_frag('action_state') || '\s*=\s*' || agent_board_test.ci_re('any')
|
|
390
|
+
|| '\s*\(\s*' || agent_board_test.ci_re('array') || '\s*\[([^]]*)\]')), ',')) e),
|
|
372
391
|
array['VIEW-STATE-SET-NOT-FOUND']),
|
|
373
392
|
coalesce(
|
|
374
393
|
(select array_agg(btrim(btrim(split_part(e, '::', 1)), '''') order by btrim(btrim(split_part(e, '::', 1)), ''''))
|
|
375
394
|
from unnest(string_to_array(
|
|
395
|
+
-- source-scan: loud-literal pg_get_expr deparses a stored predicate tree -- no comment or literal can hide in it
|
|
376
396
|
substring((select pg_get_expr(i.indpred, i.indrelid)
|
|
377
397
|
from pg_index i
|
|
378
398
|
where i.indexrelid = to_regclass('agent_board.idx_agent_board_inbox_items_agent_pending'))
|
|
379
|
-
from 'action_state
|
|
399
|
+
from agent_board_test.re(agent_board_test.col_ref_frag('action_state') || '\s*=\s*' || agent_board_test.ci_re('any')
|
|
400
|
+
|| '\s*\(\s*' || agent_board_test.ci_re('array') || '\s*\[([^]]*)\]')), ',')) e),
|
|
380
401
|
array['INDEX-STATE-SET-NOT-FOUND']),
|
|
381
402
|
'WR-5 the view''s action_state set equals idx_agent_board_inbox_items_agent_pending''s predicate set -- editing either one alone fails here');
|
|
382
403
|
|
|
@@ -578,7 +599,8 @@ select is(
|
|
|
578
599
|
-- The scoping is a catalog fact, not only a behavioural one: a future edit that
|
|
579
600
|
-- drops the predicate but leaves the fixture passing for another reason fails here.
|
|
580
601
|
select ok(
|
|
581
|
-
(select
|
|
602
|
+
(select agent_board_test.stripped_viewdef(to_regclass('agent_board.v_agent_wake_recovery'))
|
|
603
|
+
~ agent_board_test.call_re('current_agent_id')),
|
|
582
604
|
'WR-6 the view definition itself scopes on current_agent_id() -- recipient scoping is server-side, never a caller''s filter');
|
|
583
605
|
|
|
584
606
|
-- ---------------------------------------------------------------------------
|
|
@@ -617,16 +639,19 @@ select is(
|
|
|
617
639
|
'created_at <',
|
|
618
640
|
'WR-8 mark_retention_metadata''s ONLY pre-commit comparison is `created_at <` (an age cutoff) -- it never compares seq, and never gates on a lower bound');
|
|
619
641
|
|
|
620
|
-
-- `ops_retention_reason` is a jsonb KEY -- a string literal --
|
|
621
|
-
--
|
|
622
|
-
--
|
|
623
|
-
--
|
|
624
|
-
--
|
|
625
|
-
--
|
|
626
|
-
--
|
|
642
|
+
-- `ops_retention_reason` is a jsonb KEY -- a string literal -- and the UPDATE it
|
|
643
|
+
-- accompanies is inside the `$retention$` dynamic-SQL block. Both are read from
|
|
644
|
+
-- `body`, which KEEPS literals (see the _wr_members header): the key survives as a
|
|
645
|
+
-- string, and the UPDATE survives because it is one. Neither would survive the
|
|
646
|
+
-- default literal-stripping mode, and neither needs the raw `src` -- comments are
|
|
647
|
+
-- the only thing the strip removes here. Restated so the next reader does not
|
|
648
|
+
-- "simplify" the strip mode away. The UPDATE's target is the format() placeholder
|
|
649
|
+
-- `agent_board.%1$I` -- the sweep is parameterised over the relation -- so this
|
|
650
|
+
-- matches THAT spelling, not a relation name: a statement matcher would need a name
|
|
651
|
+
-- the body does not contain.
|
|
627
652
|
select ok(
|
|
628
|
-
(select
|
|
629
|
-
and body ~ 'update agent_board'
|
|
653
|
+
(select body ~ 'ops_retention_reason'
|
|
654
|
+
and body ~ agent_board_test.re('\m' || agent_board_test.ci_re('update') || '\s+' || agent_board_test.ci_re('agent_board') || '\s*\.\s*%1\$I\M')
|
|
630
655
|
from _wr_members where name = 'mark_retention_metadata'),
|
|
631
656
|
'WR-8 mark_retention_metadata stamps retention metadata through an UPDATE sweep -- the age cutoff drives retention, not a fetch');
|
|
632
657
|
|
|
@@ -650,7 +675,7 @@ select is(
|
|
|
650
675
|
'WR-8 claim_callback_outbox_events'' ONLY pre-commit gate is `available_at <=` -- a scheduling predicate, never a lower bound against a caller cursor');
|
|
651
676
|
|
|
652
677
|
select ok(
|
|
653
|
-
(select body
|
|
678
|
+
(select body !~ agent_board_test.re(agent_board_test.col_ref_frag('available_at') || '[^<>=]{0,40}(?:>=|>)')
|
|
654
679
|
from _wr_members where name = 'claim_callback_outbox_events'),
|
|
655
680
|
'WR-8 that gate is an UPPER bound (<=) and never a lower bound -- a `available_at >` would be a cursor, and is what this distinguishes');
|
|
656
681
|
|
|
@@ -683,7 +708,7 @@ select ok(
|
|
|
683
708
|
'WR-9 claim_callback_outbox_events partitions the queue on outbox_status -- the lossless property #187 relies on, restated positively here');
|
|
684
709
|
|
|
685
710
|
select ok(
|
|
686
|
-
(select
|
|
711
|
+
(select body ~ agent_board_test.re(agent_board_test.col_ref_frag('action_state') || '\s*=\s*' || agent_board_test.ci_re('any') || '\M')
|
|
687
712
|
from _wr_members where name = 'v_agent_wake_recovery'),
|
|
688
713
|
'WR-9 v_agent_wake_recovery partitions the inbox on action_state -- status, never an ordinal');
|
|
689
714
|
|
|
@@ -700,12 +725,14 @@ select is(
|
|
|
700
725
|
from pg_proc p
|
|
701
726
|
where p.pronamespace = 'agent_board'::regnamespace
|
|
702
727
|
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
703
|
-
and agent_board_test.stripped_body(p.oid, true) ~* 'action_state[[:space:]]*=[[:space:]]*[^=]'
|
|
704
728
|
-- The schema qualification is OPTIONAL on purpose (board review F3): a writer
|
|
705
729
|
-- using `set search_path` plus a bare `update inbox_items` was invisible to
|
|
706
730
|
-- the qualified form, and WR-10 stayed green naming only one writer. Same
|
|
707
731
|
-- class as the WR-7 name filter -- two name-based enumerators in one file.
|
|
708
|
-
and
|
|
732
|
+
-- col_write_re keeps the qualifier optional and binds the column to THIS relation's
|
|
733
|
+
-- write (the hand-rolled pair accepted `action_state =` anywhere in a body that also
|
|
734
|
+
-- updated inbox_items, a WHERE predicate included).
|
|
735
|
+
and agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.col_write_re('inbox_items', 'action_state')),
|
|
709
736
|
'update_inbox_item_state',
|
|
710
737
|
'WR-10 exactly ONE function writes inbox_items.action_state -- the only way out of the unhandled set');
|
|
711
738
|
|
|
@@ -332,7 +332,7 @@ set search_path = extensions, public, agent_board;
|
|
|
332
332
|
|
|
333
333
|
begin;
|
|
334
334
|
|
|
335
|
-
select plan(
|
|
335
|
+
select plan(25);
|
|
336
336
|
|
|
337
337
|
-- ===========================================================================================
|
|
338
338
|
-- THE CLASS, derived from pg_catalog. Nothing below is a list of names.
|
|
@@ -357,10 +357,7 @@ select p.oid as fn_oid, t.relname, t.relkind
|
|
|
357
357
|
where p.pronamespace = 'agent_board'::regnamespace
|
|
358
358
|
and p.prokind in ('f','p')
|
|
359
359
|
and p.prolang <> (select oid from pg_language where lanname = 'internal')
|
|
360
|
-
and
|
|
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 || '")');
|
|
360
|
+
and agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.write_re(t.relname);
|
|
364
361
|
|
|
365
362
|
create temp table _wec_closure on commit drop as
|
|
366
363
|
select * from agent_board_test.fn_call_closure();
|
|
@@ -385,8 +382,17 @@ select * from agent_board_test.fn_call_closure();
|
|
|
385
382
|
-- then satisfy the resolver arm — fail-CLOSED, and no agent_board function is written that way
|
|
386
383
|
-- today (measured: this pattern and the digits-only one agree on ALL 67 shipped raisers; they
|
|
387
384
|
-- differ only on the decoy).
|
|
385
|
+
--
|
|
386
|
+
-- Composed from the family (#437 round 1): the keywords are `ci_re` so the case rule lives in the
|
|
387
|
+
-- pattern, not in a `lower()` around each subject, and the site the lint traces `via pat` scores
|
|
388
|
+
-- the composition rather than a hand alternation on an absence form. The three uses below are
|
|
389
|
+
-- `!~ (select pat from _wec_raise_pat)`, which the lint judges by THIS expression.
|
|
388
390
|
create temp table _wec_raise_pat on commit drop as
|
|
389
|
-
select
|
|
391
|
+
select agent_board_test.re(
|
|
392
|
+
agent_board_test.ci_re('errcode') || '\s*=\s*''?42501'
|
|
393
|
+
|| '|' || agent_board_test.ci_re('sqlstate') || '\s+''?42501'
|
|
394
|
+
|| '|' || agent_board_test.ci_re('insufficient_privilege')
|
|
395
|
+
) as pat;
|
|
390
396
|
|
|
391
397
|
-- THE POPULATION, per-OID and TRANSITIVE: an authenticated-executable root that reaches a direct
|
|
392
398
|
-- writer through the call closure. `fn_call_closure()` is REFLEXIVE (every function reaches
|
|
@@ -580,7 +586,7 @@ select is(
|
|
|
580
586
|
where cl.root_oid = p.oid
|
|
581
587
|
and cl.reached_name in ('_resolved_actor_agent_id', 'current_agent_id',
|
|
582
588
|
'_require_platform_admin_user_id'))
|
|
583
|
-
and
|
|
589
|
+
and agent_board_test.stripped_body(p.oid, true) !~ agent_board_test.call_re('auth.uid', false)),
|
|
584
590
|
array[]::text[],
|
|
585
591
|
'#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');
|
|
586
592
|
|
|
@@ -599,7 +605,7 @@ select is(
|
|
|
599
605
|
select is(
|
|
600
606
|
(select coalesce(array_agg(p.sig order by p.sig), array[]::text[])
|
|
601
607
|
from _wec_pop p
|
|
602
|
-
where
|
|
608
|
+
where agent_board_test.stripped_body(p.oid, true) ~ agent_board_test.call_re('auth.uid', false)),
|
|
603
609
|
array['agent_board.self_enroll_human(text)']::text[],
|
|
604
610
|
'#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');
|
|
605
611
|
|
|
@@ -649,7 +655,7 @@ select is( -- pgtap-allow-raise-checked
|
|
|
649
655
|
and (ap.proname ~ '^_(current_)?agent_(can|has)_'
|
|
650
656
|
or ap.proname ~ '^_grant_within_'
|
|
651
657
|
or ap.proname in ('_invite_permitted', '_membership_consent_present')))
|
|
652
|
-
and
|
|
658
|
+
and agent_board_test.stripped_body(p.oid, true) !~ (select pat from _wec_raise_pat)) x),
|
|
653
659
|
array[
|
|
654
660
|
'agent_board.block_board_inviter(uuid,uuid,uuid,jsonb) -> _resolved_actor_agent_id',
|
|
655
661
|
'agent_board.create_or_find_thread(uuid,text,text,text,jsonb) -> _resolved_actor_agent_id',
|
|
@@ -698,7 +704,7 @@ select is(
|
|
|
698
704
|
and (ap.proname ~ '^_(current_)?agent_(can|has)_'
|
|
699
705
|
or ap.proname ~ '^_grant_within_'
|
|
700
706
|
or ap.proname in ('_invite_permitted', '_membership_consent_present')))
|
|
701
|
-
and
|
|
707
|
+
and agent_board_test.stripped_body(p.oid, true) !~ (select pat from _wec_raise_pat)
|
|
702
708
|
and not exists (
|
|
703
709
|
select 1 from _wec_closure cl
|
|
704
710
|
where cl.root_oid = p.oid
|
|
@@ -750,9 +756,9 @@ select ok(
|
|
|
750
756
|
-- exclusion is currently the STRICTER choice, so a failure here means WEC-8 is being harder than
|
|
751
757
|
-- it needs to be, not that it has gone soft.
|
|
752
758
|
select ok(
|
|
753
|
-
|
|
759
|
+
agent_board_test.stripped_body(
|
|
754
760
|
(select oid from pg_proc where pronamespace = 'agent_board'::regnamespace
|
|
755
|
-
and proname = 'current_agent_id'), true)
|
|
761
|
+
and proname = 'current_agent_id'), true) !~ (select pat from _wec_raise_pat),
|
|
756
762
|
'#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');
|
|
757
763
|
|
|
758
764
|
-- ===========================================================================================
|
|
@@ -1012,6 +1018,118 @@ select is(
|
|
|
1012
1018
|
'88',
|
|
1013
1019
|
'#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');
|
|
1014
1020
|
|
|
1021
|
+
-- ===========================================================================================
|
|
1022
|
+
-- WEC-24/25 — THE CROSS-CHECK (Consiliency/message-board#416 / #417 / #424, plan change 9).
|
|
1023
|
+
-- `write_re` is the suite's only definition of "writes" (the lint in
|
|
1024
|
+
-- scripts/check-test-source-scan-literals.mjs is what makes that true). This bounds it from the
|
|
1025
|
+
-- other side with a PARSER: plpgsql_check's plpgsql_show_dependency_tb, created inside this
|
|
1026
|
+
-- transaction and rolled back with it. For every plpgsql function in agent_board, the class
|
|
1027
|
+
-- relations the regex says it WRITES must be a SUBSET of the relations the parser says it
|
|
1028
|
+
-- TOUCHES.
|
|
1029
|
+
--
|
|
1030
|
+
-- WHAT THIS BOUNDS, STATED NARROWLY. The parser does not distinguish a read from a write, so a
|
|
1031
|
+
-- regex "writer" of a relation the function legitimately READS is still a subset member and is
|
|
1032
|
+
-- NOT caught here. The check convicts only a regex writer of a relation the parser never
|
|
1033
|
+
-- touches: a name matched in a comment or a literal, a look-alike identifier, or a relation
|
|
1034
|
+
-- named only inside dynamic SQL. The reverse direction (parser dependency, regex silent) is
|
|
1035
|
+
-- deliberately NOT asserted: dynamic SQL yields no parser dependency at all, and every function
|
|
1036
|
+
-- that reads A and writes B would be flagged on A — a list of false positives with a reason each.
|
|
1037
|
+
--
|
|
1038
|
+
-- DYNAMIC SQL. A body that EXECUTEs a COMPUTED string is parser-blind: `execute format('update
|
|
1039
|
+
-- agent_board.boards …')` or `q := 'update agent_board.boards …'; execute q;` is a regex writer
|
|
1040
|
+
-- (literals are KEPT in `_wec_direct`'s subject, because a raise's errcode literal must be
|
|
1041
|
+
-- visible) with no dependency — a FALSE over-match verdict. Measured before writing this: the
|
|
1042
|
+
-- parser DOES read a CONSTANT `execute 'update agent_board.boards …'` (it reports the relation),
|
|
1043
|
+
-- so the plan's m32 fixture in that spelling flips nothing; the variable-held spelling is the
|
|
1044
|
+
-- mutation. The subset check therefore SKIPS any function whose
|
|
1045
|
+
-- literal-stripped body contains an `execute` token, and WEC-25 names those functions
|
|
1046
|
+
-- positively. Measured on this tree: exactly `mark_retention_metadata`, whose dynamic targets —
|
|
1047
|
+
-- from its own allowlist literal — are messages, task_events, presence, callback_outbox_events,
|
|
1048
|
+
-- callback_delivery_attempts, inbox_items, delivery_recipients and board_memberships. That last
|
|
1049
|
+
-- one is a CLASS relation and the write is `update agent_board.%1$I …` inside format():
|
|
1050
|
+
-- invisible to `write_re('board_memberships')` AND to the parser. WEC-25 is therefore not
|
|
1051
|
+
-- decoration: it is the only assertion in this suite that names a writer of board_memberships
|
|
1052
|
+
-- neither definition of "writes" can see, and a second dynamic writer fails it by name. Its
|
|
1053
|
+
-- TARGET list above is prose: nothing here pins it (recorded as unpinned, plan m33).
|
|
1054
|
+
--
|
|
1055
|
+
-- TRIGGER FUNCTIONS need the relation they fire on to be parsed (NEW/OLD are typed by it); each
|
|
1056
|
+
-- is parsed once per pg_trigger attachment and the dependency sets are unioned. An UNATTACHED
|
|
1057
|
+
-- trigger function cannot be parsed and is reported as `PARSER-UNREADABLE`, not skipped.
|
|
1058
|
+
--
|
|
1059
|
+
-- PREREQUISITE. If plpgsql_check is not available on the cluster, WEC-24 reads
|
|
1060
|
+
-- `PREREQUISITE-MISSING plpgsql_check` and FAILS by name — never `skip`: undetermined is not a
|
|
1061
|
+
-- pass (AGENTS.md rule 4). Both the local cluster and CI's `supabase start` image ship it.
|
|
1062
|
+
-- ===========================================================================================
|
|
1063
|
+
do $$
|
|
1064
|
+
begin
|
|
1065
|
+
if exists (select 1 from pg_available_extensions where name = 'plpgsql_check') then
|
|
1066
|
+
execute 'create extension if not exists plpgsql_check with schema extensions';
|
|
1067
|
+
end if;
|
|
1068
|
+
end $$;
|
|
1069
|
+
|
|
1070
|
+
-- The parser's relation set for one function, reached through dynamic SQL so that an absent
|
|
1071
|
+
-- extension yields NULL here instead of a parse error that would abort the file (#113 class).
|
|
1072
|
+
create function pg_temp._wec_parser_rels(p_fn oid, p_rel oid) returns text[]
|
|
1073
|
+
language plpgsql as $$
|
|
1074
|
+
declare v_rels text[]; v_ns text;
|
|
1075
|
+
begin
|
|
1076
|
+
select e.extnamespace::regnamespace::text into v_ns from pg_extension e where e.extname = 'plpgsql_check';
|
|
1077
|
+
if v_ns is null then
|
|
1078
|
+
return null;
|
|
1079
|
+
end if;
|
|
1080
|
+
execute format($q$ select coalesce(array_agg(distinct d.schema || '.' || d.name), '{}'::text[])
|
|
1081
|
+
from %I.plpgsql_show_dependency_tb($1::regprocedure, $2::regclass) d
|
|
1082
|
+
where d.type = 'RELATION' $q$, v_ns)
|
|
1083
|
+
into v_rels using p_fn, p_rel;
|
|
1084
|
+
return v_rels;
|
|
1085
|
+
end $$;
|
|
1086
|
+
|
|
1087
|
+
create temp table _wec_plpgsql on commit drop as
|
|
1088
|
+
select p.oid as fn_oid, p.proname::text as proname,
|
|
1089
|
+
p.prorettype = 'trigger'::regtype as is_trigger,
|
|
1090
|
+
agent_board_test.stripped_body(p.oid) ~ agent_board_test.re('\m' || agent_board_test.ci_re('execute') || '\M') as dynamic_sql
|
|
1091
|
+
from pg_proc p
|
|
1092
|
+
where p.pronamespace = 'agent_board'::regnamespace
|
|
1093
|
+
and p.prokind in ('f', 'p')
|
|
1094
|
+
and p.prolang = (select oid from pg_language where lanname = 'plpgsql');
|
|
1095
|
+
|
|
1096
|
+
create temp table _wec_parser on commit drop as
|
|
1097
|
+
select f.fn_oid,
|
|
1098
|
+
(select count(*) from pg_extension where extname = 'plpgsql_check') > 0 as parser_present,
|
|
1099
|
+
f.is_trigger and not exists (select 1 from pg_trigger tg where tg.tgfoid = f.fn_oid) as unattached_trigger,
|
|
1100
|
+
(select array_agg(distinct r)
|
|
1101
|
+
from (select tg.tgrelid from pg_trigger tg where tg.tgfoid = f.fn_oid
|
|
1102
|
+
union all select 0::oid where not f.is_trigger) tr
|
|
1103
|
+
cross join lateral unnest(coalesce(pg_temp._wec_parser_rels(f.fn_oid, tr.tgrelid), '{}'::text[])) as r) as rels
|
|
1104
|
+
from _wec_plpgsql f;
|
|
1105
|
+
|
|
1106
|
+
select is(
|
|
1107
|
+
(select case
|
|
1108
|
+
when not bool_and(parser_present) then 'PREREQUISITE-MISSING plpgsql_check'
|
|
1109
|
+
else coalesce(string_agg(x.entry, ', ' order by x.entry), '<none>')
|
|
1110
|
+
end
|
|
1111
|
+
from _wec_parser pr
|
|
1112
|
+
left join lateral (
|
|
1113
|
+
select f.proname || ':' || w.relname as entry
|
|
1114
|
+
from _wec_plpgsql f
|
|
1115
|
+
join _wec_direct w on w.fn_oid = f.fn_oid
|
|
1116
|
+
where f.fn_oid = pr.fn_oid
|
|
1117
|
+
and not f.dynamic_sql
|
|
1118
|
+
and (pr.unattached_trigger
|
|
1119
|
+
or not ('agent_board.' || w.relname = any (coalesce(pr.rels, '{}'::text[]))))
|
|
1120
|
+
union all
|
|
1121
|
+
select f.proname || ':PARSER-UNREADABLE'
|
|
1122
|
+
from _wec_plpgsql f
|
|
1123
|
+
where f.fn_oid = pr.fn_oid and pr.unattached_trigger and not f.dynamic_sql
|
|
1124
|
+
) x on true),
|
|
1125
|
+
'<none>',
|
|
1126
|
+
'WEC-24 (change 9): every class relation `write_re` says a static plpgsql body writes is a relation the parser says that body touches — a regex writer the parser cannot see (comment, literal, look-alike, dynamic SQL) is named here as `function:relation`');
|
|
1127
|
+
|
|
1128
|
+
select is(
|
|
1129
|
+
(select coalesce(string_agg(proname, ', ' order by proname), '<none>') from _wec_plpgsql where dynamic_sql),
|
|
1130
|
+
'mark_retention_metadata',
|
|
1131
|
+
'WEC-25 (change 9): the plpgsql bodies WEC-24 skips for dynamic SQL are exactly these — mark_retention_metadata writes board_memberships through format(), which neither write_re nor the parser can see; a second dynamic writer fails here by name');
|
|
1132
|
+
|
|
1015
1133
|
select * from finish();
|
|
1016
1134
|
|
|
1017
1135
|
rollback;
|