process.test, proof-cli.test and sso.test set the too-open mode with chmod instead of taking it from the umask. stack-instances.test bootstraps under an explicit umask and asserts the 0700 config dir under umask 077. Candidate morgana/row2436581f750e(code137bda3af), Webby APPROVE rev 2939.
8.8 KiB
Row 243: too-open mode refusal tests set their mode with chmod
Brief: docs/plans/2026-10-21_gate-env-rows.md § "The startup token refusal test sets its file mode"
(Sage, sage-1909). Reviewer Webby. Lands after row 197 (mos-1162). Design notes written before 197
lands, read at origin/refactor 01e1fe512. Recheck the line cites against 197's landing before code
starts.
The defect
writeFileSync(file, data, { mode }) and mkdirSync(dir, { mode }) apply the process umask to the
mode. A test that creates a "too open" file this way and expects a refusal passes under umask 022
(0644 stays 0644) and fails under umask 077 (0644 becomes 0600, and the source accepts it).
Row 197 r6 (df0c59608) runs each gate step under umask 022 (gate-lane.mjs:46), so the lane no
longer hits this. Row 243 still makes the tests independent of the umask, as the brief asks.
Sweep
Method: every source-side mode check in packages/*/src and scripts/ (grep for mode & 0o...), then
the tests that reach each check's refusal branch, deciding whether the too-open object gets its mode at
creation or from an explicit chmod. Also a scan of every writeFileSync/mkdirSync/openSync in
packages/*/tests with a group or other bit in its mode (50 hits, 41 without a following chmod), and a
grep of the refusal messages for objects created with no mode or by a shell-out. Read at
origin/refactor with git show and git grep; nothing run.
Affected refusal tests, all three verified by reading the test (the fourth file, an accept test, follows the table):
| Creation | Test | Source check | Expected refusal |
|---|---|---|---|
packages/bus/tests/process.test.mjs:76 writeFileSync(file, secret, { mode: 0o644 }) |
startup token refusal returns safe code... | bus/src/credentials.mjs:205 (must be 0600) |
credential-file, exit 2 (:83-84) |
packages/webui/tests/proof-cli.test.mjs:28 writeFileSync(file, value, { mode }), mode 0o644 from the mode case at :27 |
proof startup refuses partial flags... | webui/src/serve.mjs:234 (0600 or 0400) |
status 2 (:38) |
packages/webui/tests/sso.test.mjs:360 writeFileSync(.../secrets/mosaic-stack/a, ..., { mode: 0o644 }) |
11: the CLI refuses --sso... | webui/src/sso.mjs:45 (no group or other bits) |
status 2 and the "no group or other permission bits" line (:361-363); the chmod to 0600 at :364 comes after |
Fix for each: keep the write, then chmodSync(<path>, 0o644) right after it. For proof-cli.test.mjs
the chmod goes inside the case map after the write, with the case's own mode, so the 0o600 cases are set
explicitly too.
Not affected (each sets the mode with a chmod or a chmodding helper before the code under test reads it):
bus credentials.test :52, credential-store.test :55-61; business access.test :225-226, business.test
:159/161/174, credentials.test :60, cli.test :151, helpers writeJson/tokenFile; cli cli.test :176,
notifier.test :318/:321, stack-release.test :1220/1239/1294/1313/1552, stack-upgrade.test :426-430,
case2-unit.test :340/:510/:635; control-board discord.test :79; discord binding.test :106/118/140/277,
notify.test :76, setspark.test :110, git.test :138/:360, offline-check.test :21; mosaic safety.test :80;
queue review.test :214/:228; ledger gitea-helper-raw.test :113; webui service.test :797, sso.test :310;
bus host-tools.test :182/:266. The 0o644/0o755 adapter and fake-binary writes (launcher, thread-launch,
session, sandbox, checkout, commit, etc.) aren't read by a mode check, or are only checked for the exec
bit, which 0700 keeps. host-tools.test.mjs:205, :271 and the confDir at :77 expect acceptance and
only need "not group-writable".
Mode checks with no refusal test at all (not this row; listed for the record): bus/src/store.mjs:121,
bus/src/host-run.mjs:71, business/src/organization.mjs:28/45/109/141,
conversation/src/controller.mjs:299, mosaic/src/materialize-fixture.mjs:16,
mosaic/src/refresh-fixture.mjs:64, seat/src/session.mjs:72, scripts/git-credential-mosaic.mjs:49,
and the mode branch of scripts/stack-release.sh:1952.
Fourth file, folded in by Sage (sage-1918): packages/cli/tests/stack-instances.test.mjs:84 and :89
assert 0644 on the config and instance.json. scripts/mosaic-config.mjs:337 creates them with
openSync(file, "wx", 0o644) and no chmod, so under umask 077 they come out 0600 and the test fails.
This is an accept test of a source mode, not a refusal test.
| Creation | Test | Source | Ruling |
|---|---|---|---|
scripts/mosaic-config.mjs:337 via bootstrap() (stack-instances.test.mjs:41, a spawnSync of scripts/bootstrap.sh) |
the --instance create test, :79-89 |
createExclusive passes 0o644 and the umask narrows it |
test-only (sage-1918) |
- No change to
mosaic-config.mjs. Letting the operator's umask narrow the config is the source's choice; a chmod to 0644 would make the config wider than that umask. No row asks for it. bootstrap()runs its child under an explicitumask 022, the pattern fromgate-lane.mjs:46:run("/bin/bash", ["-c", 'umask 022; exec "$@"', "bash", BOOTSTRAP, ...args], homeEnv(home)). Everybootstrap()call goes through it (:79, :99, :102, :119, :127, :136, :140, :152, :165, :169), so the 0644 assertions still prove the mode the source asks for, whatever the gate's umask.- New assertion: a
bootstrap --instancerun underumask 077makes the config andinstance.json0600. That pins that the source honors the umask, so a later chmod "fix" fails a test. The directories stay 0700 in both runs (they're created withmode: 0o700, which 077 can't narrow). - The
config()helper (:42) has no mode assertion and stays as it is.
Tests outside packages/*/tests: the five scripts/foundation/*.test.mjs files have no open-mode
write. cli.test.mjs:144/148 chmods to 0o000 and 0o600, which doesn't depend on the umask. The
agents/*/work probe tests are review evidence, not gate steps (discoverSteps doesn't run them), so
they're out of scope.
Tests
- Each of the four files runs whole twice, once under
umask 022and once underumask 077(bash -c 'umask 077; node --test <file>'). Both pass. The brief requires this for process.test.mjs; Sage extended it to all four (sage-1918). - Proof that the change matters: before the change, the same four files under
umask 077fail on the named assertions. I record that run too, on the slot, so the 077 pass after the change is meaningful. - Order (mos-1162): this lands after 197 r6, so r6's proof ran on the unchanged test.
Files (expected)
packages/bus/tests/process.test.mjs, packages/webui/tests/proof-cli.test.mjs,
packages/webui/tests/sso.test.mjs, packages/cli/tests/stack-instances.test.mjs. Test-only. No
source change.
Build (sage-2056: start on row 197's landing a36523713)
Branch morgana/row243, one commit on a36523713. Cites rechecked there: all hold except proof-cli's
write, which is now proof-cli.test.mjs:30 (was :28).
bus/tests/process.test.mjs: importschmodSync;chmodSync(file, 0o644)after the write at :76.webui/tests/proof-cli.test.mjs:30:chmodSync(file, mode)after the write, with each case's own mode, so the 0o600 cases are set explicitly too.webui/tests/sso.test.mjs:360:chmodSync(..., 0o644)after the write, before the--ssostart that must refuse.cli/tests/stack-instances.test.mjs:underUmask(mask, home, args)runsscripts/bootstrap.shthrough/bin/bash -c 'umask <mask>; exec "$@"';bootstrap()isunderUmask("022", ...). New subtest in T-I1: under umask 077 the config dir and root are 0700, and the config andinstance.jsonare 0600.- Hash pins: no test or script names any of the four files (a grep for the basenames; positive control
stack-release.testfound in two files).stack-release.test.mjs:2122mentionsstack-instances.test.mjsonly in a comment.
Tests: on Sage's slot (sage-2062, plan approved in sage-2059), 18:55Z. My busy check read CLEAR before
and after. Each file ran whole with node --test --test-concurrency=2 from its package, under
bash -c 'umask <mask>; exec ...', with TMPDIR from mktemp -d /mnt/storage/scratch/tmp/g243XXXX.
Script /mnt/storage/scratch/morgana-r243/slot243.sh, logs in logs/ next to it.
| Tree | umask | process | proof-cli | sso | stack-instances |
|---|---|---|---|---|---|
base a36523713 |
077 | 7/1 | 2/1 | 27/1 | 21/2 |
candidate 137bda3af |
022 | 8/0 | 3/0 | 28/0 | 24/0 |
candidate 137bda3af |
077 | 8/0 | 3/0 | 28/0 | 24/0 |
(pass/fail.) The base fails are the predicted ones: "startup token refusal returns safe code...", "proof startup refuses partial flags...", "11: the CLI refuses --sso...", and in stack-instances the 0644 subtest "alpha and beta get a 0700 config dir..." plus its parent T-I1 (so 2). stack-instances has one more test on the candidate (the umask 077 subtest). No unpredicted failure.