@consiliency/agent-board-schema 1.35.0 → 1.38.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.
@@ -0,0 +1,301 @@
1
+ // Tests for the deployment-drift DECISION.
2
+ //
3
+ // These call `evaluate()` from scripts/lib/drift-evaluate.mjs — the function the script
4
+ // ships and uses. Nothing here reimplements the rule.
5
+ //
6
+ // That matters because the first version of this file did exactly that: it extracted two
7
+ // leaf helpers and wrote the staleness rule locally, while the PR body claimed the tests
8
+ // drove the shipped code. A three-seat review found the claim false and the real control
9
+ // flow untested — including a path where a board with an unparseable timestamp was
10
+ // reported `ok`.
11
+ //
12
+ // WHAT THE `OK` CASES ARE. An earlier version of this header claimed every "reports ok"
13
+ // case below was a fail-open regression test. That was false, and it mattered: it dressed
14
+ // up ordinary coverage as security coverage. There are two different kinds here, and they
15
+ // are labelled:
16
+ // - FAIL-OPEN regression: the case returned `ok` in a previous version and must NOT.
17
+ // Those all assert UNKNOWN or DRIFT — see "UNKNOWN never collapses into OK" and
18
+ // "second-round fail-open regressions".
19
+ // - HAPPY-PATH / BOUNDARY / SKEW: the case must return OK, and the test exists so the
20
+ // check cannot be made trivially strict. A gate that fires on the normal state gets
21
+ // ignored, which is its own failure mode. These are the OK assertions at "the happy
22
+ // path is genuinely reachable", "does NOT flag a small, fresh gap", "tolerates a
23
+ // minute of clock skew", "pins the exact threshold boundary", and the behind-board
24
+ // case in "branches a mutation must not survive".
25
+ //
26
+ // The script itself — argv parsing, credential scrubbing, and the exit-code mapping this
27
+ // whole PR exists for — is NOT exercised here. It is exercised as a subprocess in
28
+ // deployment-drift-script.test.mjs, because none of it is reachable through `evaluate()`.
29
+ import { describe, it, expect } from "vitest";
30
+ import { evaluate, minorGap, pgTimestampToIso, OK, DRIFT, UNKNOWN } from "../../../scripts/lib/drift-evaluate.mjs";
31
+
32
+ const NOW = new Date("2026-08-16T18:00:00Z").getTime();
33
+ const BUMP = "2026-08-16T17:00:00Z";
34
+ const base = (over = {}) => ({
35
+ merged: "1.35.0",
36
+ published: "1.35.0",
37
+ bumpIso: BUMP,
38
+ expectedSchemaVersion: 8,
39
+ expectedDigest: "f63027fa",
40
+ boards: [],
41
+ ...over,
42
+ });
43
+ const run = (facts, opts = {}) => evaluate(facts, { nowMs: NOW, ...opts });
44
+ const board = (over = {}) => ({
45
+ ref: "b1",
46
+ running: "1.35.0",
47
+ schemaVersion: 8,
48
+ digest: "f63027fa",
49
+ installedAt: "2026-08-16 17:30:00+00",
50
+ ...over,
51
+ });
52
+
53
+ describe("the happy path is genuinely reachable", () => {
54
+ it("is OK when everything agrees", () => {
55
+ const r = run(base({ boards: [board()] }));
56
+ expect(r.status).toBe(OK);
57
+ });
58
+ });
59
+
60
+ // Each of these returned `ok` + exit 0 in the first version. That was the defect.
61
+ describe("UNKNOWN never collapses into OK", () => {
62
+ it("npm unreachable is UNKNOWN, not current", () => {
63
+ expect(run(base({ published: null })).status).toBe(UNKNOWN);
64
+ });
65
+
66
+ it("an unreadable board is UNKNOWN, not current", () => {
67
+ expect(run(base({ boards: [{ ref: "b1", error: "HTTP 401" }] })).status).toBe(UNKNOWN);
68
+ });
69
+
70
+ it("a missing credential is UNKNOWN, not current", () => {
71
+ expect(run(base({ boards: [{ ref: "b1", error: "no SUPABASE_ACCESS_TOKEN" }] })).status).toBe(UNKNOWN);
72
+ });
73
+
74
+ it("an unparseable install timestamp is UNKNOWN — the specific bug a seat found", () => {
75
+ const r = run(base({ boards: [board({ installedAt: "not-a-date" })] }));
76
+ expect(r.status).toBe(UNKNOWN);
77
+ expect(r.findings.some((f) => /unparseable/.test(f.msg))).toBe(true);
78
+ });
79
+
80
+ it("a missing install timestamp is UNKNOWN", () => {
81
+ expect(run(base({ boards: [board({ installedAt: null })] })).status).toBe(UNKNOWN);
82
+ });
83
+
84
+ it("no bump date (shallow clone) is UNKNOWN for the board leg", () => {
85
+ expect(run(base({ bumpIso: null, boards: [board()] })).status).toBe(UNKNOWN);
86
+ });
87
+
88
+ it("a major-version difference is UNKNOWN, not OK", () => {
89
+ expect(run(base({ published: "2.0.0" })).status).toBe(UNKNOWN);
90
+ });
91
+
92
+ it("a malformed merged version is UNKNOWN and stops there", () => {
93
+ const r = run(base({ merged: "not-a-version" }));
94
+ expect(r.status).toBe(UNKNOWN);
95
+ });
96
+
97
+ it("a board record with no digest is UNKNOWN — cannot confirm same bytes", () => {
98
+ expect(run(base({ boards: [board({ digest: null })] })).status).toBe(UNKNOWN);
99
+ });
100
+ });
101
+
102
+ describe("real drift is DRIFT", () => {
103
+ it("flags more unpublished versions than the threshold", () => {
104
+ expect(run(base({ merged: "1.42.0" })).status).toBe(DRIFT);
105
+ });
106
+
107
+ it("does NOT flag a small, fresh gap — a check that cries wolf gets ignored", () => {
108
+ expect(run(base({ merged: "1.36.0" })).status).toBe(OK);
109
+ });
110
+
111
+ it("flags a small gap that has gone stale by age", () => {
112
+ const old = new Date("2026-08-01T17:00:00Z").toISOString();
113
+ expect(run(base({ merged: "1.36.0", bumpIso: old })).status).toBe(DRIFT);
114
+ });
115
+
116
+ it("flags a registry AHEAD of the checkout", () => {
117
+ expect(run(base({ merged: "1.28.0", published: "1.35.0" })).status).toBe(DRIFT);
118
+ });
119
+
120
+ it("flags a board with NO install record — absent is not up to date", () => {
121
+ expect(run(base({ boards: [{ ref: "b1", running: null }] })).status).toBe(DRIFT);
122
+ });
123
+
124
+ it("flags a board behind main", () => {
125
+ expect(run(base({ boards: [board({ running: "1.28.0" })] })).status).toBe(DRIFT);
126
+ });
127
+
128
+ it("flags a record installed before the version it claims existed", () => {
129
+ const r = run(base({ boards: [board({ installedAt: "2026-07-15 02:57:23+00" })] }));
130
+ expect(r.status).toBe(DRIFT);
131
+ expect(r.findings.some((f) => /stale/.test(f.msg))).toBe(true);
132
+ });
133
+
134
+ it("flags a record whose schema_version disagrees with the manifest", () => {
135
+ expect(run(base({ boards: [board({ schemaVersion: 7 })] })).status).toBe(DRIFT);
136
+ });
137
+
138
+ it("flags a record whose digest disagrees with the manifest — different bytes", () => {
139
+ expect(run(base({ boards: [board({ digest: "deadbeef" })] })).status).toBe(DRIFT);
140
+ });
141
+
142
+ it("tolerates a minute of clock skew rather than crying stale", () => {
143
+ expect(run(base({ boards: [board({ installedAt: "2026-08-16 16:59:30+00" })] })).status).toBe(OK);
144
+ });
145
+ });
146
+
147
+ describe("pgTimestampToIso", () => {
148
+ it("parses the Postgres two-digit offset that plain Date() rejects", () => {
149
+ expect(pgTimestampToIso("2026-07-15 02:57:23+00")).toBe("2026-07-15T02:57:23+00:00");
150
+ expect(Number.isNaN(new Date("2026-07-15T02:57:23+00").getTime())).toBe(true);
151
+ });
152
+ it("keeps fractional seconds", () => {
153
+ expect(pgTimestampToIso("2026-08-16 17:17:00.04088+00")).toBe("2026-08-16T17:17:00.04088+00:00");
154
+ });
155
+ it("assumes UTC for a naive timestamp", () => {
156
+ expect(pgTimestampToIso("2026-08-16 17:17:00")).toBe("2026-08-16T17:17:00Z");
157
+ });
158
+ it("returns null for unparseable input — unknown, never fresh", () => {
159
+ for (const v of ["not-a-date", null, undefined, ""]) expect(pgTimestampToIso(v)).toBeNull();
160
+ });
161
+ });
162
+
163
+ describe("minorGap", () => {
164
+ it("counts unpublished versions", () => expect(minorGap("1.35.0", "1.28.0")).toBe(7));
165
+ it("is zero when equal", () => expect(minorGap("1.35.0", "1.35.0")).toBe(0));
166
+ it("goes negative when the registry is ahead", () => expect(minorGap("1.28.0", "1.35.0")).toBe(-7));
167
+ it("refuses across a major", () => expect(minorGap("2.0.0", "1.35.0")).toBeNull());
168
+ it("returns null rather than guessing", () => expect(minorGap("nope", "1.35.0")).toBeNull());
169
+ });
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Second-round findings. Every case below reported OK in the first rewrite.
173
+ // ---------------------------------------------------------------------------
174
+ describe("second-round fail-open regressions", () => {
175
+ it("an unpublished gap with an undeterminable bump date is UNKNOWN, not OK", () => {
176
+ expect(run(base({ merged: "1.36.0", bumpIso: null })).status).toBe(UNKNOWN);
177
+ });
178
+
179
+ it("a board behind main with an undeterminable bump date is UNKNOWN, not OK", () => {
180
+ expect(run(base({ bumpIso: null, boards: [board({ running: "1.34.0" })] })).status).toBe(UNKNOWN);
181
+ });
182
+
183
+ it("missing manifest expectations make the consistency leg UNKNOWN, not a silent skip", () => {
184
+ // Three seats reproduced this: a corrupt manifest disabled the digest and
185
+ // schema_version comparisons while still printing "record consistent".
186
+ const r = run(base({ expectedDigest: null, expectedSchemaVersion: null, boards: [board({ digest: "deadbeef" })] }));
187
+ expect(r.status).toBe(UNKNOWN);
188
+ expect(r.findings.some((f) => /record consistent/.test(f.msg))).toBe(false);
189
+ });
190
+
191
+ it("a board record with no schema_version is UNKNOWN", () => {
192
+ expect(run(base({ boards: [board({ schemaVersion: null })] })).status).toBe(UNKNOWN);
193
+ });
194
+
195
+ it("a malformed version does not parse as its numeric prefix", () => {
196
+ expect(minorGap("1.35.0garbage", "1.35.0")).toBeNull();
197
+ expect(run(base({ published: "1.35.0garbage" })).status).toBe(UNKNOWN);
198
+ });
199
+
200
+ it("a published PATCH ahead is drift, not agreement", () => {
201
+ // minor-only comparison called 1.35.1 equal to 1.35.0 while the registry was ahead
202
+ expect(run(base({ published: "1.35.1" })).status).toBe(DRIFT);
203
+ });
204
+
205
+ it("non-finite thresholds refuse to evaluate rather than passing everything", () => {
206
+ expect(run(base({ merged: "1.58.0" }), { maxVersions: NaN }).status).toBe(UNKNOWN);
207
+ expect(run(base({ merged: "1.58.0" }), { maxDays: NaN }).status).toBe(UNKNOWN);
208
+ });
209
+ });
210
+
211
+ describe("status accumulation — previously unpinned", () => {
212
+ it("DRIFT outranks UNKNOWN when both are present", () => {
213
+ // A RANK swap survived the first suite because no case mixed the two.
214
+ const r = run(base({ published: null, boards: [board({ running: "1.20.0" })] }));
215
+ expect(r.findings.some((f) => f.level === UNKNOWN)).toBe(true);
216
+ expect(r.findings.some((f) => f.level === DRIFT)).toBe(true);
217
+ expect(r.status).toBe(DRIFT);
218
+ });
219
+
220
+ it("pins the exact threshold boundary", () => {
221
+ expect(run(base({ merged: "1.38.0" })).status).toBe(OK); // gap 3 == maxVersions
222
+ expect(run(base({ merged: "1.39.0" })).status).toBe(DRIFT); // gap 4 > maxVersions
223
+ });
224
+ });
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Branches a mutation must not survive.
228
+ //
229
+ // Each of the four below was measured UNPINNED: the mutation named in its comment was
230
+ // applied to drift-evaluate.mjs and all 38 tests stayed green. A green suite is not
231
+ // evidence; these exist so that each mutation reds a named assertion.
232
+ // ---------------------------------------------------------------------------
233
+ describe("branches a mutation must not survive", () => {
234
+ // Mutation: delete the `add(UNKNOWN, ...)` for an unparseable bumpIso (:100-102).
235
+ // No board, and merged === published, so this finding is the ONLY non-OK source —
236
+ // without it the status is OK.
237
+ it("an unparseable bump timestamp is UNKNOWN even when every other leg agrees", () => {
238
+ const r = run(base({ bumpIso: "not-a-date", boards: [] }));
239
+ expect(r.status).toBe(UNKNOWN);
240
+ expect(r.findings.some((f) => f.level === UNKNOWN && /version-bump timestamp is unparseable/.test(f.msg))).toBe(true);
241
+ });
242
+
243
+ // Mutation: change the board-leg `add(UNKNOWN, ...)` for an incomparable version to OK.
244
+ it("a board across a MAJOR from merged is UNKNOWN, never OK", () => {
245
+ const r = run(base({ boards: [board({ running: "2.0.0" })] }));
246
+ expect(r.status).toBe(UNKNOWN);
247
+ expect(r.findings.some((f) => f.level === UNKNOWN && /running 2\.0\.0 is not comparable/.test(f.msg))).toBe(true);
248
+ });
249
+
250
+ // Mutation: drop the early `return` after the malformed-merged UNKNOWN (:96). The
251
+ // existing "a malformed merged version is UNKNOWN and stops there" asserts only the
252
+ // status, which the mutation preserves — so it asserted the label, not the stopping.
253
+ it("a malformed merged version STOPS evaluation — exactly one finding, nothing else runs", () => {
254
+ const r = run(base({ merged: "1.x.0", published: "1.35.0", boards: [board(), board({ ref: "b2", digest: "deadbeef" })] }));
255
+ expect(r.status).toBe(UNKNOWN);
256
+ expect(r.findings).toHaveLength(1);
257
+ expect(r.findings[0].msg).toMatch(/cannot compare anything/);
258
+ });
259
+
260
+ // Mutation: force the behind-board outcome to DRIFT unconditionally (:158 as numbered
261
+ // before this block was added). One version behind, bumped an hour ago, is inside
262
+ // release cadence and must not fire.
263
+ //
264
+ // Double duty: the ancient `installedAt` here pins the STATED LIMIT that a behind
265
+ // board's staleness check does not run (see the LIMIT comment in drift-evaluate.mjs).
266
+ // If that limit is ever closed, this assertion reds and must be rewritten deliberately
267
+ // rather than the limit being closed by accident.
268
+ // Mutation: delete the `gap < 0` branch, restoring the fall-through. Reproduced by a
269
+ // seat: a board running 1.36.0 against merged 1.35.0 with a MATCHING digest reported
270
+ // status ok, exit 0, and a message asserting the record was consistent. The publish leg
271
+ // has always called this same relation DRIFT; the board leg did not.
272
+ it("a board AHEAD of merged is DRIFT even when its digest and schema_version match", () => {
273
+ const r = run(base({ boards: [board({ running: "1.36.0" })] })); // digest/schemaVersion match
274
+ expect(r.status).toBe(DRIFT);
275
+ expect(r.findings.some((f) => f.level === DRIFT && /is running 1\.36\.0, which is AHEAD of merged/.test(f.msg))).toBe(true);
276
+ // The old fall-through produced this message while returning ok. Its presence would
277
+ // mean the ahead board was evaluated as though it were current.
278
+ expect(r.findings.some((f) => /record consistent/.test(f.msg))).toBe(false);
279
+ });
280
+
281
+ it("a board AHEAD with a MISMATCHED digest is DRIFT for being ahead, not for the bytes", () => {
282
+ // The symmetric case. A board ahead SHOULD disagree with a manifest describing merged,
283
+ // so the honest single finding is the ahead-ness; "different bytes" would be noise
284
+ // dressed as a second defect.
285
+ const r = run(base({ boards: [board({ running: "1.36.0", digest: "deadbeef", schemaVersion: 9 })] }));
286
+ expect(r.status).toBe(DRIFT);
287
+ expect(r.findings.some((f) => f.level === DRIFT && /AHEAD of merged/.test(f.msg))).toBe(true);
288
+ expect(r.findings.some((f) => /different bytes/.test(f.msg))).toBe(false);
289
+ });
290
+
291
+ it("a board a PATCH ahead is DRIFT too — the half-step sign is classified", () => {
292
+ // minorGap returns ±0.5 for a patch difference; -0.5 took the same fall-through.
293
+ expect(run(base({ boards: [board({ running: "1.35.1" })] })).status).toBe(DRIFT);
294
+ });
295
+
296
+ it("a board 1 behind and fresh is OK even with an ancient install timestamp", () => {
297
+ const r = run(base({ boards: [board({ running: "1.34.0", installedAt: "2020-01-01 00:00:00+00" })] }));
298
+ expect(r.status).toBe(OK);
299
+ expect(r.findings.some((f) => f.level === OK && /is 1 schema version\(s\) behind main/.test(f.msg))).toBe(true);
300
+ });
301
+ });
@@ -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 + 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)', () => {
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 + one INVITENODOWNGRADE + one SIGNEDWRITEFLAG + one SIGNEDWRITEMODEL forward migrations (migrationCount = prior 113 + 5 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 137)', () => {
42
42
  expect(manifest.migrationCount).toBe(manifest.migrations.length);
