@consiliency/agent-board-schema 1.48.0 → 1.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/fixtures/already-applied.json +12 -0
  2. package/migrations/20260929000100_agent_board_advisory_revert_cap_reset.sql +371 -0
  3. package/migrations/20260930000100_agent_board_claim_policy_race.sql +922 -0
  4. package/migrations/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql +158 -0
  5. package/package.json +1 -1
  6. package/schema-manifest.json +30 -3
  7. package/src/tooling.test.mjs +25 -2
  8. package/tests/agent_board_board_claim_policy.test.sql +261 -1
  9. package/tests/agent_board_claim_subject_gate.test.sql +11 -2
  10. package/tests/agent_board_claimkey.test.sql +17 -2
  11. package/tests/agent_board_claimpolicy_race.test.sql +1374 -0
  12. package/tests/agent_board_ownerless_quota_census.test.sql +6 -0
  13. package/tests/agent_board_ownerless_renew_quota.test.sql +37 -8
  14. package/tests/catalog/p_metadata_parameters.json +16 -0
  15. package/tests/fixtures/test-prereqs.sql +186 -0
  16. package/tests/isolation/expected/claimpolicy_acquire_revert_race.out +66 -0
  17. package/tests/isolation/expected/claimpolicy_board_scope.out +73 -0
  18. package/tests/isolation/expected/claimpolicy_claimrow_lock_order.out +80 -0
  19. package/tests/isolation/expected/claimpolicy_isolation_refusal.out +114 -0
  20. package/tests/isolation/expected/claimpolicy_renew_revert_race.out +66 -0
  21. package/tests/isolation/expected/claimpolicy_writer_keyshare.out +49 -0
  22. package/tests/isolation/specs/claimpolicy_acquire_revert_race.spec +118 -0
  23. package/tests/isolation/specs/claimpolicy_board_scope.spec +115 -0
  24. package/tests/isolation/specs/claimpolicy_claimrow_lock_order.spec +200 -0
  25. package/tests/isolation/specs/claimpolicy_isolation_refusal.spec +162 -0
  26. package/tests/isolation/specs/claimpolicy_renew_revert_race.spec +150 -0
  27. package/tests/isolation/specs/claimpolicy_writer_keyshare.spec +107 -0
  28. package/tests/seeds/20261001000100_agent_board_claimpolicy_lock_grant_revoke.sql.pre.sql +99 -0
