diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 4e91360da..6ef80a957 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -5415,3 +5415,5 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r 2026-10-23 | Gladstone | Gitea #1816 / Vikunja 614 hostcheck item 5 and bundle verification | Before: dpkg's correct 750 form falsely fails item 5, and two run-sheet bundle verifies require a missing repository. Draft after: exact root/mosaic/path with octal 0750 equivalence; stat/output unchanged; both verifies use cleaned-up bare scratch repos. Three additive-only test blocks, two planned mutants. Untested under Sage-3738; Sage-3740 authorizes untested branch push for static review. Queue row pending; Sage owns gate and landing. 2026-10-23 | Gladstone | #1816 / Vikunja 614 scope amendment mos-1534, sage-3741 | F1 narrowed to exact 750/0750 and single spaces; F3 allows @ in absolute unit paths with unsafe-character refusals retained; F4 moves copy_tree to / after path resolution. AS_USER audit found git -C and stack-release's existing checkout cd cover the other steps. Added new unit and unreadable-cwd copy tests only. Untested under Sage's no-Node hold; no live action. + +2026-10-23 | Gladstone | #1816 / Vikunja 614 F5 draft (mos-1536) | Before: the run sheet omits the explicit notify decision and precheck misses a bus-start refusal. After: section 9a creates a private null-binding decision as mosaic; precheck directly reuses readNotifyConfig and private_dir, and new tests cover accepted and refused decisions. Existing fixture setup additions await Mos approval through Sage; installer unchanged. Untested under no-Node hold. diff --git a/agents/gladstone/work/hostcheck-item5/BUILD.md b/agents/gladstone/work/hostcheck-item5/BUILD.md index b9d232305..a656170d3 100644 --- a/agents/gladstone/work/hostcheck-item5/BUILD.md +++ b/agents/gladstone/work/hostcheck-item5/BUILD.md @@ -112,3 +112,68 @@ subshell. This supersedes the initial outer-shell failure behavior noted above. | `scripts/case1-hostcheck.sh` | 2 | 2 | | `scripts/case2-unit.sh` | 1 | 1 | | `scripts/stack-copy.sh` | 3 | 0 | + +## F5 (mos-1536 / ops-0260): explicit notifier decision + +Before: no run-sheet step creates the required notifier decision, and precheck +accepts a host whose bus will refuse missing notify.json. After: section 9a +requires the mosaic user to create its business notify directory at 0700 and an +explicit {"notifyVersion": 1, "binding": null} file at 0600 before section 10. +The shell runs with cwd / and umask 077; noclobber refuses an existing decision. +Named binding is explicitly a later per-host choice requiring Mos's go. +case2-install.sh is unchanged and makes no silent default. + +case2-precheck.sh's existing facts helper dynamically imports readNotifyConfig +from cli.mjs and calls it directly. File type, uid, exact 0600 mode, JSON keys, +version and binding-name validation therefore stay identical to the bus. The +shell checks the business notify directory with existing private_dir (same uid, +not a symlink, exactly 0700) and prints the new notifier config line. No notifier +is started and no configuration is written. Tests append pass cases for null and +a valid named binding, and refusal cases for missing file, 0644, 0755 directory, +extra key, malformed JSON, invalid binding/version and a symlinked file. + +M5 (unrun): remove the notifier shell check, including its refusal branch; the +new notifier refusal test must fail. Also include Morgana's F3 negative mutant +M3b (allow $ in plain), killed by the new unsafe-character test. + +Existing positive precheck tests need an explicit notify fixture. Approval +requested via Sage in gladstone-0006/-0007 for minimal setup-only additions to +case2-unit.test.mjs host(t) and case2-install.test.mjs world(t); assertions remain +unchanged. Those additions are not yet applied. Correction to gladstone-0007: +case1-accept.test.mjs also names case2-precheck.sh, but uses a stand-in; it does +not execute the changed script and needs no fixture amendment. + +Execution remains held by Sage: no Node/tests run. Morgana-0207 static F1-F4 +verdict on a75532db1 is CLEAN; execution and F5 review are still open. + +F5 acceptance from mos-1536 (for review): +1. Before section 10, as mosaic, explicitly create /notify/ + at 0700 and notify.json at 0600 with default binding null; a later named + binding needs Mos's go. Do not make case2-install.sh write it. +2. Precheck must refuse missing file, non-0600 file, directory looser than 0700 + and a document outside the CLI's exact shape. Reuse/match cli.mjs validation, + and name the new line in this run-sheet step's Precheck entry. +3. Append-only test blocks cover pass, missing, 0644, 0755 dir, extra key; record + a skip-check mutant, the touched test file and its numstat. +4. F3/F4 precede F5, each follow-up commit stays on gladstone/hostcheck-item5; + send pushes to Sage/Morgana with Mos cc. No Node until Sage CLEAR. + +Static F5 draft receipt: git diff --check passed. Existing fixture approvals +remain pending; F5's new pass test creates its own explicit decisions. No claim +that the old positive precheck tests pass without the pending setup changes. + +## F5 candidate file numstat against 93694c518 (added / deleted) + +| File | Added | Deleted | +|---|---:|---:| +| `BUILD-LOG.md` | 6 | 0 | +| `agents/gladstone/work/hostcheck-item5/BUILD.md` | 179 | 0 | +| `docs/SESSIONS.md` | 1 | 0 | +| `docs/guides/host-run-sheet.md` | 34 | 2 | +| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 | +| `packages/cli/tests/case2-unit.test.mjs` | 65 | 0 | +| `packages/cli/tests/stack-copy.test.mjs` | 16 | 0 | +| `scripts/case1-hostcheck.sh` | 2 | 2 | +| `scripts/case2-precheck.sh` | 19 | 0 | +| `scripts/case2-unit.sh` | 1 | 1 | +| `scripts/stack-copy.sh` | 3 | 0 | diff --git a/docs/guides/host-run-sheet.md b/docs/guides/host-run-sheet.md index 980bf3954..dbefb06b7 100644 --- a/docs/guides/host-run-sheet.md +++ b/docs/guides/host-run-sheet.md @@ -295,6 +295,28 @@ The installer key, `/secrets/hosts///installer`, is Enrollment (section 8) writes it under the enrolling stack's config directory and deletes it when the host leaves. +## 9a. Notifier decision, before starting units + +Make the explicit per-host decision before section 10: the default below is +`binding: null`, so the bus runs without DMs. A named binding is a later per-host +choice that needs Mos's go. The installer does not make this decision or write +this file. Replace `` with the configured data root and `` +with this host's business id. If the file already exists, stop and review the +existing decision; this command refuses to overwrite it. + +```sh +sudo -u mosaic -H /bin/sh <<'MOSAIC_NOTIFY' +cd / || exit 1 +umask 077 +notify_dir='/notify/' +mkdir -p -m 0700 -- "$notify_dir" && chmod 0700 -- "$notify_dir" || exit 1 +(set -C; printf '%s\n' '{"notifyVersion": 1, "binding": null}' > "$notify_dir/notify.json") || exit 1 +chmod 0600 -- "$notify_dir/notify.json" +MOSAIC_NOTIFY +``` + +Precheck line: `ok notifier config /notify//notify.json validates (directory 0700, file 0600)`. + ## 10. Console start None by hand. I2's `case2-unit.sh` enables both units, starts the bus, then the Console (Q189, mos-1122). diff --git a/packages/cli/tests/case2-unit.test.mjs b/packages/cli/tests/case2-unit.test.mjs index f0e5ee006..31ef096e8 100644 --- a/packages/cli/tests/case2-unit.test.mjs +++ b/packages/cli/tests/case2-unit.test.mjs @@ -901,3 +901,39 @@ test("Vikunja 614 factory: plain paths still refuse every named unsafe character refuses(unit(h, { profile: "factory", extra: ["--claude", `/opt/claude${char}bin`] }), /--claude contains a character the unit cannot carry/); } }); + +test("Vikunja 614 precheck: explicit null and named notifier decisions pass", (t) => { + for (const binding of [null, "host-notify"]) { + const x = host(t); + const dir = join(x.dataRoot, "notify", "acme"); + mkdirSync(dir, { recursive: true, mode: 0o700 }); + writeFileSync(join(dir, "notify.json"), JSON.stringify({ notifyVersion: 1, binding }), { mode: 0o600 }); + const r = precheck(x); + assert.equal(r.code, 0, r.out); + assert.ok(r.out.includes(`ok notifier config ${dir}/notify.json validates (directory 0700, file 0600)`), r.out); + } +}); + +test("Vikunja 614 precheck: notifier config refuses missing, public, malformed and extra-key decisions", (t) => { + for (const fault of ["missing", "mode", "directory", "extra", "json", "binding", "version", "symlink"]) { + const x = host(t); + const dir = join(x.dataRoot, "notify", "acme"), file = join(dir, "notify.json"); + mkdirSync(dir, { recursive: true, mode: 0o700 }); + writeFileSync(file, '{"notifyVersion":1,"binding":null}\n', { mode: 0o600 }); + if (fault === "missing") rmSync(file); + if (fault === "mode") chmodSync(file, 0o644); + if (fault === "directory") chmodSync(dir, 0o755); + if (fault === "extra") writeFileSync(file, '{"notifyVersion":1,"binding":null,"extra":true}\n'); + if (fault === "json") writeFileSync(file, '{bad json\n'); + if (fault === "binding") writeFileSync(file, '{"notifyVersion":1,"binding":"bad binding"}\n'); + if (fault === "version") writeFileSync(file, '{"notifyVersion":2,"binding":null}\n'); + if (fault === "symlink") { + const target = join(dir, "target.json"); + writeFileSync(target, readFileSync(file), { mode: 0o600 }); + rmSync(file); symlinkSync(target, file); + } + const r = precheck(x); + assert.equal(r.code, 1, `${fault}: ${r.out}`); + assert.match(r.out, /REFUSED notifier config:/, `${fault}: ${r.out}`); + } +}); diff --git a/scripts/case2-precheck.sh b/scripts/case2-precheck.sh index 2e5254a81..14036995e 100755 --- a/scripts/case2-precheck.sh +++ b/scripts/case2-precheck.sh @@ -9,6 +9,7 @@ # # Checks: the config and business file validate; `sudo -n true` fails and # `id -G` prints one group; bus.tester is off for library and on for factory, +# the explicit notifier config is valid at 0600 in a private 0700 directory; # and factory refuses a business file with a library project # (profile-library-business); the run root is 0700; XDG_DATA_HOME is unset or # /.local/share; node is 24 or later; bwrap runs; the unprivileged port @@ -172,6 +173,11 @@ const say = (...f) => console.log(f.join("\t")); let system; try { system = loadSystem(); } catch (e) { say("CONFIG", "bad", e.message.split("\n")[0]); process.exit(0); } say("CONFIG", "ok", system.dataRoot, configDir()); +try { + const { readNotifyConfig } = await import(`${repo}/packages/cli/src/cli.mjs`); + readNotifyConfig(system.dataRoot, id); + say("NOTIFY", "ok"); +} catch (e) { say("NOTIFY", "bad", e.message.split("\n")[0]); } const file = businessFilePath(id); let raw; try { raw = JSON.parse(readFileSync(file, "utf8")); } catch (e) { say("BUSINESS", "bad", `cannot read ${file}: ${e.code ?? e.message}`); process.exit(0); } @@ -204,6 +210,19 @@ case "$cfg" in *) refuse "config: the check did not run: $(printf '%s' "$facts" | head -n 1)" ;; esac +# Use the CLI's exact file/owner/mode/JSON rule; its journal also needs a private directory. +if [ -n "$DATA_ROOT" ]; then + notify_dir="$DATA_ROOT/notify/$BUSINESS" + notify="$(line NOTIFY)" + case "$notify" in + "NOTIFY ok") + if private_dir "$notify_dir"; then ok "notifier config $notify_dir/notify.json validates (directory 0700, file 0600)" + else refuse "notifier config: $notify_dir must be $ME's 0700 directory"; fi ;; + "NOTIFY bad "*) refuse "notifier config: ${notify#NOTIFY bad }" ;; + *) refuse "notifier config: the check did not run" ;; + esac +fi + # Tester and profile. library="$(line LIBRARY | cut -f2)" biz="$(line BUSINESS)"