43
- expect(manifest.migrationCount).toBe(134);
43
+ expect(manifest.migrationCount).toBe(137);
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,
@@ -186,6 +186,42 @@ describe('manifest integrity', () => {
186
186
  // (the #244 resumption lookup misses and the non-null owner conjunct fails). The owner
187
187
  // uuid is unchanged, so no FK and no live session moves.
188
188
  '20260916000100_agent_board_federated_identity_injective_slug.sql',
189
+ // #317 INVITENODOWNGRADE. `_upsert_board_membership_state` is the sole sink that writes
190
+ // membership_state, and it wrote p_membership_state unconditionally -- so an invite (or a
191
+ // join request) landing on an agent who is already `joined`/`subscribed` REPLACED active
192
+ // standing with the weaker offer state. Reproduced on production. Distinct from #109, which
193
+ // was a wrong-ROW defect fixed by p_membership_id targeting; this is the right row moved in
194
+ // the wrong DIRECTION. `invited`/`requested` are offers, so applying one to an existing
195
+ // membership is now a no-op on membership_state (not an exception -- invite is idempotent by
196
+ // design and raising would break callers that succeed today). Every other transition,
197
+ // including re-invite after `removed`, is unchanged. The sink also now emits the RESULTING
198
+ // ROW's state rather than the requested argument: before the guard the two could not
199
+ // differ, and after it a no-op offer would otherwise append `invite_offer -> invited` to
200
+ // the append-only ledger beside a row that says `joined` -- the same discipline
201
+ // _redeem_unbounded_invite_for_agent already follows.
202
+ '20260917000100_agent_board_invite_no_downgrade.sql',
203
+ // #310 SIGNEDWRITEFLAG. `get_bundle_compatibility` reported
204
+ // `signed_write_enforced: false` on a system that IS enforcing, because the predicate
205
+ // still encoded the model 20260718000800 deliberately abandoned: it asked whether
206
+ // `authenticated` can EXECUTE four raw write RPCs. There are SEVEN signed gateways, not
207
+ // four, and the send family is OVERLOADED, so the old hand list was wrong twice over.
208
+ // The flag now reads pg_proc rows for `signed\_%` and asks `has_function_privilege`
209
+ // (which resolves inherited and PUBLIC-default grants that `role_routine_grants`
210
+ // structurally cannot see -- the #302 fail-open shape and the #118 recreate class).
211
+ // Definition-only: CREATE OR REPLACE with the identical signature, so the ACL survives;
212
+ // grants and a COMMENT are restated in the same migration anyway.
213
+ '20260918000100_agent_board_signed_write_enforced_repoint.sql',
214
+ // #326 SIGNEDWRITEMODEL. `signed_write_enforced` has the right VALUE and the wrong
215
+ // NAME: it is called "signed write enforced" and it tests "the signed path is
216
+ // operator-only". A boolean cannot say which proposition it means, so a reader had to
217
+ // combine it with `installed_bundle_version` and a 1.37.0 boundary to decode it. Adds a
218
+ // self-describing `signed_write_model` sibling ('operator-only-gateway'). ADDITIVE: the
219
+ // client's runtime type guard requires three keys to be PRESENT, so dropping or
220
+ // re-pointing the boolean was not available -- the first breaks every deployed client's
221
+ // recognition of a HEALTHY response, the second is the permanently-lying instrument #310
222
+ // names. Definition-only: CREATE OR REPLACE with the identical signature, grants and
223
+ // COMMENT restated in the same migration.
224
+ '20260919000100_agent_board_signed_write_model_key.sql',
189
225
  ]) {
190
226
  expect(names.has(f)).toBe(true);
191
227
  }