@@ -386,6 +386,12 @@ select is(
386
386
  -- membership to exercise it would be testing who may set the policy (BCP's subject) rather than
387
387
  -- what the reader does with it. updated_at is set as the RPC sets it, so the row is not a shape
388
388
  -- the RPC could never produce.
389
+ --
390
+ -- SINCE #400 THE TWO PATHS DIVERGE IN ONE RESPECT, recorded here rather than left as a silent
391
+ -- premise: the RPC ALSO re-applies the ownerless TTL cap to this board's active ownerless claims on
392
+ -- an advisory -> inherit revert, and a direct UPDATE does not. It changes nothing this assertion
393
+ -- reads — a clamped 30-day lease is still active and still counted — but "the row is not a shape
394
+ -- the RPC could never produce" is now a statement about the BOARD row only.
389
395
  update agent_board.boards
390
396
  set claim_policy = 'inherit', updated_at = now()
391
397
  where id = (select adv_board from _oq);
@@ -7,8 +7,16 @@
7
7
  -- SCOPED, AND THE SCOPE IS ASSERTED HERE RATHER THAN PROMISED IN PROSE. That convergence holds
8
8
  -- against an agent whose boards do not opt out. It does NOT hold against one whose board is flipped
9
9
  -- to `claim_policy = 'advisory'` and back, which an owner may do at will and which an agent that
10
- -- created its own board may do to itself. ORQ-11/12 perform that flip-flop and pin both halves: the
11
- -- horizon survives the revert, and the census cannot see that it was taken.
10
+ -- created its own board may do to itself. ORQ-11/12 perform that flip-flop and pin what survives it.
11
+ --
12
+ -- #400 CHANGED WHAT SURVIVES, AND ORQ-11'S CONTRACT WITH IT. When this file shipped, the escape was
13
+ -- UNBOUNDED: the horizon taken during the advisory window survived the revert intact, so one flip
14
+ -- bought as much time as the agent asked for. Reverting to 'inherit' now re-applies the ownerless
15
+ -- TTL cap to that board's active ownerless claims, so a cycle banks at most
16
+ -- `_claimgate_ownerless_ttl_cap()` — what any claim on any inherit board may hold. ORQ-11 asserts
17
+ -- the CLAMP where it used to assert the survival. The escape is BOUNDED, not closed: an owner
18
+ -- flipping before each expiry still keeps an over-quota agent over quota, and ORQ-12 is unchanged
19
+ -- because the census still cannot see that any of it happened.
12
20
  --
13
21
  -- ORQ-1 IS THE ASSERTION THIS FILE EXISTS FOR, and it is written as the attack #388 describes: an
14
22
  -- agent that is already over the ceiling asks for +400 days, which before this migration it got,
@@ -431,6 +439,21 @@ select is(
431
439
  -- monotone clamp guarantees convergence against an agent whose boards do not opt out; it guarantees
432
440
  -- NOTHING against one whose board owner cooperates, and the flip-flop can be repeated forever.
433
441
  --
442
+ -- WHAT #400 TOOK AWAY IS THE HORIZON, NOT THE FLIP. The revert now re-applies the ownerless TTL cap
443
+ -- (BCP-11 owns the mechanics), so ORQ-11b's +400 days does not survive ORQ-11c. ORQ-11 asserts that
444
+ -- clamp. Its contract genuinely CHANGED — it used to assert `now() + 400 days` and now asserts
445
+ -- `now() + _claimgate_ownerless_ttl_cap()` — and the change is legitimate because the BEHAVIOUR
446
+ -- changed underneath it, not because a red test needed to go green: the migration is what makes the
447
+ -- old value impossible, and this assertion is the one #399 deliberately left in place so that
448
+ -- closing the escape could not be done in prose.
449
+ --
450
+ -- IT IS STILL AN ESCAPE, AND THIS ASSERTION STILL DOCUMENTS ONE. Each cycle banks up to the cap, so
451
+ -- an owner flipping every 29 days keeps an over-quota agent over quota indefinitely; convergence is
452
+ -- still defeatable by a board owner, just no longer by a single flip. To hold MORE than the cap the
453
+ -- board must STAY advisory — and while it does, those claims are counted by the census's
454
+ -- claims_on_advisory_boards, which is the column an operator reads. Above the cap the escape is
455
+ -- ineffective or visible; below it, ORQ-12 still applies.
456
+ --
434
457
  -- AND THE AGENT CAN BE ITS OWN OWNER, which is why the fixture builds this board through
435
458
  -- create_board rather than inserting it. create_board seeds the creator's owner membership and
436
459
  -- set_board_claim_policy is gated on exactly that, so no accomplice is required — an agent that
@@ -459,11 +482,12 @@ select lives_ok(
459
482
 
460
483
  select is(
461
484
  (select lease_expires_at from agent_board.claims where lease_token = (select tok from _tok where name = 'over-escape')),
462
- now() + interval '400 days',
463
- 'ORQ-11 THE ESCAPE HATCH: reverting to inherit does NOT take the horizon back — the +400 day lease survives the revert, so an owner who flips advisory/inherit before each expiry keeps an over-quota agent over quota forever. Convergence is guaranteed only against an agent whose boards do not opt out');
485
+ now() + agent_board._claimgate_ownerless_ttl_cap(),
486
+ 'ORQ-11 THE ESCAPE HATCH, BOUNDED (#400): reverting to inherit TAKES THE HORIZON BACK to the ownerless TTL cap — the +400 day lease does not survive the revert, so a flip-flop banks at most what any claim on any inherit board may hold. Still an escape (the owner may flip again before that expiry), no longer an unbounded one');
464
487
 
465
- -- The second half, kept HERE rather than in the census file. The escape and its invisibility are
466
- -- one defect: the census resolves board policy at QUERY time (OQ-29's subject), so once the board
488
+ -- The second half, kept HERE rather than in the census file, and UNCHANGED by #400 — which is the
489
+ -- point of keeping it. Bounding the horizon did nothing about the visibility: the escape and its
490
+ -- invisibility are one defect: the census resolves board policy at QUERY time (OQ-29's subject), so once the board
467
491
  -- is back to 'inherit' this claim contributes ZERO to claims_on_advisory_boards while the overage
468
492
  -- it produced is still counted. An operator reading the census sees an agent over quota with no
469
493
  -- advisory share — the exact shape that says "this one is a squatter, act on it" — and the column
@@ -499,14 +523,19 @@ select is(
499
523
  -- text asserts "acquire_claim/renew_claim are the only SQL writers" — a recorded fact, which rule 5
500
524
  -- says is a claim about the moment it was written. Derived here instead, so a third writer that
501
525
  -- needs this guard fails a test and NAMES ITSELF rather than relying on someone remembering.
526
+ --
527
+ -- IT DID, one release later. #400's cap reset is exactly the third writer this assertion was built
528
+ -- to catch, and it turned this test red before anyone wrote it down. The expected set is widened
529
+ -- deliberately: the reset writes leases, only ever SHORTENING them to the ownerless TTL cap, and the
530
+ -- mechanics are pinned in agent_board_board_claim_policy.test.sql (BCP-11..20).
502
531
  -- ---------------------------------------------------------------------------
503
532
  select is(
504
533
  (select string_agg(p.proname, ', ' order by p.proname)
505
534
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
506
535
  where n.nspname = 'agent_board'
507
536
  and agent_board_test.stripped_body(p.oid, true) ~ '(lease_expires_at\s*=[^=]|insert into agent_board\.claims\s*\([^)]*lease_expires_at)'),
508
- 'acquire_claim, renew_claim',
509
- 'ORQ-14 CLASS: acquire_claim and renew_claim are the ONLY functions that write a claim lease — a new one names itself here instead of quietly inheriting an unconverged renewal path');
537
+ 'acquire_claim, renew_claim, set_board_claim_policy',
538
+ 'ORQ-14 CLASS: acquire_claim, renew_claim and (since #400) set_board_claim_policy''s advisory-revert cap reset are the ONLY functions that write a claim lease — a new one names itself here instead of quietly inheriting an unconverged renewal path');
510
539
 
511
540
  -- ---------------------------------------------------------------------------
512
541
  -- ORQ-15/14: CLASS, one definition of the count. Three hand-copies of a predicate across two
@@ -73,10 +73,22 @@
73
73
  "signature": "_claimgate_board_claim_policy(p_thread_id uuid)",
74
74
  "declares_p_metadata": false
75
75
  },
76
+ {
77
+ "signature": "_claimgate_board_claim_policy_of(p_board_id uuid)",
78
+ "declares_p_metadata": false
79
+ },
80
+ {
81
+ "signature": "_claimgate_board_of_thread(p_thread_id uuid)",
82
+ "declares_p_metadata": false
83
+ },
76
84
  {
77
85
  "signature": "_claimgate_kind_enforced(p_resource_kind text)",
78
86
  "declares_p_metadata": false
79
87
  },
88
+ {
89
+ "signature": "_claimgate_lock_board_policy(p_board_id uuid)",
90
+ "declares_p_metadata": false
91
+ },
80
92
  {
81
93
  "signature": "_claimgate_max_lease_horizon()",
82
94
  "declares_p_metadata": false
@@ -85,6 +97,10 @@
85
97
  "signature": "_claimgate_ownerless_guards_enabled()",
86
98
  "declares_p_metadata": false
87
99
  },
100
+ {
101
+ "signature": "_claimgate_ownerless_kinds()",
102
+ "declares_p_metadata": false
103
+ },
88
104
  {
89
105
  "signature": "_claimgate_ownerless_quota()",
90
106
  "declares_p_metadata": false
@@ -859,3 +859,189 @@ COMMENT ON FUNCTION agent_board_test.lock_edges() IS
859
859
  'FK-aware per-function row-lock edge set: explicit locks, write locks, and the FOR KEY SHARE / child locks PostgreSQL takes through foreign keys. Test-only; see message-board#281.';
860
860
 
861
861
  GRANT USAGE ON SCHEMA agent_board_test TO PUBLIC;
862
+
863
+ -- ---------------------------------------------------------------------------
864
+ -- IDENTIFIER MATCHERS (Consiliency/message-board#405, #417). ONE definition of
865
+ -- "does this body reference agent_board.<name>", used by every assertion that asks.
866
+ --
867
+ -- WHY THESE EXIST. The same lookup was hand-written at ~20 sites and drifted twice in
868
+ -- opposite directions in consecutive review rounds: first too STRICT (an abutting-paren,
869
+ -- schema-qualified, case-sensitive strpos missed `_claimgate_lock_board_policy (v)` and
870
+ -- `"_claimgate_lock_board_policy"(v)`), then too TOLERANT (a `[^A-Za-z0-9_]` guard accepts `.`,
871
+ -- so `other_schema."_claimgate_lock_board_policy"(v)` matched as though it named OUR helper, and
872
+ -- blanket case-insensitivity folded `"AGENT_BOARD"` onto `agent_board` although PostgreSQL treats
873
+ -- those as DISTINCT objects). The target is not "more tolerant" — it is EXACTLY as tolerant as
874
+ -- PostgreSQL's identifier rules, which is a thing to encode once rather than to re-derive per site.
875
+ --
876
+ -- THE RULES ENCODED, verified against the server rather than recalled:
877
+ -- * an UNQUOTED identifier folds to lower case, so any case spelling names the same object
878
+ -- (`to_regclass('AGENT_BOARD.boards') is not distinct from to_regclass('agent_board.boards')`
879
+ -- returns true);
880
+ -- * a QUOTED identifier is case-SENSITIVE, so `"AGENT_BOARD".boards` is a different object
881
+ -- (`to_regclass('"AGENT_BOARD".boards')` returns NULL);
882
+ -- * a reference is ours only if it is UNQUALIFIED or qualified by exactly `agent_board` — any
883
+ -- other qualifier names someone else's object.
884
+ --
885
+ -- CALLERS MUST USE `~` / regexp_instr WITHOUT THE 'i' FLAG. The case rules are inside these
886
+ -- patterns; adding 'i' would fold the quoted alternative and reintroduce the second defect.
887
+ -- Keywords that should stay case-insensitive get agent_board_test.ci_re() for the same reason.
888
+
889
+ -- A SPACE IN p_text BECOMES `\s+`, because every space this is ever given separates the words of a
890
+ -- multiword SQL keyword (`insert into`, `no key`, `key share`) and PostgreSQL accepts any whitespace
891
+ -- run there. stmt_re used to do that rewrite on ci_re's OUTPUT — a caller textually transforming a
892
+ -- shared matcher, which is the same shape as CPR-6 stripping a director, and fragile in the same
893
+ -- way: it depended on ci_re never emitting a space for some other reason. The rule belongs in the
894
+ -- matcher, so no call site edits what a matcher returns.
895
+ create or replace function agent_board_test.ci_re(p_text text) returns text
896
+ language sql immutable set search_path to pg_catalog as $$
897
+ select coalesce(string_agg(case when c ~ '[A-Za-z]' then '[' || upper(c) || lower(c) || ']'
898
+ when c = ' ' then '\s+'
899
+ else c end, ''), '')
900
+ from regexp_split_to_table(p_text, '') as c
901
+ $$;
902
+
903
+ create or replace function agent_board_test.ident_re(p_name text) returns text
904
+ language sql immutable set search_path to pg_catalog as $$
905
+ -- p_name is the object's real (lower-case) name: unquoted-any-case, or quoted-exactly.
906
+ select '(?:' || agent_board_test.ci_re(p_name) || '|"' || p_name || '")'
907
+ $$;
908
+
909
+ -- IDENT_CHARS is PostgreSQL's unquoted-identifier continuation set, and `$` IS IN IT: `$` is legal
910
+ -- after the first character, so `x$boards` is a DISTINCT identifier whose suffix would otherwise
911
+ -- satisfy a `boards` matcher, and `x$_claimgate_lock_board_policy(…)` a distinct FUNCTION whose
912
+ -- suffix would be credited as the protected helper. Omitting `$` was a real over-tolerance, found
913
+ -- by review and reproduced before fixing.
914
+ create or replace function agent_board_test.ident_chars() returns text
915
+ language sql immutable set search_path to pg_catalog as $$ select 'A-Za-z0-9_$' $$;
916
+
917
+ -- THE BOUNDARY GUARDS. Three rejections, each a defect that shipped:
918
+ -- * an ASCII identifier character or a quote immediately before/after — `dashboards`, `x$boards`;
919
+ -- * a NON-ASCII letter, which PostgreSQL also admits in an unquoted identifier, so
920
+ -- `é_claimgate_lock_board_policy` and `claim_policyé` are DISTINCT objects whose prefix/suffix
921
+ -- would otherwise match. The `[A-Za-z0-9_$]` class alone cannot express this;
922
+ -- * a DOT WITH ANY WHITESPACE before the name. `other . _claimgate_lock_board_policy()` is a
923
+ -- different schema's call, and the single-character class let it through because the character
924
+ -- immediately before the name was a SPACE — the round-9 schema-blindness defect reopened through
925
+ -- whitespace. PostgreSQL accepts variable-length lookbehind, verified, so `(?<!\.\s*)` states
926
+ -- it directly rather than approximating with a fixed number of spaces.
927
+ create or replace function agent_board_test.lead_guard() returns text
928
+ language sql immutable set search_path to pg_catalog as $$
929
+ select '(?<![' || agent_board_test.ident_chars() || '."])(?<![^\x00-\x7F])(?<!\.\s*)'
930
+ $$;
931
+
932
+ create or replace function agent_board_test.trail_guard() returns text
933
+ language sql immutable set search_path to pg_catalog as $$
934
+ select '(?![' || agent_board_test.ident_chars() || '])(?![^\x00-\x7F])'
935
+ $$;
936
+
937
+ -- The un-prefixed fragments. Public matchers add the (?c) director exactly once; these exist so a
938
+ -- matcher can embed another without producing a mid-pattern director, which PostgreSQL rejects.
939
+ create or replace function agent_board_test.qname_frag(p_name text) returns text
940
+ language sql immutable set search_path to pg_catalog as $$
941
+ select agent_board_test.lead_guard() || '(?:' || agent_board_test.ident_re('agent_board')
942
+ || '\s*\.\s*)?' || agent_board_test.ident_re(p_name) || agent_board_test.trail_guard()
943
+ $$;
944
+
945
+ create or replace function agent_board_test.call_frag(p_name text) returns text
946
+ language sql immutable set search_path to pg_catalog as $$
947
+ select agent_board_test.lead_guard() || '(?:' || agent_board_test.ident_re('agent_board')
948
+ || '\s*\.\s*)?' || agent_board_test.ident_re(p_name) || '\s*\('
949
+ $$;
950
+
951
+ -- THE (?c) DIRECTOR IS THE ENFORCEMENT, not the comment above it. A caller that passes 'i' to
952
+ -- regexp_instr/regexp_matches alongside one of these patterns would previously refold the quoted
953
+ -- alternative and credit `"AGENT_BOARD"."claims"` as `agent_board.claims`. That discipline was
954
+ -- DOCUMENTED and then violated in the same commit that documented it, at four call sites. `(?c)`
955
+ -- forces case-sensitive matching from inside the pattern, so a stray 'i' is now overridden rather
956
+ -- than obeyed — verified: regexp_instr(…, '(?c)"agent_board"', …, 'i') returns 0.
957
+ -- And misuse fails LOUDLY: a director is only legal at the START of a regular expression, so a
958
+ -- caller who prefixes anything gets `invalid regular expression` rather than a silent fold.
959
+ --
960
+ -- EXACTLY WHICH CALLERS THE DIRECTOR PROTECTS, stated narrowly because the first version of this
961
+ -- paragraph claimed more: a pattern that BEGINS with qname_re/call_re/stmt_re is protected against a
962
+ -- passed 'i' flag AND against the `~*` operator, because the director is inside the pattern. A
963
+ -- caller that composes a *_frag directly is NOT protected — the frags carry no director by design,
964
+ -- since one mid-pattern is a syntax error. They exist for embedding inside the _re matchers, and
965
+ -- EXECUTE on them is revoked below so they are not casually reachable; a test author who calls a
966
+ -- frag with `~*` deliberately is outside what any mechanism here can prevent, and that residue is
967
+ -- named rather than papered over.
968
+ create or replace function agent_board_test.qname_re(p_name text) returns text
969
+ language sql immutable set search_path to pg_catalog as $$
970
+ select '(?c)' || agent_board_test.qname_frag(p_name)
971
+ $$;
972
+
973
+ create or replace function agent_board_test.call_re(p_name text) returns text
974
+ language sql immutable set search_path to pg_catalog as $$
975
+ select '(?c)' || agent_board_test.call_frag(p_name)
976
+ $$;
977
+
978
+ -- A COLUMN token. Unquoted identifiers fold, so `SET CLAIM_POLICY = …` names the same column as
979
+ -- `claim_policy` — the case discipline reached the qualifier and the keywords but not the column,
980
+ -- so an upper-case column token was silently omitted from CPR-6 and CPR-12.
981
+ create or replace function agent_board_test.col_frag(p_name text) returns text
982
+ language sql immutable set search_path to pg_catalog as $$
983
+ select agent_board_test.lead_guard() || agent_board_test.ident_re(p_name) || agent_board_test.trail_guard()
984
+ $$;
985
+
986
+ create or replace function agent_board_test.stmt_re(p_verbs text, p_rel text) returns text
987
+ language sql immutable set search_path to pg_catalog as $$
988
+ -- "a statement whose verb is one of p_verbs and whose target is agent_board.p_rel".
989
+ -- SPACES INSIDE A MULTIWORD VERB BECOME `\s+`: `INSERT\nINTO` and `DELETE FROM` are the same
990
+ -- statement to PostgreSQL. The hand-rolled expressions this consolidated used `\s+` and got that
991
+ -- right; the first shared version made the space literal and LOST a property the scattered
992
+ -- versions had — a consolidation is only safe if it is at least the union of what it replaces.
993
+ select '(?c)(?:' || (select string_agg(agent_board_test.ci_re(v), '|')
994
+ from regexp_split_to_table(p_verbs, '\|') as v)
995
+ || ')\s+(?:' || agent_board_test.ci_re('only') || '\s+)?'
996
+ || agent_board_test.qname_frag(p_rel)
997
+ $$;
998
+
999
+ revoke all on function agent_board_test.qname_frag(text) from public;
1000
+ revoke all on function agent_board_test.call_frag(text) from public;
1001
+ revoke all on function agent_board_test.col_frag(text) from public;
1002
+
1003
+ -- THE INSERT-WITHOUT-COLUMN-LIST RULE, AS A NAMED OBJECT. It began as an expression inlined in
1004
+ -- CPR-6, and CPR-0's rows for it RECONSTRUCTED that expression instead of calling it — so the rows
1005
+ -- tested a copy, the copy could not diverge from itself, and mutating the production arm left them
1006
+ -- green. That is precisely the defect CPR-0 exists to catch, committed in CPR-0's own newest rows.
1007
+ -- A behaviour-table row must exercise THE SAME code object the production path compiles; anything
1008
+ -- else certifies a parallel implementation.
1009
+ --
1010
+ -- `insert into <rel> …` with no column list writes EVERY column, so for a sole-writer claim it is a
1011
+ -- writer of every column including the one being tracked. Three things this has to get right:
1012
+ -- * `(?!\s*\()` not `\s*(?!\()` — BACKTRACKING defeats the latter, since `\s*` may match zero
1013
+ -- characters and the lookahead then passes on the whitespace itself;
1014
+ -- * an ALIAS may sit between the relation and the column list, and it may be QUOTED
1015
+ -- (`insert into boards as "b"(slug, …)`), so the alias alternative admits both spellings;
1016
+ -- * VALUES / SELECT / DEFAULT / OVERRIDING are KEYWORDS, not aliases — treating them as aliases
1017
+ -- would exclude the genuine no-column-list forms this rule exists for.
1018
+ -- SPLIT INTO frag + re, matching qname/call/col, because CPR-6 embeds this rule INSIDE a larger
1019
+ -- alternation and a director is only legal at position 0. It used to embed
1020
+ -- `replace(insert_all_columns_re(...), '(?c)', '')` — so CPR-0's rows compiled X while production
1021
+ -- compiled a TEXTUAL TRANSFORMATION of X. Behaviourally identical today (CPR-6 matches with `~`,
1022
+ -- which is case-sensitive anyway, so the director is belt-and-braces there), but the rows would have
1023
+ -- stayed green if the rule ever became director-dependent, or if a `(?c)` appeared anywhere but
1024
+ -- position 0 and the replace stripped it silently. Two strings are not one object. Now CPR-6 embeds
1025
+ -- the frag and CPR-0's rows exercise the frag: the same object, no transformation at any call site.
1026
+ create or replace function agent_board_test.insert_all_columns_frag(p_rel text) returns text
1027
+ language sql immutable set search_path to pg_catalog as $$
1028
+ select agent_board_test.ci_re('insert') || '\s+' || agent_board_test.ci_re('into')
1029
+ || '\s+(?:' || agent_board_test.ci_re('only') || '\s+)?'
1030
+ || agent_board_test.qname_frag(p_rel)
1031
+ || '(?!(?:\s+(?:' || agent_board_test.ci_re('as') || '\s+)?(?!'
1032
+ -- ONLY VALUES AND SELECT. `default` and `overriding` were excluded here too and were DEAD
1033
+ -- CODE: an exclusion only matters if the keyword can be followed by `\s*\(` and so be mistaken
1034
+ -- for an alias, and DEFAULT is only ever followed by VALUES, OVERRIDING by SYSTEM/USER.
1035
+ -- Verified by reverting each: the two live exclusions change a verdict, the two dead ones do
1036
+ -- not. Removed rather than kept with rows that cannot gate them.
1037
+ || agent_board_test.ci_re('values') || '(?![A-Za-z0-9_$])|'
1038
+ || agent_board_test.ci_re('select') || '(?![A-Za-z0-9_$]))'
1039
+ || '(?:"[^"]*"|[A-Za-z_][A-Za-z0-9_$]*))?\s*\()'
1040
+ $$;
1041
+
1042
+ create or replace function agent_board_test.insert_all_columns_re(p_rel text) returns text
1043
+ language sql immutable set search_path to pg_catalog as $$
1044
+ select '(?c)' || agent_board_test.insert_all_columns_frag(p_rel)
1045
+ $$;
1046
+
1047
+ revoke all on function agent_board_test.insert_all_columns_frag(text) from public;
@@ -0,0 +1,66 @@
1
+ Parsed test spec with 3 sessions
2
+
3
+ starting permutation: w_begin w_flip a_begin a_acq w_commit a_commit chk
4
+ step w_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000a1';
5
+ step w_flip:
6
+ select claim_policy
7
+ from agent_board.set_board_claim_policy(
8
+ (select id from agent_board.boards where slug = 'cpr-acq-board'), 'inherit');
9
+
10
+ claim_policy
11
+ ------------
12
+ inherit
13
+ (1 row)
14
+
15
+ step a_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000a1';
16
+ step a_acq:
17
+ select resource_key
18
+ from agent_board.acquire_claim(null, 'file', 'cpr-acq-key', now() + interval '400 days',
19
+ null, '40500000-0000-4000-8000-000000000010');
20
+ <waiting ...>
21
+ step w_commit: COMMIT;
22
+ step a_acq: <... completed>
23
+ ERROR: ownerless claim lease horizon exceeds cap (30 days)
24
+ step a_commit: COMMIT;
25
+ step chk:
26
+ select count(*)::int as active_claims,
27
+ coalesce(bool_or(lease_expires_at > now() + interval '31 days'), false) as over_cap_lease
28
+ from agent_board.claims
29
+ where agent_id = '40500000-0000-4000-8000-000000000001'
30
+ and resource_kind = 'file'
31
+ and status = 'active'
32
+ and released_at is null;
33
+
34
+ active_claims|over_cap_lease
35
+ -------------+--------------
36
+ 0|f
37
+ (1 row)
38
+
39
+
40
+ starting permutation: a_begin a_acq a_commit chk
41
+ step a_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000a1';
42
+ step a_acq:
43
+ select resource_key
44
+ from agent_board.acquire_claim(null, 'file', 'cpr-acq-key', now() + interval '400 days',
45
+ null, '40500000-0000-4000-8000-000000000010');
46
+
47
+ resource_key
48
+ ------------
49
+ cpr-acq-key
50
+ (1 row)
51
+
52
+ step a_commit: COMMIT;
53
+ step chk:
54
+ select count(*)::int as active_claims,
55
+ coalesce(bool_or(lease_expires_at > now() + interval '31 days'), false) as over_cap_lease
56
+ from agent_board.claims
57
+ where agent_id = '40500000-0000-4000-8000-000000000001'
58
+ and resource_kind = 'file'
59
+ and status = 'active'
60
+ and released_at is null;
61
+
62
+ active_claims|over_cap_lease
63
+ -------------+--------------
64
+ 1|t
65
+ (1 row)
66
+
@@ -0,0 +1,73 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: w_begin w_flip a1_begin a1_acq w_commit a1_commit chk
4
+ step w_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000c1';
5
+ step w_flip:
6
+ select claim_policy
7
+ from agent_board.set_board_claim_policy(
8
+ (select id from agent_board.boards where slug = 'cpr-scope-b1'), 'inherit');
9
+
10
+ claim_policy
11
+ ------------
12
+ inherit
13
+ (1 row)
14
+
15
+ step a1_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000c1';
16
+ step a1_acq:
17
+ select resource_key
18
+ from agent_board.acquire_claim(null, 'file', 'cpr-scope-key-b1', now() + interval '400 days',
19
+ null, '40500000-0000-4000-8000-000000000041');
20
+ <waiting ...>
21
+ step w_commit: COMMIT;
22
+ step a1_acq: <... completed>
23
+ ERROR: ownerless claim lease horizon exceeds cap (30 days)
24
+ step a1_commit: COMMIT;
25
+ step chk:
26
+ select resource_key, (lease_expires_at > now() + interval '31 days') as over_cap_lease
27
+ from agent_board.claims
28
+ where agent_id = '40500000-0000-4000-8000-000000000003'
29
+ and status = 'active' and released_at is null
30
+ order by resource_key;
31
+
32
+ resource_key|over_cap_lease
33
+ ------------+--------------
34
+ (0 rows)
35
+
36
+
37
+ starting permutation: w_begin w_flip a2_begin a2_acq w_commit a2_commit chk
38
+ step w_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000c1';
39
+ step w_flip:
40
+ select claim_policy
41
+ from agent_board.set_board_claim_policy(
42
+ (select id from agent_board.boards where slug = 'cpr-scope-b1'), 'inherit');
43
+
44
+ claim_policy
45
+ ------------
46
+ inherit
47
+ (1 row)
48
+
49
+ step a2_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000c1';
50
+ step a2_acq:
51
+ select resource_key
52
+ from agent_board.acquire_claim(null, 'file', 'cpr-scope-key-b2', now() + interval '400 days',
53
+ null, '40500000-0000-4000-8000-000000000042');
54
+
55
+ resource_key
56
+ ----------------
57
+ cpr-scope-key-b2
58
+ (1 row)
59
+
60
+ step w_commit: COMMIT;
61
+ step a2_commit: COMMIT;
62
+ step chk:
63
+ select resource_key, (lease_expires_at > now() + interval '31 days') as over_cap_lease
64
+ from agent_board.claims
65
+ where agent_id = '40500000-0000-4000-8000-000000000003'
66
+ and status = 'active' and released_at is null
67
+ order by resource_key;
68
+
69
+ resource_key |over_cap_lease
70
+ ----------------+--------------
71
+ cpr-scope-key-b2|t
72
+ (1 row)
73
+
@@ -0,0 +1,80 @@
1
+ Parsed test spec with 4 sessions
2
+
3
+ starting permutation: rc_begin rc_reclaim w_begin w_flip rc_commit w_commit chk
4
+ step rc_begin: BEGIN; SET LOCAL "request.jwt.claim.role" = 'service_role';
5
+ step rc_reclaim:
6
+ select owner_agent_id
7
+ from agent_board.reclaim_fleet_machine(
8
+ '40500000-0000-4000-8000-0000000000c0', '40500000-0000-4000-8000-00000000000b',
9
+ '40500000-0000-4000-8000-00000000000a', 'message-board#405 lock-order probe');
10
+
11
+ owner_agent_id
12
+ ------------------------------------
13
+ 40500000-0000-4000-8000-00000000000b
14
+ (1 row)
15
+
16
+ step w_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000f9';
17
+ step w_flip:
18
+ select claim_policy
19
+ from agent_board.set_board_claim_policy(
20
+ (select id from agent_board.boards where slug = 'cpr-inv-board'), 'inherit');
21
+
22
+ claim_policy
23
+ ------------
24
+ inherit
25
+ (1 row)
26
+
27
+ step rc_commit: COMMIT;
28
+ step w_commit: COMMIT;
29
+ step chk:
30
+ select (select claim_policy from agent_board.boards where slug = 'cpr-inv-board') as policy,
31
+ (select owner_agent_id from agent_board.fleet_machines
32
+ where id = '40500000-0000-4000-8000-0000000000c0') as machine_owner;
33
+
34
+ policy |machine_owner
35
+ -------+------------------------------------
36
+ inherit|40500000-0000-4000-8000-00000000000b
37
+ (1 row)
38
+
39
+
40
+ starting permutation: x_begin x_rowlock w_begin w_flip x_renew w_commit x_commit chk
41
+ step x_begin: BEGIN;
42
+ step x_rowlock: select resource_key from agent_board.claims where id = '40500000-0000-4000-8000-000000000091' for update;
43
+ resource_key
44
+ ------------
45
+ cpr-inv-key
46
+ (1 row)
47
+
48
+ step w_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000f9';
49
+ step w_flip:
50
+ select claim_policy
51
+ from agent_board.set_board_claim_policy(
52
+ (select id from agent_board.boards where slug = 'cpr-inv-board'), 'inherit');
53
+ <waiting ...>
54
+ step x_renew:
55
+ SET LOCAL ROLE authenticated;
56
+ SET LOCAL "request.jwt.claim.role" = 'authenticated';
57
+ SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000f9';
58
+ select resource_key
59
+ from agent_board.renew_claim('40500000-0000-4000-8000-0000000000f8', now() + interval '400 days');
60
+ <waiting ...>
61
+ step w_flip: <... completed>
62
+ ERROR: deadlock detected
63
+ step x_renew: <... completed>
64
+ resource_key
65
+ ------------
66
+ cpr-inv-key
67
+ (1 row)
68
+
69
+ step w_commit: COMMIT;
70
+ step x_commit: COMMIT;
71
+ step chk:
72
+ select (select claim_policy from agent_board.boards where slug = 'cpr-inv-board') as policy,
73
+ (select owner_agent_id from agent_board.fleet_machines
74
+ where id = '40500000-0000-4000-8000-0000000000c0') as machine_owner;
75
+
76
+ policy | machine_owner
77
+ --------+------------------------------------
78
+ advisory|40500000-0000-4000-8000-000000000009
79
+ (1 row)
80
+
@@ -0,0 +1,114 @@
1
+ Parsed test spec with 5 sessions
2
+
3
+ starting permutation: rr_begin rr_acq rr_rollback chk
4
+ step rr_begin: BEGIN ISOLATION LEVEL REPEATABLE READ; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
5
+ step rr_acq:
6
+ select resource_key
7
+ from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
8
+ null, '40500000-0000-4000-8000-000000000050');
9
+
10
+ ERROR: claim policy enforcement requires transaction isolation level read committed (got repeatable read)
11
+ step rr_rollback: ROLLBACK;
12
+ step chk:
13
+ select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
14
+ from agent_board.claims
15
+ where agent_id = '40500000-0000-4000-8000-000000000004'
16
+ and status = 'active' and released_at is null;
17
+
18
+ claims
19
+ ------
20
+ <none>
21
+ (1 row)
22
+
23
+
24
+ starting permutation: rc_begin rc_acq rc_commit chk
25
+ step rc_begin: BEGIN; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
26
+ step rc_acq:
27
+ select resource_key
28
+ from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
29
+ null, '40500000-0000-4000-8000-000000000050');
30
+
31
+ resource_key
32
+ ------------
33
+ cpr-iso-key
34
+ (1 row)
35
+
36
+ step rc_commit: COMMIT;
37
+ step chk:
38
+ select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
39
+ from agent_board.claims
40
+ where agent_id = '40500000-0000-4000-8000-000000000004'
41
+ and status = 'active' and released_at is null;
42
+
43
+ claims
44
+ -----------
45
+ cpr-iso-key
46
+ (1 row)
47
+
48
+
49
+ starting permutation: rr_begin rr_machine rr_rollback chk
50
+ step rr_begin: BEGIN ISOLATION LEVEL REPEATABLE READ; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
51
+ step rr_machine:
52
+ select resource_key
53
+ from agent_board.acquire_claim(null, 'machine', 'cpr-iso-machine', now() + interval '1 hour',
54
+ null, null);
55
+
56
+ resource_key
57
+ ---------------
58
+ cpr-iso-machine
59
+ (1 row)
60
+
61
+ step rr_rollback: ROLLBACK;
62
+ step chk:
63
+ select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
64
+ from agent_board.claims
65
+ where agent_id = '40500000-0000-4000-8000-000000000004'
66
+ and status = 'active' and released_at is null;
67
+
68
+ claims
69
+ ------
70
+ <none>
71
+ (1 row)
72
+
73
+
74
+ starting permutation: sz_begin sz_acq sz_rollback chk
75
+ step sz_begin: BEGIN ISOLATION LEVEL SERIALIZABLE; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
76
+ step sz_acq:
77
+ select resource_key
78
+ from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
79
+ null, '40500000-0000-4000-8000-000000000050');
80
+
81
+ ERROR: claim policy enforcement requires transaction isolation level read committed (got serializable)
82
+ step sz_rollback: ROLLBACK;
83
+ step chk:
84
+ select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
85
+ from agent_board.claims
86
+ where agent_id = '40500000-0000-4000-8000-000000000004'
87
+ and status = 'active' and released_at is null;
88
+
89
+ claims
90
+ ------
91
+ <none>
92
+ (1 row)
93
+
94
+
95
+ starting permutation: ru_begin ru_acq ru_rollback chk
96
+ step ru_begin: BEGIN ISOLATION LEVEL READ UNCOMMITTED; SET LOCAL ROLE authenticated; SET LOCAL "request.jwt.claim.role" = 'authenticated'; SET LOCAL "request.jwt.claim.sub" = '40500000-0000-4000-8000-0000000000d1';
97
+ step ru_acq:
98
+ select resource_key
99
+ from agent_board.acquire_claim(null, 'file', 'cpr-iso-key', now() + interval '1 hour',
100
+ null, '40500000-0000-4000-8000-000000000050');
101
+
102
+ ERROR: claim policy enforcement requires transaction isolation level read committed (got read uncommitted)
103
+ step ru_rollback: ROLLBACK;
104
+ step chk:
105
+ select coalesce(string_agg(resource_key, ',' order by resource_key), '<none>') as claims
106
+ from agent_board.claims
107
+ where agent_id = '40500000-0000-4000-8000-000000000004'
108
+ and status = 'active' and released_at is null;
109
+
110
+ claims
111
+ ------
112
+ <none>
113
+ (1 row)
114
+