// PR #1916 redaction (org-redact5): a CLI refusal names the file by its label, // never by host path. Every fixture here sits under a directory named for the // sentinel, so a refusal that quoted the path would carry it. import { test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createServer } from "node:net"; import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { gitSocketPath } from "../../seat/src/session.mjs"; import { withLock } from "../src/access.mjs"; import { fixtureFile, readFixture } from "../src/dev-fixture.mjs"; import { createGitSocket } from "../src/git-socket.mjs"; import { consoleStart, consoleStop } from "../src/org-console.mjs"; import { claimHostLock, hostLock } from "../src/host.mjs"; import { journalPath, openJournal } from "../src/notifier.mjs"; import { readNotifyConfig } from "../src/notify-config.mjs"; import { orgCommand } from "../src/org.mjs"; import { instancesCommand } from "../src/instances.mjs"; import { FIXTURE_ENTRY } from "../../plugins/model-provider/fixture/index.mjs"; const SENTINEL = "sk-test-NOT-A-SECRET-1234"; const ROOT_SKIP = process.getuid() === 0 && "root ignores the modes"; function under(t) { const top = mkdtempSync(join(tmpdir(), "redact-")); t.after(() => rmSync(top, { recursive: true, force: true })); const root = join(top, SENTINEL); mkdirSync(root, { mode: 0o700 }); return root; } // The refusal is exactly `message` (a string) or matches it (a RegExp), with exit // code `exitCode`, and carries neither the sentinel nor any of `paths`. function refusal(message, { exitCode = 3, paths = [], code } = {}) { return (error) => { assert.equal(error.name, "CliError"); assert.equal(error.exitCode, exitCode); if (code !== undefined) assert.equal(error.code, code); if (message instanceof RegExp) assert.match(error.message, message); else assert.equal(error.message, message); assert.ok(!error.message.includes(SENTINEL), error.message); for (const path of paths) assert.ok(!error.message.includes(path), error.message); return true; }; } // --- access.json lock --- test("access lock: an unreadable lock is named by label, never by host path", (t) => { const file = join(under(t), "access.json"); writeFileSync(`${file}.lock`, "not-a-pid\n", { mode: 0o600 }); assert.throws(() => withLock(file, () => assert.fail("must refuse")), refusal("refused: access file lock is unreadable or not a pid; if no write is running, remove it by hand and try again", { paths: [file] })); }); test("access lock: a lock left by a dead pid is named by label, never by host path", (t) => { const file = join(under(t), "access.json"); const dead = spawnSync(process.execPath, ["-e", ""]); writeFileSync(`${file}.lock`, `${dead.pid}\n`, { mode: 0o600 }); assert.throws(() => withLock(file, () => assert.fail("must refuse")), refusal(`refused: access file lock is left by pid ${dead.pid}, which is not running; if no write is running, remove it by hand and try again`, { paths: [file] })); }); // --- bus host lock --- const MINE = { pid: process.pid, startTime: "1", business: "acme", startedAt: "2026-11-10T00:00:00.000Z" }; const STALE = { pid: 999999999, startTime: "1", business: "acme", startedAt: "2026-11-09T00:00:00.000Z" }; function hostRoot(t, lock) { const dataRoot = join(under(t), "data"); mkdirSync(join(dirname(hostLock(dataRoot))), { recursive: true, mode: 0o700 }); if (lock !== undefined) writeFileSync(hostLock(dataRoot), lock, { mode: 0o600 }); return dataRoot; } test("host lock: an unreadable host.lock refuses by label, never by host path", (t) => { const dataRoot = hostRoot(t, "{not json"); assert.throws(() => claimHostLock(dataRoot, MINE), refusal("bus host state is unreadable", { paths: [dataRoot] })); }); test("host lock: a malformed host.lock refuses by label, never by host path", (t) => { const dataRoot = hostRoot(t, JSON.stringify({ pid: "x" })); assert.throws(() => claimHostLock(dataRoot, MINE), refusal("bus host state is malformed", { paths: [dataRoot] })); }); test("host lock: a stale host.lock refused on request is named by label, never by host path", (t) => { const dataRoot = hostRoot(t, JSON.stringify(STALE)); assert.throws(() => claimHostLock(dataRoot, MINE, { refuseStale: true }), refusal("stale bus host lock", { paths: [dataRoot] })); }); test("host lock: a lock that keeps changing is named by label, never by host path", (t) => { const dataRoot = hostRoot(t, JSON.stringify(STALE)); let n = 0; const race = () => writeFileSync(hostLock(dataRoot), JSON.stringify({ ...STALE, startedAt: `2026-11-09T00:00:${String(++n % 60).padStart(2, "0")}.000Z` }), { mode: 0o600 }); assert.throws(() => claimHostLock(dataRoot, MINE, { between: race }), refusal("bus host lock kept changing; no bus host started", { paths: [dataRoot] })); }); // --- notify journal --- test("notify journal: a directory link, dangling or not, is refused by label", (t) => { const root = under(t); const dangling = journalPath(root, "demo"); mkdirSync(dirname(dirname(dangling)), { recursive: true, mode: 0o700 }); symlinkSync(join(root, "missing"), dirname(dangling)); assert.throws(() => openJournal(dangling), refusal("notify journal directory must not be a symlink", { paths: [dangling] })); const real = join(root, "real"); mkdirSync(real, { mode: 0o700 }); const linked = journalPath(root, "acme"); symlinkSync(real, dirname(linked)); assert.throws(() => openJournal(linked), refusal("notify journal directory must not be a symlink", { paths: [linked] })); }); test("notify journal: a parent that is a file is refused by its error code", (t) => { const root = under(t); writeFileSync(join(root, "plain"), ""); const file = join(root, "plain", "notify", "demo", "sent.jsonl"); assert.throws(() => openJournal(file), refusal("notify journal directory cannot be created (ENOTDIR)", { paths: [file] })); }); test("notify journal: a directory of the wrong mode is refused by label", (t) => { const file = journalPath(under(t), "demo"); mkdirSync(dirname(file), { recursive: true, mode: 0o755 }); chmodSync(dirname(file), 0o755); assert.throws(() => openJournal(file), refusal("notify journal directory must be mode 0700 and owned by this user", { paths: [file] })); }); test("notify journal: a directory it cannot write or create is refused by its error code", { skip: ROOT_SKIP }, (t) => { const root = under(t); const file = journalPath(root, "demo"); mkdirSync(dirname(file), { recursive: true, mode: 0o700 }); chmodSync(dirname(file), 0o500); try { assert.throws(() => openJournal(file), refusal("notify journal directory is not writable (EACCES)", { paths: [file] })); } finally { chmodSync(dirname(file), 0o700); } const parent = dirname(dirname(file)); const other = journalPath(root, "acme"); chmodSync(parent, 0o500); try { assert.throws(() => openJournal(other), refusal("notify journal directory cannot be created (EACCES)", { paths: [other] })); } finally { chmodSync(parent, 0o700); } }); test("notify journal: a journal link, an unwritable journal and a directory journal are refused by label", { skip: ROOT_SKIP }, (t) => { const root = under(t); const file = journalPath(root, "demo"); mkdirSync(dirname(file), { recursive: true, mode: 0o700 }); writeFileSync(join(root, "elsewhere.jsonl"), "", { mode: 0o600 }); symlinkSync(join(root, "elsewhere.jsonl"), file); assert.throws(() => openJournal(file), refusal("notify journal must not be a symlink", { paths: [file] })); rmSync(file); writeFileSync(file, "", { mode: 0o400 }); chmodSync(file, 0o400); assert.throws(() => openJournal(file), refusal("notify journal is not writable (EACCES)", { paths: [file] })); const asDir = journalPath(root, "acme"); mkdirSync(asDir, { recursive: true, mode: 0o700 }); assert.throws(() => openJournal(asDir), refusal("notify journal must be a regular file, mode 0600, owned by this user", { paths: [asDir] })); }); test("notify journal: a journal of the wrong mode and a malformed line are refused by label", (t) => { const file = journalPath(under(t), "demo"); mkdirSync(dirname(file), { recursive: true, mode: 0o700 }); writeFileSync(file, "", { mode: 0o644 }); chmodSync(file, 0o644); assert.throws(() => openJournal(file), refusal("notify journal must be a regular file, mode 0600, owned by this user", { paths: [file] })); chmodSync(file, 0o600); writeFileSync(file, "not json\n"); assert.throws(() => openJournal(file), refusal("notify journal line 1 is malformed (json)", { paths: [file] })); }); // --- notifier config --- function notifyRoot(t, text, mode = 0o600) { const dataRoot = join(under(t), "data"); const dir = join(dataRoot, "notify", "acme"); mkdirSync(dir, { recursive: true, mode: 0o700 }); if (text !== undefined) { writeFileSync(join(dir, "notify.json"), text, { mode }); chmodSync(join(dir, "notify.json"), mode); } return dataRoot; } test("notifier config: a missing file names the business and its place under the data root", (t) => { const dataRoot = notifyRoot(t); assert.throws(() => readNotifyConfig(dataRoot, "acme"), refusal('no notifier config for acme: write {"notifyVersion": 1, "binding": ""} to notify/acme/notify.json under the data root, mode 0600, or "binding": null to run without DMs', { paths: [dataRoot] })); }); test("notifier config: a file of the wrong mode names the business only", (t) => { const dataRoot = notifyRoot(t, JSON.stringify({ notifyVersion: 1, binding: null }), 0o644); assert.throws(() => readNotifyConfig(dataRoot, "acme"), refusal("notifier config for acme must be a regular file, mode 0600, owned by this user", { paths: [dataRoot] })); }); test("notifier config: a file that is not JSON names the business only", (t) => { const dataRoot = notifyRoot(t, "{not json"); assert.throws(() => readNotifyConfig(dataRoot, "acme"), refusal("notifier config for acme is not JSON", { paths: [dataRoot] })); }); test("notifier config: a file of the wrong shape names the business only", (t) => { const dataRoot = notifyRoot(t, JSON.stringify({ notifyVersion: 1, binding: null, extra: 1 })); assert.throws(() => readNotifyConfig(dataRoot, "acme"), refusal('notifier config for acme must be exactly {"notifyVersion": 1, "binding": | null}', { paths: [dataRoot] })); }); // --- dev fixture --- const RECORD = { fixtureVersion: 1, model: "deterministic", endpoint: "http://127.0.0.1:45678", port: 45678, pid: 999999, instance: "fxtest", entry: FIXTURE_ENTRY }; function fixtureRoot(t, { record = RECORD, text = JSON.stringify(record), dirMode = 0o700, fileMode = 0o600 } = {}) { const dataRoot = join(under(t), "data"); const file = fixtureFile(dataRoot); mkdirSync(dirname(file), { recursive: true, mode: dirMode }); chmodSync(dirname(file), dirMode); if (text !== null) { writeFileSync(file, text, { mode: fileMode }); chmodSync(file, fileMode); } return dataRoot; } test("dev fixture: a shared fixture directory is refused by label", (t) => { const dataRoot = fixtureRoot(t, { dirMode: 0o755 }); assert.throws(() => readFixture(dataRoot), refusal("fixture-state-unsafe: dev-fixture directory is not a private directory of this user", { code: "fixture-state-unsafe", paths: [dataRoot] })); }); test("dev fixture: a linked fixture file is refused by label and error code", (t) => { const dataRoot = fixtureRoot(t, { text: null }); writeFileSync(join(dataRoot, "elsewhere.json"), JSON.stringify(RECORD), { mode: 0o600 }); symlinkSync(join(dataRoot, "elsewhere.json"), fixtureFile(dataRoot)); assert.throws(() => readFixture(dataRoot), refusal("fixture-state-unsafe: fixture.json can't be opened (ELOOP)", { code: "fixture-state-unsafe", paths: [dataRoot] })); }); test("dev fixture: a shared fixture file is refused by label", (t) => { const dataRoot = fixtureRoot(t, { fileMode: 0o644 }); assert.throws(() => readFixture(dataRoot), refusal("fixture-state-unsafe: fixture.json is not a private file of this user", { code: "fixture-state-unsafe", paths: [dataRoot] })); }); test("dev fixture: a fixture file that is not JSON is refused by label", (t) => { const dataRoot = fixtureRoot(t, { text: "not json" }); assert.throws(() => readFixture(dataRoot), refusal("fixture-state-invalid: fixture.json is not JSON", { code: "fixture-state-invalid", paths: [dataRoot] })); }); test("dev fixture: a non-loopback endpoint and a record of the wrong shape are refused by label", (t) => { let dataRoot = fixtureRoot(t, { record: { ...RECORD, endpoint: "http://0.0.0.0:45678" } }); assert.throws(() => readFixture(dataRoot), refusal("fixture-endpoint-not-loopback: fixture.json names an endpoint that is not http://127.0.0.1:", { code: "fixture-endpoint-not-loopback", paths: [dataRoot] })); dataRoot = fixtureRoot(t, { record: { ...RECORD, port: 1 } }); assert.throws(() => readFixture(dataRoot), refusal("fixture-state-invalid: fixture.json is not a fixture record", { code: "fixture-state-invalid", paths: [dataRoot] })); }); // --- git socket --- test("git socket: a path that exists and is not a socket is refused by label", async (t) => { const dataRoot = join(under(t), "data"); const path = gitSocketPath(dataRoot); mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); writeFileSync(path, "", { mode: 0o600 }); const sock = createGitSocket({ host: {}, business: { id: "acme", projects: {} }, dataRoot, env: {}, log: () => {} }); await assert.rejects(sock.listen(), refusal("git socket path exists and isn't a socket", { paths: [dataRoot, path] })); }); // --- console launch --- // The Console is given a port this test holds, so its listen fails and it exits at // startup. Should it start anyway, it is stopped before the tree goes. test("console: a Console that exits at startup is named by its log under the data root, never by host path", { timeout: 60000 }, async (t) => { const root = under(t); const ctx = { configDir: join(root, "config"), dataRoot: join(root, "data"), instance: "redact", env: { PATH: process.env.PATH, HOME: root } }; for (const dir of [ctx.configDir, ctx.dataRoot]) mkdirSync(dir, { mode: 0o700 }); const held = createServer(); await new Promise((resolve, reject) => held.once("error", reject).listen(0, "127.0.0.1", resolve)); try { await assert.rejects(consoleStart(ctx, { detach: true, port: String(held.address().port), warn: () => {} }), refusal(/^console failed to start; see console\/console-[0-9]+-[0-9]+\.log under the data root$/, { paths: [ctx.dataRoot] })); } finally { await consoleStop(ctx, { warn: () => {} }).catch(() => {}); await new Promise((resolve) => held.close(resolve)); } }); // --- review round (org-redact5 Sol P2 #3): documents, --vars and raw fs errors --- const quietIo = () => ({ stdout: { write: () => true }, stderr: { write: () => true } }); test("org validate: a missing or invalid document is refused by kind, never by host path or bytes", async (t) => { const root = under(t), missing = join(root, "missing.json"), bad = join(root, "bad.json"); writeFileSync(bad, `{"${SENTINEL}": `, { mode: 0o600 }); await assert.rejects(orgCommand(["validate", missing], quietIo()), refusal("cannot read organization document: ENOENT", { exitCode: 2, paths: [missing, root] })); await assert.rejects(orgCommand(["validate", bad], quietIo()), refusal("cannot read organization document: not valid JSON", { exitCode: 2, paths: [bad, root] })); }); test("org template render: a raw filesystem error is named by operation and code, never by host path", async (t) => { const root = under(t), output = join(root, "alpha.json"); writeFileSync(output, "{}\n", { mode: 0o600 }); await assert.rejects(orgCommand(["template", "render", "alpha", "--output", output], quietIo()), refusal("EEXIST: cannot open (EEXIST)", { exitCode: 2, paths: [output, root] })); }); test("instance set: a missing or invalid --vars file is refused by flag and kind, never by host path or bytes", async (t) => { const root = under(t), missing = join(root, "vars.json"), bad = join(root, "bad.json"); writeFileSync(bad, `{"${SENTINEL}": `, { mode: 0o600 }); const rev = "a".repeat(64); const callFor = () => assert.fail("must refuse before any call"); await assert.rejects(instancesCommand(["instance", "set", "acme", "coder", "--vars", missing, "--revision", rev], quietIo(), callFor), refusal("--vars file: ENOENT", { exitCode: 4, paths: [missing, root] })); await assert.rejects(instancesCommand(["instance", "set", "acme", "coder", "--vars", bad, "--revision", rev], quietIo(), callFor), refusal("--vars file: not valid JSON", { exitCode: 4, paths: [bad, root] })); });