@@ -1,5 +1,5 @@
1
1
  begin;
2
- select plan(12);
2
+ select plan(21);
3
3
 
4
4
  -- objects exist
5
5
  select has_table('agent_board', 'bundle_installation', 'bundle_installation table exists');
@@ -25,27 +25,47 @@ select is((agent_board.get_bundle_compatibility() ->> 'ready'), 'false',
25
25
  select is((agent_board.get_bundle_compatibility() ->> 'schema_version'), '8',
26
26
  'compatibility reports schema version 8');
27
27
 
28
- -- Post-MBSESSION the 8 core sends are re-granted to authenticated, so the LIVE
29
- -- baseline posture is signed_write_enforced=false. Exercise BOTH branches in this
30
- -- rolled-back txn. Enforced branch: revoke EXECUTE from authenticated on ALL
31
- -- eight gated-write overloads (the probe checks by routine name, so any single
32
- -- remaining overload grant keeps enforcement false) -> reports active=true.
33
- revoke execute on function agent_board.send_direct_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
34
- revoke execute on function agent_board.send_direct_message(uuid, uuid, jsonb, text, text, text, text, text, text, jsonb) from authenticated;
28
+ -- #310: this block used to exercise the flag's two branches by revoking and re-granting the
29
+ -- EIGHT send overloads, because the predicate asked whether `authenticated` could execute them.
30
+ -- That predicate encoded the model 20260718000800 deliberately abandoned, and it reported
31
+ -- `false` for a system that WAS enforcing. 20260918000100 re-points it at the signed gateway's
32
+ -- grants, so the same two branches are now driven from a `signed_*` function instead.
33
+ --
34
+ -- Both branches still run inside this rolled-back txn, and driving BOTH is the point: an
35
+ -- assertion that only ever sees `true` cannot tell "the flag is right" from "the flag is stuck".
36
+ select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'true',
37
+ 'signed-write enforcement reports active on the shipped posture — the signed gateway exists and no signed_* is executable by authenticated or anon (#310)');
38
+
39
+ -- Enforcement-off branch: EXECUTE on any ONE signed_* gateway reaching `authenticated` is
40
+ -- enough. The probe iterates pg_proc rows, so this holds for every gateway and every overload,
41
+ -- not just the one named here.
42
+ grant execute on function agent_board.signed_post_board_message(jsonb, jsonb) to authenticated;
43
+ select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'false',
44
+ 'enforcement flips to false when a single signed_* gateway becomes executable by authenticated');
45
+
46
+ revoke execute on function agent_board.signed_post_board_message(jsonb, jsonb) from authenticated;
47
+
48
+ -- `anon` is checked too, and separately: the retired predicate looked only at `authenticated`,
49
+ -- so an anon grant on the signed path would have left it reporting enforced.
50
+ grant execute on function agent_board.signed_send_direct_message(jsonb, jsonb) to anon;
51
+ select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'false',
52
+ 'enforcement flips to false for an ANON grant as well — the retired predicate only looked at authenticated');
53
+
54
+ revoke execute on function agent_board.signed_send_direct_message(jsonb, jsonb) from anon;
55
+ select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'true',
56
+ 'and back to true once the grant is withdrawn — the flag tracks the grant rather than latching');
57
+
58
+ -- The re-granted SEND path must NOT move the flag -- #310's defect stated as an assertion.
59
+ -- This REVOKES and RE-GRANTS a send overload so it observes a genuine state change; the earlier
60
+ -- draft asserted the same value twice with nothing happening in between, which documented the
61
+ -- claim rather than probing it.
35
62
  revoke execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) from authenticated;
