@consiliency/agent-board-schema 1.28.0 → 1.35.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 +28 -0
- package/migrations/20260910000100_agent_board_ceiling_lock_comment_truth.sql +89 -0
- package/migrations/20260911000100_agent_board_ledger_revoked_board_creation.sql +330 -0
- package/migrations/20260912000100_agent_board_receipt_subject_gate.sql +348 -0
- package/migrations/20260913000100_agent_board_task_thread_lock_order.sql +292 -0
- package/migrations/20260914000100_agent_board_claim_subject_gate.sql +500 -0
- package/migrations/20260915000100_agent_board_fedsession_stored_identity_gate.sql +353 -0
- package/migrations/20260916000100_agent_board_federated_identity_injective_slug.sql +1646 -0
- package/package.json +1 -1
- package/schema-manifest.json +66 -3
- package/src/tooling.test.mjs +47 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +144 -29
- package/tests/agent_board_claim_subject_gate.test.sql +1025 -0
- package/tests/agent_board_enroll_resumption.test.sql +7 -1
- package/tests/agent_board_federated_identity_injective_slug.test.sql +388 -0
- package/tests/agent_board_fedsession.test.sql +17 -2
- package/tests/agent_board_fedsession_stored_identity_gate.test.sql +606 -0
- package/tests/agent_board_fk_lock_edges.test.sql +422 -0
- package/tests/agent_board_ledger_authority.test.sql +142 -71
- package/tests/agent_board_receipt_subject_gate.test.sql +624 -0
- package/tests/agent_board_signer_lifecycle.test.sql +23 -6
- package/tests/agent_board_source_strip_helper.test.sql +157 -1
- package/tests/agent_board_task_thread_lock_order.test.sql +337 -0
- package/tests/catalog/p_metadata_parameters.json +12 -0
- package/tests/fixtures/test-prereqs.sql +420 -11
- package/tests/isolation/expected/ledger_authority_revoke.out +219 -1
- package/tests/isolation/expected/task_thread_lock_order.out +203 -0
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +5 -4
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +4 -4
- package/tests/isolation/specs/ledger_authority_revoke.spec +115 -0
- package/tests/isolation/specs/task_thread_lock_order.spec +197 -0
- package/tests/seeds/20260916000100_agent_board_federated_identity_injective_slug.sql.pre.sql +196 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consiliency/agent-board-schema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"description": "Canonical, immutable, exact-pinnable agent_board schema bundle: ordered SQL migrations, digest + compatibility manifest, and deterministic materialize/plan/check tooling for adopters.",
|
|
5
5
|
"repository": "https://github.com/Consiliency/message-board.git",
|
|
6
6
|
"type": "module",
|
package/schema-manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema": "agent_board_schema_manifest.v1",
|
|
3
3
|
"interface": "IF-0-MBPLANE-BUNDLE-1",
|
|
4
4
|
"package": "@consiliency/agent-board-schema",
|
|
5
|
-
"packageVersion": "1.
|
|
5
|
+
"packageVersion": "1.35.0",
|
|
6
6
|
"schemaVersion": 8,
|
|
7
7
|
"minClientVersion": "1.1.0",
|
|
8
8
|
"source": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"usage": "upsert (retention/monitoring defaults; on conflict do nothing)"
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
-
"migrationCount":
|
|
50
|
-
"aggregateSha256": "
|
|
49
|
+
"migrationCount": 134,
|
|
50
|
+
"aggregateSha256": "f63027fa44451c55a39fcc8f463df6ae6f4b92765578be053d8bf3b9a2bee07e",
|
|
51
51
|
"migrations": [
|
|
52
52
|
{
|
|
53
53
|
"order": 0,
|
|
@@ -1191,6 +1191,69 @@
|
|
|
1191
1191
|
"sourceRef": "message-board",
|
|
1192
1192
|
"bytes": 67493,
|
|
1193
1193
|
"sha256": "7522e32c61f8845e511255d94d273d98eb7b72b2f906976c3a2494629927462d"
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
"order": 127,
|
|
1197
|
+
"id": "20260910000100_agent_board_ceiling_lock_comment_truth",
|
|
1198
|
+
"filename": "20260910000100_agent_board_ceiling_lock_comment_truth.sql",
|
|
1199
|
+
"origin": "message-board-forward",
|
|
1200
|
+
"sourceRef": "message-board",
|
|
1201
|
+
"bytes": 7087,
|
|
1202
|
+
"sha256": "b8ddc658be347d9e7207111968d0b8de7a88759cb3cf181f61c4ef306086f384"
|
|
1203
|
+
},
|
|
1204
|
+
{
|
|
1205
|
+
"order": 128,
|
|
1206
|
+
"id": "20260911000100_agent_board_ledger_revoked_board_creation",
|
|
1207
|
+
"filename": "20260911000100_agent_board_ledger_revoked_board_creation.sql",
|
|
1208
|
+
"origin": "message-board-forward",
|
|
1209
|
+
"sourceRef": "message-board",
|
|
1210
|
+
"bytes": 22749,
|
|
1211
|
+
"sha256": "4291af2bd6c93b7d9f8ec596f41dc62edf941ea10a51529df04e223d1cbc50c5"
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
"order": 129,
|
|
1215
|
+
"id": "20260912000100_agent_board_receipt_subject_gate",
|
|
1216
|
+
"filename": "20260912000100_agent_board_receipt_subject_gate.sql",
|
|
1217
|
+
"origin": "message-board-forward",
|
|
1218
|
+
"sourceRef": "message-board",
|
|
1219
|
+
"bytes": 19033,
|
|
1220
|
+
"sha256": "1083f7720ccab9c9b422415f2a8eb743de5d84b0ab8d8689059bd355514e9aa9"
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
"order": 130,
|
|
1224
|
+
"id": "20260913000100_agent_board_task_thread_lock_order",
|
|
1225
|
+
"filename": "20260913000100_agent_board_task_thread_lock_order.sql",
|
|
1226
|
+
"origin": "message-board-forward",
|
|
1227
|
+
"sourceRef": "message-board",
|
|
1228
|
+
"bytes": 17738,
|
|
1229
|
+
"sha256": "259a05c4a6603518ab449e799fcb235b0e16a6650954cb2db19b436c0a7a1e83"
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
"order": 131,
|
|
1233
|
+
"id": "20260914000100_agent_board_claim_subject_gate",
|
|
1234
|
+
"filename": "20260914000100_agent_board_claim_subject_gate.sql",
|
|
1235
|
+
"origin": "message-board-forward",
|
|
1236
|
+
"sourceRef": "message-board",
|
|
1237
|
+
"bytes": 34342,
|
|
1238
|
+
"sha256": "88207b2e3f28ec8022ad4ca921721ebba877668042c66b5ac74c85e4c8cefc21"
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
"order": 132,
|
|
1242
|
+
"id": "20260915000100_agent_board_fedsession_stored_identity_gate",
|
|
1243
|
+
"filename": "20260915000100_agent_board_fedsession_stored_identity_gate.sql",
|
|
1244
|
+
"origin": "message-board-forward",
|
|
1245
|
+
"sourceRef": "message-board",
|
|
1246
|
+
"bytes": 18615,
|
|
1247
|
+
"sha256": "5a8f4abb822e30c2a695a310bb1742f2565bb2afd0b6a06cad43fcbdd106ad5e"
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
"order": 133,
|
|
1251
|
+
"id": "20260916000100_agent_board_federated_identity_injective_slug",
|
|
1252
|
+
"filename": "20260916000100_agent_board_federated_identity_injective_slug.sql",
|
|
1253
|
+
"origin": "message-board-forward",
|
|
1254
|
+
"sourceRef": "message-board",
|
|
1255
|
+
"bytes": 84162,
|
|
1256
|
+
"sha256": "d9f05e18094954f2e4ee32bf7b129f0a9ffc39bc800a5efa9cf7938e1555c8dc"
|
|
1194
1257
|
}
|
|
1195
1258
|
],
|
|
1196
1259
|
"excludedBridge": {
|
package/src/tooling.test.mjs
CHANGED
|
@@ -38,9 +38,9 @@ describe('manifest integrity', () => {
|
|
|
38
38
|
expect(manifest.schemaVersion).toBe(8);
|
|
39
39
|
expect(manifest.minClientVersion).toMatch(/^\d+\.\d+\.\d+$/);
|
|
40
40
|
});
|
|
41
|
-
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 =
|
|
41
|
+
it('registers the four MBAUTHZ + four MBSENDAUTHZ + four MBSESSION + four MBDELEG + three MBFED + three MBHARDEN + two MBPORTALGATE + two MBPORTAL-PR32 + two ROOMOWN + two ROOMLINK + one ROOMDELEG + one LOCALAUTH + one ROOMUIAPI + one FEDSESSION + one FEDSESSION-AUTOJOIN + one GATENULL + one MEMBSTATE + one AGENTSDIR + one LEAVEIDEM + one ROLERESET + one MBROWID + one MBHYGIENE + one MBCONSENT-A + one MBCONSENT-B(a) + one MBCONSENT-B(i) + one MBCONSENT-B(ii) + one MBCONSENT-B(iii) + one MBHARDEN-#97 + one MBHARDEN-#118 + one INVITEPOLICY + one OBSGATE + one OBSGATE-2 + one OBSGATE-3 + one OBSGATE-4 + one FLEETMACHINERLS + one RESOLVENOOP + one ROOMDELEG-LANEB + one CLAIMKEY + two MINTHARDEN + one FLEETMACHINEHIJACK + one CLAIMGATE-SHADOW + one CLAIMGATE-ENFORCE + one FLEETMACHINEREACTIVATION + one DELEGCAPCEILING + one FLEETMACHINERECLAIM + one DELEGSOLEWRITER + one FLEETDOMAINRECLAIM + five ATOMICFLEETONBOARDING (onboarding + ceiling-lock + revocation-evidence + fedsession-gate + fedsession-lock-order) + one OUTBOXWAKE + one ENROLLLOCKORDER + one FEDPROVIDERABSENCE + one WAKERECOVERY + one CONSENTRECONCILE + one ENROLLRESUMPTION + one SIGNEDAUTHORITY + one SIGNERLIFECYCLE + one LEDGERAUTHORITY + one CEILINGLOCKCOMMENT + one LEDGERBOARDCREATE + one RECEIPTSUBJECTGATE + one TASKTHREADLOCKORDER + one CLAIMSUBJECTGATE + one FEDSESSIONIDENTITYGATE + one FEDIDENTITYSLUG forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 134)', () => {
|
|
42
42
|
expect(manifest.migrationCount).toBe(manifest.migrations.length);
|
|
43
|
-
expect(manifest.migrationCount).toBe(
|
|
43
|
+
expect(manifest.migrationCount).toBe(134);
|
|
44
44
|
// MBSESSION switched enforcement on (scoped-send re-grant): the client-visible
|
|
45
45
|
// schema version bumped to 8. MBDELEG tightens delegation and MBFED adds only
|
|
46
46
|
// enrollment-only additive surface (identity_providers registry, enrollment audit,
|
|
@@ -141,6 +141,51 @@ describe('manifest integrity', () => {
|
|
|
141
141
|
// MBCONSENT-B(iii) (#109/#110 + the gating race): validate and mutate the SAME
|
|
142
142
|
// row. The sink gains an optional row identity and event kind.
|
|
143
143
|
'20260810000100_agent_board_consent_locking.sql',
|
|
144
|
+
// RECEIPTSUBJECTGATE (#287): a read receipt is a MEMO of access, never a SOURCE of it --
|
|
145
|
+
// mark_read requires the NAMED agent to pass the thread-access predicate, closing the
|
|
146
|
+
// route by which service_role could mint an inherit-thread grant for a third party. The
|
|
147
|
+
// predicate is parameterized (_agent_can_access_thread) and the current-agent form
|
|
148
|
+
// recomposed from it, so the disjunct set has ONE definition. The receipt grandfather is
|
|
149
|
+
// deliberately preserved and documented, not removed.
|
|
150
|
+
'20260912000100_agent_board_receipt_subject_gate.sql',
|
|
151
|
+
// CLAIMSUBJECTGATE (#291): a claim is a LOCK, never a KEY -- acquire_claim requires the
|
|
152
|
+
// NAMED agent to pass the thread-access predicate before a thread-bound claim is written,
|
|
153
|
+
// closing the sibling of the #287 mint on agent_board.claims (and, through the thread
|
|
154
|
+
// disjunct of _current_agent_can_access_task, on task access). The permanence of a
|
|
155
|
+
// released or expired claim is RATIFIED here rather than fixed, in comment on function
|
|
156
|
+
// and in specs/security.md, so a future reader cannot mistake it for an oversight.
|
|
157
|
+
'20260914000100_agent_board_claim_subject_gate.sql',
|
|
158
|
+
// TASKTHREADLOCKORDER (#281 R1): one total order for (threads, tasks) -- threads BEFORE
|
|
159
|
+
// tasks -- in append_task_event (an explicit `threads ... for key share` hoisted above the
|
|
160
|
+
// existing `tasks ... for update`, with a recheck of the pre-read thread_id) and in
|
|
161
|
+
// _upsert_delivery_recipient (the OBSGATE thread stamp hoisted above the
|
|
162
|
+
// delivery_recipients insert, whose FK takes KEY SHARE on tasks). Definition-only.
|
|
163
|
+
'20260913000100_agent_board_task_thread_lock_order.sql',
|
|
164
|
+
// FEDSESSIONIDENTITYGATE (#246): a federated human is returned only to the identity that
|
|
165
|
+
// OWNS it. The owner uid is sha256(issuer || '|' || subject), a concatenation with no
|
|
166
|
+
// length prefix and no escaping, so two distinct principals can derive the same uid --
|
|
167
|
+
// and the return path checked only `active` and `kind = 'human'`, neither of which says
|
|
168
|
+
// the row belongs to the caller. Both return sites now require the agent's stored
|
|
169
|
+
// metadata.federation issuer AND subject to equal the caller's verified pair: the
|
|
170
|
+
// idempotent branch and the unique_violation race handler, which returned an existing
|
|
171
|
+
// row with no comparison either. A missing stamp is a refusal (coalesce(..., false)),
|
|
172
|
+
// not a pass. The ambiguous DERIVATION is deliberately unchanged -- that is a separate
|
|
173
|
+
// migration with an auth.users.email re-key -- because this gate defeats the collision
|
|
174
|
+
// with the encoding still in place.
|
|
175
|
+
'20260915000100_agent_board_fedsession_stored_identity_gate.sql',
|
|
176
|
+
// FEDIDENTITYSLUG (#246): the (issuer, subject) -> owner slug derivation becomes ONE
|
|
177
|
+
// injective serialization, and the ambiguous one is dropped rather than kept beside it.
|
|
178
|
+
// `issuer || '|' || subject` has no length prefix and no escaping, so the pair is not
|
|
179
|
+
// recoverable from the preimage and two principals can derive one slug -- and the slug
|
|
180
|
+
// is not a label, it is the auth.users key _ensure_agent_auth_user builds the email
|
|
181
|
+
// from. All five live derivation sites now call agent_board._federated_identity_slug,
|
|
182
|
+
// which length-prefixes each component. NOT definition-only: the migration also
|
|
183
|
+
// BACKFILLS every existing federated owner across all four surfaces that carry the
|
|
184
|
+
// slug -- auth.users.email, raw_user_meta_data.slug, auth.identities, and the human
|
|
185
|
+
// agent's handle -- because re-deriving without re-keying the email orphans the owner
|
|
186
|
+
// (the #244 resumption lookup misses and the non-null owner conjunct fails). The owner
|
|
187
|
+
// uuid is unchanged, so no FK and no live session moves.
|
|
188
|
+
'20260916000100_agent_board_federated_identity_injective_slug.sql',
|
|
144
189
|
]) {
|
|
145
190
|
expect(names.has(f)).toBe(true);
|
|
146
191
|
}
|
|
@@ -110,12 +110,21 @@
|
|
|
110
110
|
-- demonstrated silenced that way (#198). They are presence claims, so a
|
|
111
111
|
-- comment can only make a MISSING construct look present -- the same
|
|
112
112
|
-- fail-open direction, on weaker claims;
|
|
113
|
-
-- * the
|
|
114
|
-
--
|
|
115
|
-
--
|
|
116
|
-
--
|
|
117
|
-
--
|
|
118
|
-
--
|
|
113
|
+
-- * the ONE remaining deliberate raw read, added by message-board#277: the
|
|
114
|
+
-- KNOWN-STALE record for set_board_authority_ceiling's inline every-reader
|
|
115
|
+
-- claim. Its evidence IS a comment, so a stripped body cannot see it by
|
|
116
|
+
-- construction. It is a PRESENCE claim, so the fail-open direction is "a
|
|
117
|
+
-- comment makes a missing thing look present" -- which is exactly what it is
|
|
118
|
+
-- asserting, deliberately.
|
|
119
|
+
--
|
|
120
|
+
-- CORRECTED BY message-board#277, because this paragraph was itself an example of
|
|
121
|
+
-- the defect #277 fixes -- prose claiming what the code no longer does. It used to
|
|
122
|
+
-- say the catalog CLASS assertions at the end decide serialized/UNSERIALIZED with
|
|
123
|
+
-- `strpos(p.prosrc, …)` over RAW prosrc. They do not, and had not since #196/#199:
|
|
124
|
+
-- the serialization state and the surface-set predicates all read
|
|
125
|
+
-- agent_board_test.stripped_body(), and one surface predicate reads
|
|
126
|
+
-- stripped_body(oid, true) because its marker is a string literal. Verified against
|
|
127
|
+
-- the statements themselves, not against this list.
|
|
119
128
|
-- All of it is inside exact-set or presence assertions that #196 Bucket A
|
|
120
129
|
-- deliberately excluded; it belongs to the follow-up that closes Buckets B and C,
|
|
121
130
|
-- together with the nesting hole in the strip described above.
|
|
@@ -123,7 +132,7 @@ create extension if not exists pgtap with schema extensions;
|
|
|
123
132
|
set search_path = extensions, public, agent_board;
|
|
124
133
|
|
|
125
134
|
begin;
|
|
126
|
-
select plan(
|
|
135
|
+
select plan(58);
|
|
127
136
|
|
|
128
137
|
-- ---------------------------------------------------------------------------
|
|
129
138
|
-- The stripped-body reader every positional assertion below uses is
|
|
@@ -491,33 +500,54 @@ select ok(
|
|
|
491
500
|
'the ceiling setter holds the same lock before its owner gate and upsert [BACKSTOP: tests/isolation/specs/ceiling_setter_lock_before_upsert.spec]'
|
|
492
501
|
);
|
|
493
502
|
|
|
503
|
+
-- THE CEILING CLASSES, DERIVED ONCE (message-board#277). Both sides of this lock
|
|
504
|
+
-- -- who READS the ceiling for authorization, and who WRITES it -- are needed by
|
|
505
|
+
-- four assertions below, one of which compares them to the lock helper's own
|
|
506
|
+
-- COMMENT. Written out four times they would be four things that can drift; the
|
|
507
|
+
-- drift would show up as one of them failing, but only after someone read four
|
|
508
|
+
-- copies to work out which was right. So the predicates live here, once.
|
|
509
|
+
--
|
|
510
|
+
-- CALL SHAPE for the serialization state, not strpos on a bare name: that is the
|
|
511
|
+
-- dangerous direction -- a reader that DROPS the lock but keeps a variable named
|
|
512
|
+
-- after it would still print "serialized".
|
|
513
|
+
-- SCHEMA-QUALIFIED, because a bare-name anchor proves call SYNTAX, not callee
|
|
514
|
+
-- IDENTITY: `\m_lock_board_authority_ceiling\s*\(` also matches
|
|
515
|
+
-- `zz_other._lock_board_authority_ceiling(...)` (measured true). Every live call
|
|
516
|
+
-- site in agent_board is `agent_board.`-qualified, so requiring the qualification
|
|
517
|
+
-- costs nothing today and is fail-CLOSED if someone later writes an unqualified
|
|
518
|
+
-- call (it reads as NOT calling). Overload count is pinned separately, so schema +
|
|
519
|
+
-- name is the identity.
|
|
520
|
+
--
|
|
521
|
+
-- The WRITE predicate names the DML verb and the relation together rather than
|
|
522
|
+
-- "mentions board_authority_ceilings", so _grant_within_ceiling -- which reads the
|
|
523
|
+
-- table and is the reason the read class exists at all -- is not miscounted as a
|
|
524
|
+
-- writer. It sees only STATIC SQL in the function's own body: a write built into an
|
|
525
|
+
-- `execute` string is a literal and is stripped away, and a write performed by a
|
|
526
|
+
-- TRIGGER or by a callee belongs to that object, not to this one. Same blindness
|
|
527
|
+
-- the positional claims above carry and for the same reason; stated here so the
|
|
528
|
+
-- class is not read as "every write to this table", which it is not.
|
|
529
|
+
create temp table _ceiling_class as
|
|
530
|
+
select 'agent_board.' || regexp_replace(p.oid::regprocedure::text, '^([^(]*\.)?', '') as ident,
|
|
531
|
+
p.proname::text as name, -- text, not `name`: array_agg(name) has no is(name[], text[]) overload
|
|
532
|
+
(strpos(lower(agent_board_test.stripped_body(p.oid)), '_grant_within_ceiling') > 0) as is_reader,
|
|
533
|
+
(agent_board_test.stripped_body(p.oid) ~*
|
|
534
|
+
'(insert\s+into|update|delete\s+from|merge\s+into)\s+(only\s+)?(agent_board\.)?board_authority_ceilings\M') as is_writer,
|
|
535
|
+
(agent_board_test.stripped_body(p.oid) ~* '\magent_board\._lock_board_authority_ceiling\s*\(') as serialized
|
|
536
|
+
from pg_proc p
|
|
537
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
538
|
+
where n.nspname = 'agent_board';
|
|
539
|
+
|
|
494
540
|
-- CLASS (catalog-derived, EC-4 form). Every agent_board function that reads the
|
|
495
541
|
-- ceiling for authorization, enumerated from pg_proc — never from a list written
|
|
496
542
|
-- here — each carrying its serialization state POSITIVELY. A new reader changes
|
|
497
543
|
-- the array and NAMES ITSELF in the diff; a reader that is serialized without
|
|
498
|
-
-- being declared here also fails. The
|
|
499
|
-
-- knowingly-unfixed
|
|
500
|
-
-- they carry the same absence race and are out of #186's scope.
|
|
544
|
+
-- being declared here also fails. The two UNSERIALIZED entries are what remains of
|
|
545
|
+
-- the knowingly-unfixed set recorded in 20260901000110's header (it recorded three;
|
|
546
|
+
-- #276 retired one): they carry the same absence race and are out of #186's scope.
|
|
501
547
|
select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below are string literals in the expected array, never invoked
|
|
502
|
-
(select array_agg(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
-- direction -- a reader that DROPS the lock but keeps a variable named
|
|
506
|
-
-- after it would still print "serialized".
|
|
507
|
-
-- SCHEMA-QUALIFIED, because a bare-name anchor proves call SYNTAX, not callee
|
|
508
|
-
-- IDENTITY: `\m_lock_board_authority_ceiling\s*\(` also matches
|
|
509
|
-
-- `zz_other._lock_board_authority_ceiling(...)` (measured true). Every live call
|
|
510
|
-
-- site in agent_board is `agent_board.`-qualified, so requiring the
|
|
511
|
-
-- qualification costs nothing today and is fail-CLOSED if someone later writes
|
|
512
|
-
-- an unqualified call (it reads as NOT calling). Overload count is pinned
|
|
513
|
-
-- separately, so schema + name is the identity.
|
|
514
|
-
case when agent_board_test.stripped_body(p.oid) ~* '\magent_board\._lock_board_authority_ceiling\s*\('
|
|
515
|
-
then 'serialized' else 'UNSERIALIZED' end as entry
|
|
516
|
-
from pg_proc p
|
|
517
|
-
join pg_namespace n on n.oid = p.pronamespace
|
|
518
|
-
where n.nspname = 'agent_board'
|
|
519
|
-
and strpos(lower(agent_board_test.stripped_body(p.oid)), '_grant_within_ceiling') > 0
|
|
520
|
-
) readers),
|
|
548
|
+
(select array_agg(ident || ' => ' || case when serialized then 'serialized' else 'UNSERIALIZED' end
|
|
549
|
+
order by ident || ' => ' || case when serialized then 'serialized' else 'UNSERIALIZED' end)
|
|
550
|
+
from _ceiling_class where is_reader),
|
|
521
551
|
array[
|
|
522
552
|
'agent_board._redeem_unbounded_invite_for_agent(text,uuid) => UNSERIALIZED',
|
|
523
553
|
'agent_board.create_invite_link(uuid,text,text,integer,timestamp with time zone,uuid,jsonb) => UNSERIALIZED',
|
|
@@ -532,6 +562,91 @@ select is( -- pgtap-allow-raise-checked: catalog read only; the RPC names below
|
|
|
532
562
|
'every ceiling-authorization reader in pg_proc is declared with its serialization state'
|
|
533
563
|
);
|
|
534
564
|
|
|
565
|
+
-- CLASS (catalog-derived, message-board#277). The WRITE side of the same lock,
|
|
566
|
+
-- declared the same way. The helper's COMMENT makes a claim about writers as well
|
|
567
|
+
-- as readers, and until now only ONE writer was checked -- by name, in the
|
|
568
|
+
-- lock-before-upsert assertion above. A second writer could appear with no lock at
|
|
569
|
+
-- all and nothing here would notice; the comment would still say every writer takes
|
|
570
|
+
-- it. Now it names itself.
|
|
571
|
+
select is( -- pgtap-allow-raise-checked: catalog read only; the RPC name below is a string literal in the expected array, never invoked
|
|
572
|
+
(select array_agg(ident || ' => ' || case when serialized then 'serialized' else 'UNSERIALIZED' end
|
|
573
|
+
order by ident || ' => ' || case when serialized then 'serialized' else 'UNSERIALIZED' end)
|
|
574
|
+
from _ceiling_class where is_writer),
|
|
575
|
+
array[
|
|
576
|
+
'agent_board.set_board_authority_ceiling(uuid,text,uuid) => serialized'
|
|
577
|
+
]::text[],
|
|
578
|
+
'every function that WRITES board_authority_ceilings is declared with its serialization state'
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
-- The COMMENT on the lock helper is the most discoverable statement of intent about
|
|
582
|
+
-- it -- it is attached to the lock itself -- and until #277 it claimed a property
|
|
583
|
+
-- that the reader class above ACTIVELY REFUTED: "every RPC that writes the ceiling
|
|
584
|
+
-- and every RPC that reads it for authorization takes this FIRST". Two of the four
|
|
585
|
+
-- readers do not take it at all. Correcting the sentence would have fixed today and
|
|
586
|
+
-- left the same rot mechanism in place: prose three files away from the assertion
|
|
587
|
+
-- that knows the answer.
|
|
588
|
+
--
|
|
589
|
+
-- So the COMMENT now carries the two lists in a fixed machine-readable form and this
|
|
590
|
+
-- assertion compares them, name for name and state for state, to the SAME
|
|
591
|
+
-- catalog-derived classes the two assertions above declare. A reader or writer added,
|
|
592
|
+
-- removed, or moved between serialized and UNSERIALIZED fails here as well, and the
|
|
593
|
+
-- failure prints the disagreeing lists side by side.
|
|
594
|
+
--
|
|
595
|
+
-- FAIL-CLOSED ON A MANGLED BLOCK: an absent or malformed block makes substring()
|
|
596
|
+
-- return NULL, which is coalesced to a sentinel that no catalog list can equal --
|
|
597
|
+
-- never to the empty list, which is a real and different answer ('(none)').
|
|
598
|
+
-- Deleting the block is therefore a failure, not a silent pass. Mutation-verified in
|
|
599
|
+
-- all three directions (state flipped, name dropped, block deleted).
|
|
600
|
+
select is(
|
|
601
|
+
(select array[
|
|
602
|
+
coalesce(string_agg(name, ', ' order by name) filter (where is_writer and serialized), '(none)'),
|
|
603
|
+
coalesce(string_agg(name, ', ' order by name) filter (where is_writer and not serialized), '(none)'),
|
|
604
|
+
coalesce(string_agg(name, ', ' order by name) filter (where is_reader and serialized), '(none)'),
|
|
605
|
+
coalesce(string_agg(name, ', ' order by name) filter (where is_reader and not serialized), '(none)')
|
|
606
|
+
] from _ceiling_class),
|
|
607
|
+
(select array[
|
|
608
|
+
coalesce(substring(c from 'WRITERS\[serialized: ([^]]*)\]'), '(NO WRITERS BLOCK IN THE COMMENT)'),
|
|
609
|
+
coalesce(substring(c from 'WRITERS\[serialized: [^]]*\]\[UNSERIALIZED: ([^]]*)\]'), '(NO WRITERS UNSERIALIZED BLOCK IN THE COMMENT)'),
|
|
610
|
+
coalesce(substring(c from 'READERS\[serialized: ([^]]*)\]'), '(NO READERS BLOCK IN THE COMMENT)'),
|
|
611
|
+
coalesce(substring(c from 'READERS\[serialized: [^]]*\]\[UNSERIALIZED: ([^]]*)\]'), '(NO READERS UNSERIALIZED BLOCK IN THE COMMENT)')
|
|
612
|
+
] from (select obj_description('agent_board._lock_board_authority_ceiling(uuid)'::regprocedure, 'pg_proc') as c) d),
|
|
613
|
+
'the ceiling lock COMMENT names exactly the catalog-derived writers and readers, with the same serialization state'
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
-- The COMMENT lists BARE NAMES (onboard_federated_fleet_agent alone has 23
|
|
617
|
+
-- parameters; a signature list would be unreadable and would rot on any signature
|
|
618
|
+
-- change). A bare name is an identity only while no two members of these classes
|
|
619
|
+
-- share one -- otherwise the comparison above would accept the wrong overload's
|
|
620
|
+
-- state. Catalog-derived, so a colliding overload fails here and prints its name.
|
|
621
|
+
select is(
|
|
622
|
+
(select array_agg(name order by name) from (
|
|
623
|
+
select name from _ceiling_class where is_reader or is_writer group by name having count(*) > 1
|
|
624
|
+
) dupes),
|
|
625
|
+
null::text[],
|
|
626
|
+
'no two ceiling readers/writers share a bare name, so the COMMENT may identify them by name'
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
-- KNOWN STALE, RECORDED POSITIVELY (message-board#277). The same overclaim the
|
|
630
|
+
-- helper's COMMENT carried is ALSO an inline comment inside
|
|
631
|
+
-- set_board_authority_ceiling's live body: "Same board-keyed advisory lock every
|
|
632
|
+
-- ceiling READER for authorization takes." It is false in the same way and for the
|
|
633
|
+
-- same two readers. It is NOT corrected in #277 because correcting prose inside a
|
|
634
|
+
-- body means recreating a SECURITY DEFINER function -- a much wider blast radius
|
|
635
|
+
-- than a `comment on function`, and a decision to take deliberately rather than to
|
|
636
|
+
-- smuggle into a comment fix.
|
|
637
|
+
--
|
|
638
|
+
-- Asserted POSITIVELY rather than left as a note in a PR body (CLAUDE.md rule 1: an
|
|
639
|
+
-- exemption states its claim, never relies on absence). WHEN YOU FIX IT: this
|
|
640
|
+
-- assertion fails and names itself -- delete it, and re-derive the COMMENT's blocks
|
|
641
|
+
-- from the catalog while you are there. Raw prosrc on purpose: the evidence IS a
|
|
642
|
+
-- comment, so the stripped body cannot see it (presence claim, not positional).
|
|
643
|
+
select ok(
|
|
644
|
+
(select p.prosrc from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
645
|
+
where n.nspname = 'agent_board' and p.proname = 'set_board_authority_ceiling')
|
|
646
|
+
like '%Same board-keyed advisory lock every ceiling READER for authorization takes%',
|
|
647
|
+
'KNOWN STALE (#277): set_board_authority_ceiling still carries the refuted every-reader claim inline -- pinned here so fixing it cannot go unnoticed'
|
|
648
|
+
);
|
|
649
|
+
|
|
535
650
|
-- CLASS (catalog-derived). Every function that touches this migration set's own
|
|
536
651
|
-- surface — the bounded-invite table, the human revocation registry, the fleet
|
|
537
652
|
-- revocation ledger, or the ceiling lock — with its exact non-owner EXECUTE
|