diff --git a/src/config.ts b/src/config.ts index be8090c31..8e6028354 100644 --- a/src/config.ts +++ b/src/config.ts @@ -412,10 +412,9 @@ export function backupConfigBeforeOpenAiTierMigration( write: (target, bytes) => writeFileSync(target, bytes), harden: target => { try { chmodSync(target, 0o600); } catch { /* platform may ignore chmod */ } - // Soft-fail: a wedged/failed icacls on CI temp volumes must not abort - // startServer mid-suite (timeout + EBUSY cascade on shared TEST_DIR). - // chmod above still applies; live credential writes keep required:true. - if (process.platform === "win32") hardenSecretPath(target, { required: false }); + // The snapshot contains the same secrets as config.json, so it must not be + // published unless inherited Windows ACLs were removed successfully. + if (process.platform === "win32") hardenSecretPath(target, { required: true }); }, publishNoReplace: (temp, backup) => linkSync(temp, backup), truncate: target => truncateSync(target, 0), diff --git a/tests/openai-provider-option-startup.test.ts b/tests/openai-provider-option-startup.test.ts index 9e9e61c33..9c2ff6199 100644 --- a/tests/openai-provider-option-startup.test.ts +++ b/tests/openai-provider-option-startup.test.ts @@ -291,6 +291,38 @@ describe("OpenAI provider option startup coordinator", () => { expect([...state.files.keys()].filter(path => path.endsWith(".tmp"))).toEqual([]); }); + test("default backup creation fails closed when Windows ACL hardening fails", () => { + const root = mkdtempSync(join(tmpdir(), "ocx-backup-acl-failure-")); + const source = join(root, "config.json"); + const backup = `${source}.pre-openai-tiers-v2.bak`; + const previousPlatform = process.platform; + const previousUsername = process.env.USERNAME; + process.platform = "win32"; + process.env.USERNAME = "ocx-test-user"; + windowsAcl.resetHardenedStateForTests(); + windowsAcl.setPlatformForTests("win32"); + windowsAcl.setIcaclsRunnerForTests(() => ({ + success: false, + exitCode: 5, + timedOut: false, + stdout: "Access is denied.", + })); + try { + writeFileSync(source, "original-secret"); + expect(() => backupConfigBeforeOpenAiTierMigration(source)).toThrow(); + expect(existsSync(backup)).toBe(false); + expect(readdirSync(root).filter(name => name.endsWith(".tmp"))).toEqual([]); + } finally { + windowsAcl.setIcaclsRunnerForTests(null); + windowsAcl.setPlatformForTests(null); + windowsAcl.resetHardenedStateForTests(); + process.platform = previousPlatform; + if (previousUsername === undefined) delete process.env.USERNAME; + else process.env.USERNAME = previousUsername; + rmSync(root, { recursive: true, force: true }); + } + }); + test("backup temp cleanup forgets successful ACL memos and retains failed removals", () => { const root = mkdtempSync(join(tmpdir(), "ocx-backup-acl-")); const source = join(root, "config.json");