36
- revoke execute on function agent_board.send_multicast_message(uuid, uuid, jsonb, text, text, text, text, text, text, jsonb) from authenticated;
37
- revoke execute on function agent_board.post_board_message(uuid, uuid, uuid, text, text, text, text, text, jsonb) from authenticated;
38
- revoke execute on function agent_board.post_board_message(uuid, uuid, text, text, text, text, text, text, text, jsonb) from authenticated;
39
- revoke execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, uuid[], uuid, text, text, text, jsonb) from authenticated;
40
- revoke execute on function agent_board.create_tasking_delivery(uuid, uuid, text, text, jsonb, text, text, text, text, text, jsonb) from authenticated;
41
63
  select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'true',
42
- 'signed-write enforcement reports active when all gated write RPCs are revoked from authenticated');
64
+ '#310: REVOKING a send RPC from authenticated does not move the flag — the retired predicate would have flipped to true here, which is precisely what it got wrong');
43
65
 
44
- -- Re-grant branch: re-granting EXECUTE on any one gated write RPC (as MBSESSION
45
- -- does at 20260718000800) flips enforcement back to false.
46
66
  grant execute on function agent_board.send_multicast_message(uuid, uuid, uuid[], text, text, text, text, text, jsonb) to authenticated;
47
- select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'false',
48
- 'enforcement flips to false when any gated write RPC is re-granted to authenticated');
67
+ select is((agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'), 'true',
68
+ '#310: and RE-GRANTING it does not move the flag either — authenticated holding EXECUTE on the unsigned send path is the shipped model, not a lapse');
49
69
 
50
70
  -- after the materializer records an install: ready + bundle identity present,
51
71
  -- and no credential/row content leaks (only whitelisted keys)
@@ -57,5 +77,40 @@ select is(
57
77
  'true:1.1.0:deadbeef',
58
78
  'ready + bundle identity reported after install record');
59
79
 
80
+ -- ===========================================================================
81
+ -- SIGNEDWRITEMODEL (message-board#326)
82
+ --
83
+ -- `signed_write_enforced` has the right VALUE and the wrong NAME: it is called
84
+ -- "signed write enforced" and it tests "the signed path is operator-only". The
85
+ -- sibling below NAMES the proposition, so a reader decodes the boolean without
86
+ -- consulting the 1.37.0 version boundary.
87
+ -- ===========================================================================
88
+ select is((agent_board.get_bundle_compatibility() ->> 'signed_write_model'),
89
+ 'operator-only-gateway',
90
+ 'MB326-1: the handshake NAMES the model its boolean is answering, so the flag decodes without a version boundary');
91
+
92
+ -- The key is a CONSTANT about this bundle, not a derivation from the database.
93
+ -- Driving the boolean to its other branch must not move it: a model that flipped
94
+ -- with the state it describes would be a second boolean, not a label.
95
+ grant execute on function agent_board.signed_post_board_message(jsonb, jsonb) to authenticated;
96
+ select is(
97
+ (agent_board.get_bundle_compatibility() ->> 'signed_write_model') || ':' ||
98
+ (agent_board.get_bundle_compatibility() ->> 'signed_write_enforced'),
99
+ 'operator-only-gateway:false',
100
+ 'MB326-2: the model NAME is a constant about the bundle — it stays put while the boolean it labels flips to false');
101
+ revoke execute on function agent_board.signed_post_board_message(jsonb, jsonb) from authenticated;
102
+
103
+ -- ADDITIVE, which is the whole argument for adding rather than renaming.
104
+ -- packages/agent-board-client/src/compatibility.ts recognises a health response
105
+ -- with `"schema_version" in value && "signed_write_enforced" in value && "ready"
106
+ -- in value`. Asserted as PRESENCE of each of the three, one at a time, because
107
+ -- that is what the guard tests -- a combined check would pass on two of three.
108
+ select ok(agent_board.get_bundle_compatibility() ? 'schema_version',
109
+ 'MB326-3: the deployed client type guard''s first key is still PRESENT');
110
+ select ok(agent_board.get_bundle_compatibility() ? 'signed_write_enforced',
111
+ 'MB326-4: ...its second key, the one a rename or retirement would have removed, is still PRESENT');
112
+ select ok(agent_board.get_bundle_compatibility() ? 'ready',
113
+ 'MB326-5: ...and its third');
114
+
60
115
  select * from finish();
61
116
  rollback;
@@ -709,7 +709,14 @@ select is(
709
709
  from pg_proc p join pg_namespace n on n.oid = p.pronamespace
710
710
  where n.nspname = 'agent_board' and p.prokind = 'f'
711
711
  and p.proname in ('join_board','resolve_board_request','update_board_membership','_upsert_board_membership_state')
712
- and pg_get_functiondef(p.oid) ~ '_membership_consent_present'),
712
+ -- stripped_body, NOT pg_get_functiondef. This read the RAW definition, so a COMMENT
713
+ -- naming the predicate counted as a READER of it: #317 added an in-body comment to
714
+ -- _upsert_board_membership_state explaining why that function must NOT emit a state the
715
+ -- predicate would read as consent, and this assertion promptly reported the function as a
716
+ -- third reader. The direction was safe -- an exact-set match can only gain a name, so it
717
+ -- failed LOUDLY rather than passing -- but "mentions it" and "calls it" are different
718
+ -- claims and this one is about calls. Same helper, same reason, as #196/#199.
719
+ and agent_board_test.stripped_body(p.oid) ~ '_membership_consent_present'),
713
720
  'resolve_board_request, update_board_membership',
714
721
  'scope: exactly TWO membership routes read the predicate');
715
722