# 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(, 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 explicit `umask 022`, the pattern from `gate-lane.mjs:46`: `run("/bin/bash", ["-c", 'umask 022; exec "$@"', "bash", BOOTSTRAP, ...args], homeEnv(home))`. Every `bootstrap()` 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 --instance` run under `umask 077` makes the config and `instance.json` 0600. 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 with `mode: 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 022` and once under `umask 077` (`bash -c 'umask 077; node --test '`). 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 077` fail 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`: imports `chmodSync`; `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 `--sso` start that must refuse. - `cli/tests/stack-instances.test.mjs`: `underUmask(mask, home, args)` runs `scripts/bootstrap.sh` through `/bin/bash -c 'umask ; exec "$@"'`; `bootstrap()` is `underUmask("022", ...)`. New subtest in T-I1: under umask 077 the config dir and root are 0700, and the config and `instance.json` are 0600. - Hash pins: no test or script names any of the four files (a grep for the basenames; positive control `stack-release.test` found in two files). `stack-release.test.mjs:2122` mentions `stack-instances.test.mjs` only 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 ; 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.