@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.
- package/fixtures/already-applied.json +12 -0
- package/migrations/20260917000100_agent_board_invite_no_downgrade.sql +398 -0
- package/migrations/20260918000100_agent_board_signed_write_enforced_repoint.sql +139 -0
- package/migrations/20260919000100_agent_board_signed_write_model_key.sql +107 -0
- package/package.json +1 -1
- package/schema-manifest.json +30 -3
- package/src/deployment-drift-script.test.mjs +424 -0
- package/src/deployment-drift.test.mjs +301 -0
- package/src/tooling.test.mjs +38 -2
- package/tests/agent_board_bundle_endpoint_compat.test.sql +73 -18
- package/tests/agent_board_consent_substrate.test.sql +8 -1
- package/tests/agent_board_invite_no_downgrade.test.sql +689 -0
- package/tests/agent_board_signed_write_enforced.test.sql +366 -0
- package/tests/agent_board_source_strip_helper.test.sql +100 -1
- package/tests/agent_board_write_enforcement_class.test.sql +1016 -0
- package/tests/fixtures/isolation-substrate.sql +1 -1
- package/tests/fixtures/test-prereqs.sql +22 -0
|
@@ -0,0 +1,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
|
+
});
|
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 + 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 =
|
|
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(
|
|
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(
|
|
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
|
-
--
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
'
|
|
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'), '
|
|
48
|
-
'
|
|
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
|
-
|
|
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
|
|