17 KiB
Row 292 (#1797): config_checks runs the target loader on any config
Builder Gizmoduck (sage-3146, mos-1387), reviewer Duckworth, gate owner Sage. Brief
docs/plans/2026-10-22_config-checks-any-key-brief.md. Gander built round 1 on 01983cd3f
(gander/config-checks-any-key, packet agents/gander/work/config-checks-any-key/, Duckworth static
approve duckworth-0229 with C1-C2 and N1-N4). Gander doesn't build 292; questions about round 1 go
to Gander. Nothing in this packet has run. No test, mutant or repo node ran, with one exception
recorded under "Disclosure".
Rebase (sage-3146 item 1, sage-3148)
Branch gizmoduck/row292, base b23783f39 (the row 270+271 landing 7328da8ca plus its docs commit;
row 203 landed earlier). git rebase --onto b23783f39 01983cd3f d4c796e44.
| commit | rebased | patch-id (stable) before | after |
|---|---|---|---|
0585b3213 fix(release) |
3b17c7550 |
a922252a85b1317e09a625193172167b7ea39edb | 15f1a38e3c1e6c72c38b77a460b6cd4ce60c6dd4 |
e7371025f docs (plan, packet) |
a54baff21 |
8e9b78ac185a49548e94e76c83f7fea1a982038a | 8e9b78ac185a49548e94e76c83f7fea1a982038a |
d4c796e44 docs (approval) |
9c6e39b25 |
fc50b630d432ff7134e3d6c711a054d15ff1f997 | fc50b630d432ff7134e3d6c711a054d15ff1f997 |
e7371025f and d4c796e44 keep their patch-ids. 0585b3213 changes, by the conflict below.
The conflict and its resolution
Only scripts/stack-release.sh conflicted, in config_checks (now :1076-1107). Row 203
(284d55cf2) made the old probe exit 0 on a console or a federation key and the refusal read
"holds a console or federation key". Gander's commit replaced the probe and split the refusal into a
console text and a text for any other key. The test file and docs/TOOLS.md merged without conflict.
Resolution, per sage-3146 item 1 and sage-3148:
- The probe picks
consolefirst, thenfederation, then the first key outside the oldest setconfigVersion, environment, dataRoot, execution. Without the federation step, a config that holds a later key beforefederationwould get Gander's text although the loader refuses federation. - With no target loader, a console or federation key gets 203's reason "it has no
scripts/mosaic-config.mjs", as at
b23783f39. Any other later key gets Gander's "to validate configuration key: ". - The refusal for a console or federation key is 203's line, byte for byte as
284d55cf2andb23783f39have it (compared by script, not by eye). Gander's text, "is refused by $TO's loader ($first); remove the refused key first (restore the config.json backup from the window's step 1), ...", applies only when the key is neither console nor federation. In both,$firstis the loader's first stderr line, which names the refused key (unsupported configuration key: "<key>"). - The comment names 203's protection. docs/TOOLS.md gains one sentence: a console or federation key keeps row 203's message.
Gander's console-only text "holds a console key" (round 1 at 01983cd3f, before 203) is gone: 203's
wording covers console as well. Duckworth's N1 (console plus another refused key keeps the console
text) holds, now for federation too.
Row 203's tests: every line of stack-release.test.mjs up to :2600 at b23783f39 is unchanged. The
rebased file only appends. KEY_MSG (:1471), the item 3 console cases (:1575-1580), the row 203
federation test (:1585-1595) and :1607 are byte-identical to the base. No 203 test or assertion
changed, so nothing goes to Mos under mos-1272 for it.
Range-diff
git range-diff 01983cd3f..d4c796e44 b23783f39..9c6e39b25:
1: 0585b3213 ! 1: 3b17c7550 fix(release): validate non-console config keys with target loader
@@ docs/TOOLS.md: onto the pointer's copy from the target, past any same-copy re-pi
+key. Every existing parseable config is checked; a missing file passes. A target
+without a loader accepts only the oldest top-level keys: `configVersion`,
+`environment`, `dataRoot`, and `execution`; any later key refuses. The refusal
-+names the rejected key and says to restore the config.json backup first. So a
++names the rejected key and says to restore the config.json backup first. A
++`console` or `federation` key keeps row 203's message, which says the config
++holds a console or federation key. So a
window re-pins to row 192 or later before it writes the key, and a rollback past
row 192 removes the key first.
@@ scripts/stack-release.sh: slot_problem() {
-# target's own scripts/mosaic-config.mjs validate; its output, the resolved config, is not kept.
+# Row 200 item 3 and #1797: every existing parseable config must be one the
+# target loader reads. Older copies without a loader accept only the four keys
-+# in c3d29e796's first loader. Resolved config stdout is never kept.
++# in c3d29e796's first loader. A console or federation key (row 203) keeps its own refusal text.
++# Resolved config stdout is never kept.
config_checks() {
- local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first
-- # 0: a console key; 1: no key, or no file; 2: the file doesn't parse, so whether it holds the key
+- # 0: a console or federation key (row 203: a pre-203 loader refuses federation too); 1: neither, or no file; 2: the file doesn't parse, so whether it holds the key
- # is unknown, and the run refuses rather than pass it (webby-0028 N2). load_config already exits on
- # such a file before anything is read or stopped, so this is reached only if the file changes after it.
+ local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first extra
+ # 1: missing file; 2: unreadable JSON. This also fences a change after load_config.
++ # extra: "console" or "federation" first (row 203: a pre-203 loader refuses federation too),
++ # then the first key outside the oldest set.
local key=0
- node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
--process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' -- "$cfg" 2>/dev/null || key=$?
+-process.exit(d!==null&&typeof d==="object"&&(Object.hasOwn(d,"console")||Object.hasOwn(d,"federation"))?0:1)' -- "$cfg" 2>/dev/null || key=$?
+ extra="$(node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
+const oldest=["configVersion","environment","dataRoot","execution"];
+if(d===null||typeof d!=="object"||Array.isArray(d)){process.stdout.write(JSON.stringify("[configuration shape]"));process.exit(0)}
-+const k=Object.hasOwn(d,"console")?"console":Object.keys(d).find(k=>!oldest.includes(k));if(k!==undefined)process.stdout.write(JSON.stringify(k))' -- "$cfg" 2>/dev/null)" || key=$?
++const k=Object.hasOwn(d,"console")?"console":Object.hasOwn(d,"federation")?"federation":Object.keys(d).find(k=>!oldest.includes(k));if(k!==undefined)process.stdout.write(JSON.stringify(k))' -- "$cfg" 2>/dev/null)" || key=$?
[ "$key" != 1 ] || return 0
if [ "$key" != 0 ]; then
check_refuse config-console-key "$cfg cannot be read as JSON, so whether $TO's loader reads it is unknown; fix the config first, then run the $OP again"
@@ scripts/stack-release.sh: slot_problem() {
- if [ ! -f "$loader" ]; then out="it has no scripts/mosaic-config.mjs"
+ if [ ! -f "$loader" ]; then
+ [ -n "$extra" ] || return 0
-+ if [ "$extra" = '"console"' ]; then out="it has no scripts/mosaic-config.mjs"
++ if [ "$extra" = '"console"' ] || [ "$extra" = '"federation"' ]; then out="it has no scripts/mosaic-config.mjs"
+ else out="it has no scripts/mosaic-config.mjs to validate configuration key: $extra"; fi
elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0
fi
first="${out%%$'\n'*}"
-- check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
-+ if [ "$extra" = '"console"' ]; then
-+ check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
+- check_refuse config-console-key "$cfg holds a console or federation key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
++ if [ "$extra" = '"console"' ] || [ "$extra" = '"federation"' ]; then
++ check_refuse config-console-key "$cfg holds a console or federation key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
+ else
+ check_refuse config-console-key "$cfg is refused by $TO's loader ($first); remove the refused key first (restore the config.json backup from the window's step 1), then run the $OP again"
+ fi
2: e7371025f = 2: a54baff21 docs(release): record any-key config plan and static review packet
3: d4c796e44 = 3: 9c6e39b25 docs(release): record static approval and post-compaction review conditions
C1 and N3 (sage-3146 item 3, duckworth-0229 C1)
Commit 4fa7d6834, test file only.
- LOADER_OLD (stack-release-fixture.mjs:301) is today's loader with a shorter first list, so it
still reads
compactionReadBytesandcompactionStreamsthrough the.concat. It is no pre-270 witness. The new stand-inPRE270is C1's copy of today's loader with the.concat(["compactionReadBytes", "compactionStreams"])taken out. That leaves["configVersion", "environment", "dataRoot", "execution", "console", "federation"], the list the loader had at bce491715^ (01b57f48b), beforebce491715added the first key. The stand-in asserts the replace changed something, so a reworded loader fails loudly rather than passing a no-op stand-in. The fixture module isn't edited (not an owned file). - New test "config-any-key: a pre-270 target refuses each row 270 key on a full activate and
rollback, naming the key". It seeds on B, activates C1 and installs PRE270 as B's loader. For
compactionReadBytes1048576 andcompactionStreams1024 in turn, it first runs C1's own loadervalidateon the config (exit 0, so the refusal is the target's). Then it asserts the exact refusal for a fullactivate Band forrollback, usingrefused(): exit 3, the exact stderr, no file changed, no start or stop and one refused log line. The text is ANY_CONFIG_MSG withmosaic-config: unsupported configuration key: "<key>". Afterwards it restores the loader and config, and the rollback to B passes. - N3: the missing-loader test sets
compactionStreamsto 1024, not['messages']. - The missing-loader test also checks 203's no-loader text word for word for
consoleand forfederation, each placed aftercompactionStreamsin the file. This covers resolution items 1 and 2, which no 203 test reaches: 203's tests all use a target with a loader.
Files (manifest)
Paths changed from b23783f39 to the candidate, with sha256 at the candidate:
| path | b23783f39 sha256 |
candidate sha256 |
|---|---|---|
| scripts/stack-release.sh | d3f52f72f2ea345118d30dc34f045f2b5b6abce41697e62f6de07d7a2f25748c | 8f535d53896fefe44525dea39169ac8df29f3d7845f83552a34ec1cd707595bb |
| packages/cli/tests/stack-release.test.mjs | 45faa5d236d4cfb5cf3c026d64d039a5f29ce8868efeed4e9428cf85a5f0067f | 5a228aee7c3811f906513809f8a78a1fe4f3a26057089ae6c87918f432a5ba65 |
| docs/TOOLS.md | 423babffc7be40a2d578ef00e97cdc49d887762e416fc20726d328d96b2a9533 | 611aeb79d603dd1af1917dcbb3db81b51ac0ae30c5b4991ea72e18bc45ee59de |
The rest of the diff is packets: Gander's agents/gander/work/config-checks-any-key/ (eight files) and this one (BUILD.md, mutants.py). The manifest with every path and sha256 goes to Duckworth with the round key.
Mutants
agents/gizmoduck/work/queue-292/mutants.py. Each anchor appears exactly once, and each mutated
script passes bash -n (checked with python3 and bash only, no node). None has run. M1-M5 are
Gander's edits (changes.json) on the resolved function. M6-M9 test the resolution. M1 restores the
console-only skip, the brief's required mutant.
Kill rules (sage-3152)
Each mutant runs only its killing test: node --test --test-name-pattern '<pattern>' tests/stack-release.test.mjs. The patterns are the test names, anchored with ^ and $, and all
four are top-level tests. A mutant counts as killed only when all three of these hold:
- The run ends with
ℹ fail 1, and the failing test is the named one. The spec reporter lists it twice, inline and in the failing-tests summary. - The first error's stack names every listed stack-release.test.mjs line.
- The log holds the listed text. The texts carry no quote characters, which node's inspect may escape.
Anything else is a STOP that goes to Sage before the next mutant: a fail in another test or at another line, a missing text, zero tests matched, a timeout or a crash before the assertion. A fixture or module-load error is the second-fence trap from 264a M2. A mutant whose targeted run passes gets one run of stack-release.test.mjs whole before it counts as a survivor.
| id | edit | test (pattern) | stack lines | text | why that assertion |
|---|---|---|---|---|---|
| M1 | console-only skip restored | non-console target refusal | :517 (refused), :2619 | actual: 0 |
no console key, so the check returns 0 and activate B runs: exit 0, not 3 |
| M2 | failed validate ignored (|| true) |
non-console target refusal | :517, :2619 | actual: 0 |
the stand-in's exit 2 is lost, so activate B runs |
| M3 | no-loader target passes any later key | missing-loader copies | :2670 | actual: 0 |
compactionReadBytes, the first case, passes with status 0 |
| M4 | malformed JSON treated as absent | missing config passes but malformed JSON | :2684 | actual: 0 |
key 2 falls through to the witness loader, which exits 0 |
| M5 | oldest-key config skips the loader | accepting loaders run validate | :2654 | actual: [] |
the witness file is never written |
| M6 | probe drops the federation preference | missing-loader copies | :2677 | to validate configuration key: |
federation after compactionStreams: the probe picks compactionStreams |
| M7 | probe drops the console preference | missing-loader copies | :2677 | to validate configuration key: |
the same for console, the first case of that loop |
| M8 | final refusal treats only console as 203's | missing-loader copies | :2677 | is refused by old |
federation gets Gander's text, not KEY_MSG |
| M9 | no-loader branch treats only console as 203's | missing-loader copies | :2677 | to validate configuration key: |
federation's reason gains the later-key wording |
M1 would also fail the pre-270 test and the row 203 federation test, and M8 the row 203 federation test. The targeted runs don't select those, so they aren't counted.
Run plan (on Sage's CLEAR only, after Duckworth approves this round)
- stack-release.test.mjs whole, limit 2100 s.
- The cli suite whole,
node --test tests/*.test.mjsin packages/cli, limit 2100 s. - The ten
scripts/test-*.sh, limit 900 s each. - Only on a green base: M1-M9 under the kill rules above. Targeted runs get 600 s and a
whole-file rerun of a survivor gets 2100 s. After each mutant:
git checkout -- scripts/stack-release.sh, sha256 equal to the candidate's (8f535d53...) and a clean tree.
Every stage gets its own short TMPDIR, mktemp -d /mnt/storage/scratch/tmp/gXXX, directly under
/mnt/storage/scratch/tmp (sage-3152, 574 run 1's socket-path fault). Every mutant run gets one too.
All runs use pipefail, tee and timeout -k 10. The busy check passes before each start. A first red
in stages 1-3 stops the run.
The limits come from the run 5 receipts (gizmo-271-out/r4, 01:49Z 10-23). stack-release.test.mjs whole took 682 s (duration_ms 681648), the cli suite 624 s and the longest shell suite 44 s. Each limit is at least 3x. Expected time: about 26 minutes for the base, plus about a minute per targeted mutant (fixture setup and one test, not measured yet), plus 12 minutes per whole-file rerun.
C2 (duckworth-0229, sage-3146 item 4): every B, C1 and C release now validates its copied real loader. Any red in an old cli test is a suspect from this change. It stops the run and goes to Sage and Mos under mos-1272, unplaced.
Disclosure
During the hold (sage-3146 item 6, no repo node) I ran node --check packages/cli/tests/stack-release.test.mjs once, between the rebase finish (02:43:51Z, reflog) and
the first C1 commit (02:45:41Z). It is a parse-only syntax check that lasted a few milliseconds. It printed nothing. No other node, test, queue write or https push/fetch has run.