From 507604a288e575939a9abd5b0acaae683959ad6c Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:18:55 +0200 Subject: [PATCH 1/8] fix: address issue #766 Fixes #766 --- src/server/management-auth.ts | 85 +++++++++++++++++++++----- src/server/management/config-routes.ts | 2 + tests/server-management-auth.test.ts | 37 +++++++++++ 3 files changed, 110 insertions(+), 14 deletions(-) diff --git a/src/server/management-auth.ts b/src/server/management-auth.ts index 52fd46401..fcb0e12ad 100644 --- a/src/server/management-auth.ts +++ b/src/server/management-auth.ts @@ -43,6 +43,8 @@ export type ManagementAuthState = token: string; source: "environment" | "file"; sessions: Map; + /** Set when file-backed token was accepted despite unverified Windows ACL harden. */ + aclUnverified?: boolean; } | { available: false; reason: string }; @@ -50,38 +52,58 @@ function fail(reason: string): ManagementAuthState { return { available: false, reason }; } +function allowUnverifiedAdminTokenAcl(): boolean { + const raw = process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL?.trim().toLowerCase(); + return raw === "1" || raw === "true" || raw === "yes"; +} + function assertSafeDirectory(path: string): void { mkdirSync(path, { recursive: true, mode: 0o700 }); const stat = lstatSync(path); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("management token directory is not a regular directory"); chmodSync(path, 0o700); const hardened = hardenSecretDir(path, { required: true }); - if (!hardened.ok) throw new Error("management token directory ACL hardening did not complete"); + if (!hardened.ok) { + if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(hardened.diagnostics ?? "")) { + console.warn(`[opencodex] management token directory ACL unverified (${hardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); + return; + } + throw new Error("management token directory ACL hardening did not complete"); + } } -function readExistingToken(path: string): string { +function readExistingToken(path: string): { token: string; aclUnverified: boolean } { const stat = lstatSync(path); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 512) { throw new Error("management token path is not a regular secret file"); } chmodSync(path, 0o600); const hardened = hardenSecretPath(path, { required: true }); - if (!hardened.ok) throw new Error("management token file ACL hardening did not complete"); + let aclUnverified = false; + if (!hardened.ok) { + if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(hardened.diagnostics ?? "")) { + console.warn(`[opencodex] management token file ACL unverified (${hardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); + aclUnverified = true; + } else { + throw new Error("management token file ACL hardening did not complete"); + } + } const token = readFileSync(path, "utf8").trim(); if (!/^ocx_admin_[A-Za-z0-9_-]{43}$/.test(token)) throw new Error("management token file is invalid"); - return token; + return { token, aclUnverified }; } function removeBestEffort(path: string): void { try { unlinkSync(path); } catch { /* fail-closed state is preserved by the caller */ } } -function createTokenFile(path: string): string { +function createTokenFile(path: string): { token: string; aclUnverified: boolean } { const directory = dirname(path); const token = `ocx_admin_${randomBytes(32).toString("base64url")}`; const temporary = join(directory, `.${randomUUID()}.admin-token.tmp`); let linked = false; let fd: number | null = null; + let aclUnverified = false; try { fd = openSync(temporary, "wx", 0o600); writeFileSync(fd, `${token}\n`, "utf8"); @@ -90,7 +112,14 @@ function createTokenFile(path: string): string { fd = null; chmodSync(temporary, 0o600); const temporaryHardened = hardenSecretPath(temporary, { required: true }); - if (!temporaryHardened.ok) throw new Error("management token temporary ACL hardening did not complete"); + if (!temporaryHardened.ok) { + if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(temporaryHardened.diagnostics ?? "")) { + console.warn(`[opencodex] management token temporary ACL unverified (${temporaryHardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); + aclUnverified = true; + } else { + throw new Error("management token temporary ACL hardening did not complete"); + } + } try { linkSync(temporary, path); linked = true; @@ -99,8 +128,15 @@ function createTokenFile(path: string): string { throw error; } const finalHardened = hardenSecretPath(path, { required: true }); - if (!finalHardened.ok) throw new Error("management token file ACL hardening did not complete"); - return token; + if (!finalHardened.ok) { + if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(finalHardened.diagnostics ?? "")) { + console.warn(`[opencodex] management token file ACL unverified (${finalHardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); + aclUnverified = true; + } else { + throw new Error("management token file ACL hardening did not complete"); + } + } + return { token, aclUnverified }; } catch (error) { if (linked) removeBestEffort(path); throw error; @@ -112,30 +148,51 @@ function createTokenFile(path: string): string { } } -function ready(token: string, source: "environment" | "file", config: OcxConfig): ManagementAuthState { +function ready( + token: string, + source: "environment" | "file", + config: OcxConfig, + options: { aclUnverified?: boolean } = {}, +): ManagementAuthState { if (isDataPlaneAdmissionSecret(token, config)) { return fail("management credential conflicts with a data-plane credential"); } - return { available: true, token, source, sessions: new Map() }; + return { + available: true, + token, + source, + sessions: new Map(), + ...(options.aclUnverified ? { aclUnverified: true } : {}), + }; +} + +let lastManagementAuthAclUnverified = false; + +/** Whether the current process accepted a file-backed admin token without verified NTFS ACL harden. */ +export function managementAuthAclUnverified(): boolean { + return lastManagementAuthAclUnverified; } export function initializeManagementAuthState(config: OcxConfig): ManagementAuthState { const environmentToken = process.env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim(); if (environmentToken) { + lastManagementAuthAclUnverified = false; return ready(environmentToken, "environment", config); } try { const path = adminApiTokenFilePath(); assertSafeDirectory(dirname(path)); - let token: string; + let loaded: { token: string; aclUnverified: boolean }; try { - token = readExistingToken(path); + loaded = readExistingToken(path); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; - token = createTokenFile(path); + loaded = createTokenFile(path); } - return ready(token, "file", config); + lastManagementAuthAclUnverified = loaded.aclUnverified === true; + return ready(loaded.token, "file", config, { aclUnverified: loaded.aclUnverified }); } catch (error) { + lastManagementAuthAclUnverified = false; return fail(error instanceof Error ? error.message : "management token initialization failed"); } } diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 9a05fbef9..4d83e9c23 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -55,6 +55,7 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ". import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost"; import type { PersistedUsageAttempt } from "../../usage/log"; import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; +import { managementAuthAclUnverified } from "../management-auth"; import { applySystemEnvToggle } from "../system-env"; import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache"; import { runWindowsTrayAction } from "../windows-tray-control"; @@ -121,6 +122,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise { else process.env.OPENCODEX_API_AUTH_TOKEN = previousDataToken; if (previousAdminToken === undefined) delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; else process.env.OPENCODEX_ADMIN_AUTH_TOKEN = previousAdminToken; + if (previousAllowUnverifiedAcl === undefined) delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; + else process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = previousAllowUnverifiedAcl; if (testHome) rmSync(testHome, { recursive: true, force: true }); testHome = ""; }); @@ -436,4 +440,37 @@ describe("management and data-plane credential separation", () => { await server.stop(true); } }); + + test("OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL soft-fails timeouts and surfaces aclUnverified", async () => { + delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; + process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = "1"; + saveConfig(remoteConfig()); + const adminToken = `ocx_admin_${"c".repeat(43)}`; + writeFileSync(join(testHome, "admin-api-token"), `${adminToken}\n`, { mode: 0o600 }); + setPlatformForTests("win32"); + setIcaclsRunnerForTests(args => { + const target = args[0] ?? ""; + if (target.endsWith("admin-api-token")) { + return { success: false, exitCode: null, timedOut: true, stdout: "" }; + } + return { success: true, exitCode: 0, timedOut: false, stdout: "" }; + }); + const state = initializeManagementAuthState(remoteConfig()); + expect(state.available).toBe(true); + if (!state.available) return; + expect(state.aclUnverified).toBe(true); + expect(managementAuthAclUnverified()).toBe(true); + + const server = startServer(0); + try { + const settings = await fetch(new URL("/api/settings", server.url), { + headers: { "x-opencodex-api-key": adminToken }, + }); + expect(settings.status).toBe(200); + const body = await settings.json() as { managementAuthAclUnverified?: boolean }; + expect(body.managementAuthAclUnverified).toBe(true); + } finally { + await server.stop(true); + } + }); }); From 9cdb0da2ee3d78835df1f1ae5388e82b3924f5dd Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:42:35 +0200 Subject: [PATCH 2/8] test: set USERNAME in win32 ACL opt-in test on CI GitHub macOS/Linux runners lack USERNAME; fake win32 ACL tests need it. --- tests/server-management-auth.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/server-management-auth.test.ts b/tests/server-management-auth.test.ts index 18595d24b..5cb1a5889 100644 --- a/tests/server-management-auth.test.ts +++ b/tests/server-management-auth.test.ts @@ -447,6 +447,7 @@ describe("management and data-plane credential separation", () => { saveConfig(remoteConfig()); const adminToken = `ocx_admin_${"c".repeat(43)}`; writeFileSync(join(testHome, "admin-api-token"), `${adminToken}\n`, { mode: 0o600 }); + process.env.USERNAME ??= "tester"; setPlatformForTests("win32"); setIcaclsRunnerForTests(args => { const target = args[0] ?? ""; From 20d72015d84e74a94d0cc66ba325753a916b7a0e Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:13:10 +0200 Subject: [PATCH 3/8] fix(service): propagate ACL opt-in into Windows service env Bake OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL into Task Scheduler and WinSW installs when set at install time, and isolate the opt-in in management-auth tests. --- src/lib/allow-unverified-admin-token-acl.ts | 10 ++++++++++ src/lib/winsw.ts | 2 ++ src/server/management-auth.ts | 6 +----- src/service.ts | 2 ++ tests/server-management-auth.test.ts | 1 + tests/service.test.ts | 11 +++++++++++ tests/winsw.test.ts | 4 ++++ 7 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 src/lib/allow-unverified-admin-token-acl.ts diff --git a/src/lib/allow-unverified-admin-token-acl.ts b/src/lib/allow-unverified-admin-token-acl.ts new file mode 100644 index 000000000..14eadaa0b --- /dev/null +++ b/src/lib/allow-unverified-admin-token-acl.ts @@ -0,0 +1,10 @@ +export function allowUnverifiedAdminTokenAcl(env: NodeJS.ProcessEnv = process.env): boolean { + const raw = env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL?.trim().toLowerCase(); + return raw === "1" || raw === "true" || raw === "yes"; +} + +/** Truthy install-time value to bake into Windows service environments. */ +export function bakedAllowUnverifiedAdminTokenAcl(env: NodeJS.ProcessEnv = process.env): string | undefined { + if (!allowUnverifiedAdminTokenAcl(env)) return undefined; + return env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL!.trim(); +} diff --git a/src/lib/winsw.ts b/src/lib/winsw.ts index 5222e1228..2970ecb3a 100644 --- a/src/lib/winsw.ts +++ b/src/lib/winsw.ts @@ -21,6 +21,7 @@ import { join, resolve } from "node:path"; import { expandUserPath, getConfigDir, loadConfig } from "../config"; import { recordOwnedConfigPath } from "./config-ownership"; import { durableBunPath } from "./bun-runtime"; +import { bakedAllowUnverifiedAdminTokenAcl } from "./allow-unverified-admin-token-acl"; import { serviceApiTokenFilePath } from "./service-secrets"; export const WINSW_VERSION = "2.12.0"; @@ -100,6 +101,7 @@ export function buildWinswXml(entry: WinswEntry, env: NodeJS.ProcessEnv = proces env.CODEX_HOME?.trim() ? ` ` : null, ` `, aclTimeout ? ` ` : null, + bakedAllowUnverifiedAdminTokenAcl(env) ? ` ` : null, ].filter((line): line is string => Boolean(line)); return ` diff --git a/src/server/management-auth.ts b/src/server/management-auth.ts index fcb0e12ad..a5451f7a2 100644 --- a/src/server/management-auth.ts +++ b/src/server/management-auth.ts @@ -13,6 +13,7 @@ import { } from "node:fs"; import { dirname, join } from "node:path"; import { adminApiTokenFilePath } from "../lib/admin-secrets"; +import { allowUnverifiedAdminTokenAcl } from "../lib/allow-unverified-admin-token-acl"; import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl"; import type { OcxConfig } from "../types"; import { @@ -52,11 +53,6 @@ function fail(reason: string): ManagementAuthState { return { available: false, reason }; } -function allowUnverifiedAdminTokenAcl(): boolean { - const raw = process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL?.trim().toLowerCase(); - return raw === "1" || raw === "true" || raw === "yes"; -} - function assertSafeDirectory(path: string): void { mkdirSync(path, { recursive: true, mode: 0o700 }); const stat = lstatSync(path); diff --git a/src/service.ts b/src/service.ts index bdf614079..e90a4ff11 100644 --- a/src/service.ts +++ b/src/service.ts @@ -34,6 +34,7 @@ import { } from "./lib/windows-elevation"; import { defaultWinswEntry, installWinswService, startWinswService, stopWinswService, statusWinswRaw, uninstallWinswService, winswStatusSummary, WINSW_SERVICE_ID, WINSW_SHA256, WINSW_VERSION } from "./lib/winsw"; import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl"; +import { bakedAllowUnverifiedAdminTokenAcl } from "./lib/allow-unverified-admin-token-acl"; import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths"; import { recordOwnedConfigPath } from "./lib/config-ownership"; import { maybeShowStarPrompt } from "./cli/star-prompt"; @@ -1039,6 +1040,7 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ windowsBatchSet("OCX_SERVICE_LOG", serviceLogPath(), "path"), windowsBatchSet("OCX_BUN", bun, "path"), windowsBatchSet("OCX_CLI", cli, "path"), + windowsBatchSet("OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL", bakedAllowUnverifiedAdminTokenAcl()), 'if exist "%OCX_API_TOKEN_FILE%" (', ' set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"', ")", diff --git a/tests/server-management-auth.test.ts b/tests/server-management-auth.test.ts index 5cb1a5889..0ebddfefa 100644 --- a/tests/server-management-auth.test.ts +++ b/tests/server-management-auth.test.ts @@ -64,6 +64,7 @@ function websocketHandshakeOpens(url: URL, token: string): Promise { } beforeEach(() => { + delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; testHome = mkdtempSync(join(tmpdir(), "ocx-management-auth-")); process.env.OPENCODEX_HOME = testHome; process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; diff --git a/tests/service.test.ts b/tests/service.test.ts index e2005372a..721a9bd61 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -501,6 +501,17 @@ describe("Windows service task", () => { expect(script).toContain('"%OCX_BUN%" "%OCX_CLI%" start --port'); expect(script).not.toContain('"C:\\Bun&Dir\\100%bun^\\bun.exe"'); }); + test("bakes OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL when install env opts in", () => { + const previous = process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; + try { + process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = "yes"; + const script = buildWindowsServiceScript({ bun: "C:\\OpenCodex\\bun.exe", cli: "C:\\OpenCodex\\cli.ts" }); + expect(script).toContain('set "OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL=yes"'); + } finally { + if (previous === undefined) delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; + else process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = previous; + } + }); test("switches the wrapper console to UTF-8 and sleeps via ping (timeout dies without console stdin)", () => { const script = buildWindowsServiceScript({ bun: "C:\\OpenCodex\\bun.exe", cli: "C:\\OpenCodex\\cli.ts" }); diff --git a/tests/winsw.test.ts b/tests/winsw.test.ts index eb42df8ee..a3448bdb6 100644 --- a/tests/winsw.test.ts +++ b/tests/winsw.test.ts @@ -73,6 +73,10 @@ describe("winsw xml", () => { expect(xml).toContain(''); expect(xml).toContain(`${WINSW_SERVICE_ID}`); }); + test("carries OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL when install env opts in", () => { + const xml = buildWinswXml(entry, { ...env, OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL: "true" }); + expect(xml).toContain(''); + }); test("honors OCX_BAKE_PORT when building WinSW arguments", () => { const xml = buildWinswXml(entry, { ...env, OCX_BAKE_PORT: "14444" }); expect(xml).toContain("start --port 14444"); From f1d1893a008cf858e6835cb594d8bc7bacbb9bc1 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:23:58 +0200 Subject: [PATCH 4/8] fix(security): propagate directory ACL unverified into management auth assertSafeDirectory soft-continues were discarded, so /api/settings could under-report. OR directory status into aclUnverified and cover dir-timeout vs file-ok with and without the opt-in. --- src/server/management-auth.ts | 13 ++++-- tests/server-management-auth.test.ts | 69 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/server/management-auth.ts b/src/server/management-auth.ts index a5451f7a2..dd18c597b 100644 --- a/src/server/management-auth.ts +++ b/src/server/management-auth.ts @@ -53,7 +53,8 @@ function fail(reason: string): ManagementAuthState { return { available: false, reason }; } -function assertSafeDirectory(path: string): void { +/** Returns true when the directory was accepted without verified NTFS ACL harden. */ +function assertSafeDirectory(path: string): boolean { mkdirSync(path, { recursive: true, mode: 0o700 }); const stat = lstatSync(path); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("management token directory is not a regular directory"); @@ -62,10 +63,11 @@ function assertSafeDirectory(path: string): void { if (!hardened.ok) { if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(hardened.diagnostics ?? "")) { console.warn(`[opencodex] management token directory ACL unverified (${hardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); - return; + return true; } throw new Error("management token directory ACL hardening did not complete"); } + return false; } function readExistingToken(path: string): { token: string; aclUnverified: boolean } { @@ -177,7 +179,7 @@ export function initializeManagementAuthState(config: OcxConfig): ManagementAuth } try { const path = adminApiTokenFilePath(); - assertSafeDirectory(dirname(path)); + const directoryUnverified = assertSafeDirectory(dirname(path)); let loaded: { token: string; aclUnverified: boolean }; try { loaded = readExistingToken(path); @@ -185,8 +187,9 @@ export function initializeManagementAuthState(config: OcxConfig): ManagementAuth if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; loaded = createTokenFile(path); } - lastManagementAuthAclUnverified = loaded.aclUnverified === true; - return ready(loaded.token, "file", config, { aclUnverified: loaded.aclUnverified }); + const aclUnverified = directoryUnverified || loaded.aclUnverified === true; + lastManagementAuthAclUnverified = aclUnverified; + return ready(loaded.token, "file", config, { aclUnverified }); } catch (error) { lastManagementAuthAclUnverified = false; return fail(error instanceof Error ? error.message : "management token initialization failed"); diff --git a/tests/server-management-auth.test.ts b/tests/server-management-auth.test.ts index 0ebddfefa..181ffcbf9 100644 --- a/tests/server-management-auth.test.ts +++ b/tests/server-management-auth.test.ts @@ -475,4 +475,73 @@ describe("management and data-plane credential separation", () => { await server.stop(true); } }); + + test("directory ACL timeout without opt-in keeps management unavailable even when the token file hardens", async () => { + delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; + delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; + saveConfig(remoteConfig()); + const adminToken = `ocx_admin_${"d".repeat(43)}`; + writeFileSync(join(testHome, "admin-api-token"), `${adminToken}\n`, { mode: 0o600 }); + process.env.USERNAME ??= "tester"; + setPlatformForTests("win32"); + setIcaclsRunnerForTests(args => { + const target = args[0] ?? ""; + // Directory harden times out; token-file harden succeeds. + if (target.endsWith("admin-api-token")) { + return { success: true, exitCode: 0, timedOut: false, stdout: "" }; + } + return { success: false, exitCode: null, timedOut: true, stdout: "" }; + }); + // Drop any directory cache left by saveConfig so this case actually re-hardens. + resetHardenedStateForTests(); + const state = initializeManagementAuthState(remoteConfig()); + expect(state.available).toBe(false); + expect(managementAuthAclUnverified()).toBe(false); + + const server = startServer(0); + try { + const settings = await fetch(new URL("/api/settings", server.url), { + headers: { "x-opencodex-api-key": adminToken }, + }); + expect(settings.status).toBe(503); + expect((await fetch(new URL("/healthz", server.url))).status).toBe(200); + } finally { + await server.stop(true); + } + }); + + test("directory ACL timeout with opt-in surfaces aclUnverified when the token file hardens", async () => { + delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; + process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = "1"; + saveConfig(remoteConfig()); + const adminToken = `ocx_admin_${"e".repeat(43)}`; + writeFileSync(join(testHome, "admin-api-token"), `${adminToken}\n`, { mode: 0o600 }); + process.env.USERNAME ??= "tester"; + setPlatformForTests("win32"); + setIcaclsRunnerForTests(args => { + const target = args[0] ?? ""; + if (target.endsWith("admin-api-token")) { + return { success: true, exitCode: 0, timedOut: false, stdout: "" }; + } + return { success: false, exitCode: null, timedOut: true, stdout: "" }; + }); + resetHardenedStateForTests(); + const state = initializeManagementAuthState(remoteConfig()); + expect(state.available).toBe(true); + if (!state.available) return; + expect(state.aclUnverified).toBe(true); + expect(managementAuthAclUnverified()).toBe(true); + + const server = startServer(0); + try { + const settings = await fetch(new URL("/api/settings", server.url), { + headers: { "x-opencodex-api-key": adminToken }, + }); + expect(settings.status).toBe(200); + const body = await settings.json() as { managementAuthAclUnverified?: boolean }; + expect(body.managementAuthAclUnverified).toBe(true); + } finally { + await server.stop(true); + } + }); }); From 2b8c82cee7e2fdf38c47298847462dce64d6e63f Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:41:04 +0200 Subject: [PATCH 5/8] fix(security): retry required ACL harden after soft timeout Drop OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL. Namespace the timeout memo so loadConfig soft-fails no longer poison management required:true hardens, and surface OPENCODEX_ADMIN_AUTH_TOKEN as the fail-closed escape on 503. --- src/lib/allow-unverified-admin-token-acl.ts | 10 -- src/lib/windows-secret-acl.ts | 6 +- src/lib/winsw.ts | 2 - src/server/management-auth.ts | 94 ++++++------------ src/server/management/config-routes.ts | 2 - src/service.ts | 2 - tests/server-management-auth.test.ts | 104 +++++++++----------- tests/service.test.ts | 11 --- tests/windows-secret-acl.test.ts | 15 +++ tests/winsw.test.ts | 4 - 10 files changed, 97 insertions(+), 153 deletions(-) delete mode 100644 src/lib/allow-unverified-admin-token-acl.ts diff --git a/src/lib/allow-unverified-admin-token-acl.ts b/src/lib/allow-unverified-admin-token-acl.ts deleted file mode 100644 index 14eadaa0b..000000000 --- a/src/lib/allow-unverified-admin-token-acl.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function allowUnverifiedAdminTokenAcl(env: NodeJS.ProcessEnv = process.env): boolean { - const raw = env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL?.trim().toLowerCase(); - return raw === "1" || raw === "true" || raw === "yes"; -} - -/** Truthy install-time value to bake into Windows service environments. */ -export function bakedAllowUnverifiedAdminTokenAcl(env: NodeJS.ProcessEnv = process.env): string | undefined { - if (!allowUnverifiedAdminTokenAcl(env)) return undefined; - return env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL!.trim(); -} diff --git a/src/lib/windows-secret-acl.ts b/src/lib/windows-secret-acl.ts index e5ce2eee9..fa898c3b2 100644 --- a/src/lib/windows-secret-acl.ts +++ b/src/lib/windows-secret-acl.ts @@ -363,7 +363,11 @@ async function describeAclStateAfterTimeoutAsync(targetPath: string, deadline: n function timeoutMemoKey(targetPath: string, opts: HardenOptions): string { // Destination-path memo only (issue #612). Never a parent directory — directory ACLs // are not authoritative for newly created temps. - return opts.timeoutMemoKey ?? targetPath; + // + // Namespace by required-ness (#766): a soft `required:false` timeout during loadConfig + // must not poison a later `required:true` management-token harden of the same path. + const base = opts.timeoutMemoKey ?? targetPath; + return `${opts.required ? "required" : "optional"}:${base}`; } /** diff --git a/src/lib/winsw.ts b/src/lib/winsw.ts index 2970ecb3a..5222e1228 100644 --- a/src/lib/winsw.ts +++ b/src/lib/winsw.ts @@ -21,7 +21,6 @@ import { join, resolve } from "node:path"; import { expandUserPath, getConfigDir, loadConfig } from "../config"; import { recordOwnedConfigPath } from "./config-ownership"; import { durableBunPath } from "./bun-runtime"; -import { bakedAllowUnverifiedAdminTokenAcl } from "./allow-unverified-admin-token-acl"; import { serviceApiTokenFilePath } from "./service-secrets"; export const WINSW_VERSION = "2.12.0"; @@ -101,7 +100,6 @@ export function buildWinswXml(entry: WinswEntry, env: NodeJS.ProcessEnv = proces env.CODEX_HOME?.trim() ? ` ` : null, ` `, aclTimeout ? ` ` : null, - bakedAllowUnverifiedAdminTokenAcl(env) ? ` ` : null, ].filter((line): line is string => Boolean(line)); return ` diff --git a/src/server/management-auth.ts b/src/server/management-auth.ts index dd18c597b..164b843f0 100644 --- a/src/server/management-auth.ts +++ b/src/server/management-auth.ts @@ -13,7 +13,6 @@ import { } from "node:fs"; import { dirname, join } from "node:path"; import { adminApiTokenFilePath } from "../lib/admin-secrets"; -import { allowUnverifiedAdminTokenAcl } from "../lib/allow-unverified-admin-token-acl"; import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl"; import type { OcxConfig } from "../types"; import { @@ -44,8 +43,6 @@ export type ManagementAuthState = token: string; source: "environment" | "file"; sessions: Map; - /** Set when file-backed token was accepted despite unverified Windows ACL harden. */ - aclUnverified?: boolean; } | { available: false; reason: string }; @@ -53,55 +50,46 @@ function fail(reason: string): ManagementAuthState { return { available: false, reason }; } -/** Returns true when the directory was accepted without verified NTFS ACL harden. */ -function assertSafeDirectory(path: string): boolean { +function assertSafeDirectory(path: string): void { mkdirSync(path, { recursive: true, mode: 0o700 }); const stat = lstatSync(path); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("management token directory is not a regular directory"); chmodSync(path, 0o700); const hardened = hardenSecretDir(path, { required: true }); if (!hardened.ok) { - if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(hardened.diagnostics ?? "")) { - console.warn(`[opencodex] management token directory ACL unverified (${hardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); - return true; - } - throw new Error("management token directory ACL hardening did not complete"); + throw new Error( + "management token directory ACL hardening did not complete; set OPENCODEX_ADMIN_AUTH_TOKEN to use an environment token instead of a file-backed token", + ); } - return false; } -function readExistingToken(path: string): { token: string; aclUnverified: boolean } { +function readExistingToken(path: string): string { const stat = lstatSync(path); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 512) { throw new Error("management token path is not a regular secret file"); } chmodSync(path, 0o600); const hardened = hardenSecretPath(path, { required: true }); - let aclUnverified = false; if (!hardened.ok) { - if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(hardened.diagnostics ?? "")) { - console.warn(`[opencodex] management token file ACL unverified (${hardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); - aclUnverified = true; - } else { - throw new Error("management token file ACL hardening did not complete"); - } + throw new Error( + "management token file ACL hardening did not complete; set OPENCODEX_ADMIN_AUTH_TOKEN to use an environment token instead of a file-backed token", + ); } const token = readFileSync(path, "utf8").trim(); if (!/^ocx_admin_[A-Za-z0-9_-]{43}$/.test(token)) throw new Error("management token file is invalid"); - return { token, aclUnverified }; + return token; } function removeBestEffort(path: string): void { try { unlinkSync(path); } catch { /* fail-closed state is preserved by the caller */ } } -function createTokenFile(path: string): { token: string; aclUnverified: boolean } { +function createTokenFile(path: string): string { const directory = dirname(path); const token = `ocx_admin_${randomBytes(32).toString("base64url")}`; const temporary = join(directory, `.${randomUUID()}.admin-token.tmp`); let linked = false; let fd: number | null = null; - let aclUnverified = false; try { fd = openSync(temporary, "wx", 0o600); writeFileSync(fd, `${token}\n`, "utf8"); @@ -111,12 +99,9 @@ function createTokenFile(path: string): { token: string; aclUnverified: boolean chmodSync(temporary, 0o600); const temporaryHardened = hardenSecretPath(temporary, { required: true }); if (!temporaryHardened.ok) { - if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(temporaryHardened.diagnostics ?? "")) { - console.warn(`[opencodex] management token temporary ACL unverified (${temporaryHardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); - aclUnverified = true; - } else { - throw new Error("management token temporary ACL hardening did not complete"); - } + throw new Error( + "management token temporary ACL hardening did not complete; set OPENCODEX_ADMIN_AUTH_TOKEN to use an environment token instead of a file-backed token", + ); } try { linkSync(temporary, path); @@ -127,14 +112,11 @@ function createTokenFile(path: string): { token: string; aclUnverified: boolean } const finalHardened = hardenSecretPath(path, { required: true }); if (!finalHardened.ok) { - if (allowUnverifiedAdminTokenAcl() && /timed out|ETIMEDOUT|budget exhausted|previous attempt timed out/i.test(finalHardened.diagnostics ?? "")) { - console.warn(`[opencodex] management token file ACL unverified (${finalHardened.diagnostics}); continuing because OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL is set`); - aclUnverified = true; - } else { - throw new Error("management token file ACL hardening did not complete"); - } + throw new Error( + "management token file ACL hardening did not complete; set OPENCODEX_ADMIN_AUTH_TOKEN to use an environment token instead of a file-backed token", + ); } - return { token, aclUnverified }; + return token; } catch (error) { if (linked) removeBestEffort(path); throw error; @@ -146,52 +128,30 @@ function createTokenFile(path: string): { token: string; aclUnverified: boolean } } -function ready( - token: string, - source: "environment" | "file", - config: OcxConfig, - options: { aclUnverified?: boolean } = {}, -): ManagementAuthState { +function ready(token: string, source: "environment" | "file", config: OcxConfig): ManagementAuthState { if (isDataPlaneAdmissionSecret(token, config)) { return fail("management credential conflicts with a data-plane credential"); } - return { - available: true, - token, - source, - sessions: new Map(), - ...(options.aclUnverified ? { aclUnverified: true } : {}), - }; -} - -let lastManagementAuthAclUnverified = false; - -/** Whether the current process accepted a file-backed admin token without verified NTFS ACL harden. */ -export function managementAuthAclUnverified(): boolean { - return lastManagementAuthAclUnverified; + return { available: true, token, source, sessions: new Map() }; } export function initializeManagementAuthState(config: OcxConfig): ManagementAuthState { const environmentToken = process.env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim(); if (environmentToken) { - lastManagementAuthAclUnverified = false; return ready(environmentToken, "environment", config); } try { const path = adminApiTokenFilePath(); - const directoryUnverified = assertSafeDirectory(dirname(path)); - let loaded: { token: string; aclUnverified: boolean }; + assertSafeDirectory(dirname(path)); + let token: string; try { - loaded = readExistingToken(path); + token = readExistingToken(path); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; - loaded = createTokenFile(path); + token = createTokenFile(path); } - const aclUnverified = directoryUnverified || loaded.aclUnverified === true; - lastManagementAuthAclUnverified = aclUnverified; - return ready(loaded.token, "file", config, { aclUnverified }); + return ready(token, "file", config); } catch (error) { - lastManagementAuthAclUnverified = false; return fail(error instanceof Error ? error.message : "management token initialization failed"); } } @@ -246,7 +206,11 @@ export function requireManagementAuth( config?: OcxConfig, ): Response | null { if (!state.available) { - return Response.json({ error: "management API unavailable" }, { status: 503 }); + return Response.json({ + error: "management API unavailable", + reason: state.reason, + hint: "Set OPENCODEX_ADMIN_AUTH_TOKEN to bypass file-backed admin token ACL hardening", + }, { status: 503 }); } const actual = req.headers.get("x-opencodex-api-key")?.trim() || req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim(); diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 4d83e9c23..9a05fbef9 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -55,7 +55,6 @@ import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from ". import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost"; import type { PersistedUsageAttempt } from "../../usage/log"; import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors"; -import { managementAuthAclUnverified } from "../management-auth"; import { applySystemEnvToggle } from "../system-env"; import { getCachedStartupHealth, invalidateStartupHealthCache } from "../startup-health-cache"; import { runWindowsTrayAction } from "../windows-tray-control"; @@ -122,7 +121,6 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise { } beforeEach(() => { - delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; testHome = mkdtempSync(join(tmpdir(), "ocx-management-auth-")); process.env.OPENCODEX_HOME = testHome; process.env.OPENCODEX_API_AUTH_TOKEN = "data-secret"; @@ -81,8 +79,6 @@ afterEach(() => { else process.env.OPENCODEX_API_AUTH_TOKEN = previousDataToken; if (previousAdminToken === undefined) delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; else process.env.OPENCODEX_ADMIN_AUTH_TOKEN = previousAdminToken; - if (previousAllowUnverifiedAcl === undefined) delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; - else process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = previousAllowUnverifiedAcl; if (testHome) rmSync(testHome, { recursive: true, force: true }); testHome = ""; }); @@ -209,7 +205,11 @@ describe("management and data-plane credential separation", () => { headers: { "x-opencodex-api-key": "ocx_admin_unhardened" }, }); expect(management.status).toBe(503); - expect(await management.json()).toEqual({ error: "management API unavailable" }); + const body = await management.json() as { error?: string; hint?: string; reason?: string }; + expect(body.error).toBe("management API unavailable"); + expect(body.hint).toContain("OPENCODEX_ADMIN_AUTH_TOKEN"); + expect(typeof body.reason).toBe("string"); + expect(body.reason!.length).toBeGreaterThan(0); } finally { await server.stop(true); } @@ -442,43 +442,8 @@ describe("management and data-plane credential separation", () => { } }); - test("OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL soft-fails timeouts and surfaces aclUnverified", async () => { + test("directory ACL timeout keeps management unavailable and names OPENCODEX_ADMIN_AUTH_TOKEN", async () => { delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; - process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = "1"; - saveConfig(remoteConfig()); - const adminToken = `ocx_admin_${"c".repeat(43)}`; - writeFileSync(join(testHome, "admin-api-token"), `${adminToken}\n`, { mode: 0o600 }); - process.env.USERNAME ??= "tester"; - setPlatformForTests("win32"); - setIcaclsRunnerForTests(args => { - const target = args[0] ?? ""; - if (target.endsWith("admin-api-token")) { - return { success: false, exitCode: null, timedOut: true, stdout: "" }; - } - return { success: true, exitCode: 0, timedOut: false, stdout: "" }; - }); - const state = initializeManagementAuthState(remoteConfig()); - expect(state.available).toBe(true); - if (!state.available) return; - expect(state.aclUnverified).toBe(true); - expect(managementAuthAclUnverified()).toBe(true); - - const server = startServer(0); - try { - const settings = await fetch(new URL("/api/settings", server.url), { - headers: { "x-opencodex-api-key": adminToken }, - }); - expect(settings.status).toBe(200); - const body = await settings.json() as { managementAuthAclUnverified?: boolean }; - expect(body.managementAuthAclUnverified).toBe(true); - } finally { - await server.stop(true); - } - }); - - test("directory ACL timeout without opt-in keeps management unavailable even when the token file hardens", async () => { - delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; - delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; saveConfig(remoteConfig()); const adminToken = `ocx_admin_${"d".repeat(43)}`; writeFileSync(join(testHome, "admin-api-token"), `${adminToken}\n`, { mode: 0o600 }); @@ -486,17 +451,16 @@ describe("management and data-plane credential separation", () => { setPlatformForTests("win32"); setIcaclsRunnerForTests(args => { const target = args[0] ?? ""; - // Directory harden times out; token-file harden succeeds. if (target.endsWith("admin-api-token")) { return { success: true, exitCode: 0, timedOut: false, stdout: "" }; } return { success: false, exitCode: null, timedOut: true, stdout: "" }; }); - // Drop any directory cache left by saveConfig so this case actually re-hardens. resetHardenedStateForTests(); const state = initializeManagementAuthState(remoteConfig()); expect(state.available).toBe(false); - expect(managementAuthAclUnverified()).toBe(false); + if (state.available) return; + expect(state.reason).toContain("OPENCODEX_ADMIN_AUTH_TOKEN"); const server = startServer(0); try { @@ -504,42 +468,70 @@ describe("management and data-plane credential separation", () => { headers: { "x-opencodex-api-key": adminToken }, }); expect(settings.status).toBe(503); + const body = await settings.json() as { error?: string; hint?: string; reason?: string }; + expect(body.error).toBe("management API unavailable"); + expect(body.hint).toContain("OPENCODEX_ADMIN_AUTH_TOKEN"); + expect(body.reason).toContain("OPENCODEX_ADMIN_AUTH_TOKEN"); expect((await fetch(new URL("/healthz", server.url))).status).toBe(200); } finally { await server.stop(true); } }); - test("directory ACL timeout with opt-in surfaces aclUnverified when the token file hardens", async () => { + test("required management harden retries after a soft loadConfig directory timeout", async () => { delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN; - process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = "1"; saveConfig(remoteConfig()); - const adminToken = `ocx_admin_${"e".repeat(43)}`; + const adminToken = `ocx_admin_${"f".repeat(43)}`; writeFileSync(join(testHome, "admin-api-token"), `${adminToken}\n`, { mode: 0o600 }); process.env.USERNAME ??= "tester"; setPlatformForTests("win32"); + + let softPhase = true; + let requiredPhaseCalls = 0; setIcaclsRunnerForTests(args => { const target = args[0] ?? ""; if (target.endsWith("admin-api-token")) { return { success: true, exitCode: 0, timedOut: false, stdout: "" }; } - return { success: false, exitCode: null, timedOut: true, stdout: "" }; + if (softPhase) { + return { success: false, exitCode: null, timedOut: true, stdout: "" }; + } + requiredPhaseCalls += 1; + return { success: true, exitCode: 0, timedOut: false, stdout: "" }; }); resetHardenedStateForTests(); + + const soft = hardenSecretDir(testHome, { required: false }); + expect(soft.ok).toBe(false); + expect(soft.diagnostics).toMatch(/timed out|budget exhausted|previous attempt/i); + + softPhase = false; const state = initializeManagementAuthState(remoteConfig()); expect(state.available).toBe(true); if (!state.available) return; - expect(state.aclUnverified).toBe(true); - expect(managementAuthAclUnverified()).toBe(true); + expect(state.source).toBe("file"); + expect(requiredPhaseCalls).toBeGreaterThan(0); + }); + + test("OPENCODEX_ADMIN_AUTH_TOKEN bypasses file-backed ACL hardening", async () => { + process.env.OPENCODEX_ADMIN_AUTH_TOKEN = "env-admin-secret"; + saveConfig(remoteConfig()); + process.env.USERNAME ??= "tester"; + setPlatformForTests("win32"); + setIcaclsRunnerForTests(() => ({ success: false, exitCode: null, timedOut: true, stdout: "" })); + resetHardenedStateForTests(); + + const state = initializeManagementAuthState(remoteConfig()); + expect(state.available).toBe(true); + if (!state.available) return; + expect(state.source).toBe("environment"); const server = startServer(0); try { - const settings = await fetch(new URL("/api/settings", server.url), { - headers: { "x-opencodex-api-key": adminToken }, + const management = await fetch(new URL("/api/config", server.url), { + headers: { "x-opencodex-api-key": "env-admin-secret" }, }); - expect(settings.status).toBe(200); - const body = await settings.json() as { managementAuthAclUnverified?: boolean }; - expect(body.managementAuthAclUnverified).toBe(true); + expect(management.status).toBe(200); } finally { await server.stop(true); } diff --git a/tests/service.test.ts b/tests/service.test.ts index 721a9bd61..e2005372a 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -501,17 +501,6 @@ describe("Windows service task", () => { expect(script).toContain('"%OCX_BUN%" "%OCX_CLI%" start --port'); expect(script).not.toContain('"C:\\Bun&Dir\\100%bun^\\bun.exe"'); }); - test("bakes OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL when install env opts in", () => { - const previous = process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; - try { - process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = "yes"; - const script = buildWindowsServiceScript({ bun: "C:\\OpenCodex\\bun.exe", cli: "C:\\OpenCodex\\cli.ts" }); - expect(script).toContain('set "OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL=yes"'); - } finally { - if (previous === undefined) delete process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL; - else process.env.OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL = previous; - } - }); test("switches the wrapper console to UTF-8 and sleeps via ping (timeout dies without console stdin)", () => { const script = buildWindowsServiceScript({ bun: "C:\\OpenCodex\\bun.exe", cli: "C:\\OpenCodex\\cli.ts" }); diff --git a/tests/windows-secret-acl.test.ts b/tests/windows-secret-acl.test.ts index 95871b44b..55c445474 100644 --- a/tests/windows-secret-acl.test.ts +++ b/tests/windows-secret-acl.test.ts @@ -548,6 +548,21 @@ describe("async hardenSecretPath (issue #612)", () => { expect(calls).toBe(0); // destination-keyed memo; not a parent-directory shortcut }); + test("optional timeout memo does not poison a later required harden of the same path", () => { + setIcaclsRunnerForTests(() => timeout); + const first = hardenSecretPath(secretFile(), { required: false }); + expect(first.ok).toBe(false); + + let calls = 0; + setIcaclsRunnerForTests(() => { + calls += 1; + return ok; + }); + const second = hardenSecretPath(secretFile(), { required: true }); + expect(second.ok).toBe(true); + expect(calls).toBeGreaterThan(0); + }); + test("async harden still grants owner before inheritance removal", async () => { const steps: string[] = []; setAsyncIcaclsRunnerForTests(async args => { diff --git a/tests/winsw.test.ts b/tests/winsw.test.ts index a3448bdb6..eb42df8ee 100644 --- a/tests/winsw.test.ts +++ b/tests/winsw.test.ts @@ -73,10 +73,6 @@ describe("winsw xml", () => { expect(xml).toContain(''); expect(xml).toContain(`${WINSW_SERVICE_ID}`); }); - test("carries OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL when install env opts in", () => { - const xml = buildWinswXml(entry, { ...env, OPENCODEX_ALLOW_UNVERIFIED_ADMIN_TOKEN_ACL: "true" }); - expect(xml).toContain(''); - }); test("honors OCX_BAKE_PORT when building WinSW arguments", () => { const xml = buildWinswXml(entry, { ...env, OCX_BAKE_PORT: "14444" }); expect(xml).toContain("start --port 14444"); From d2e9a8726dc0eeb67486e05aac493995dc99df19 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:42:33 +0200 Subject: [PATCH 6/8] fix(security): drop accidental service.ts tip merge from ACL pivot Keep the PR scoped to the timeout-memo + ADMIN_AUTH_TOKEN escape; restore service/winsw to the prior tip minus only the unverified-ACL bake. --- src/lib/winsw.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/winsw.ts b/src/lib/winsw.ts index 5222e1228..059aed522 100644 --- a/src/lib/winsw.ts +++ b/src/lib/winsw.ts @@ -172,12 +172,6 @@ function runWinsw(args: string[]): string { /** `install /p` prompts for the service-account password on the console — stdin must be inherited. */ function runWinswInteractive(args: string[]): void { - if (!process.stdin.isTTY) { - throw new Error( - "WinSW install requires an interactive console to prompt for the service account password. " - + "Run `ocx service install --native` from an elevated Command Prompt or PowerShell window, not a hidden or piped session.", - ); - } execFileSync(winswExePath(), args, { stdio: "inherit" }); } From 045fe42520dfd96264fbd4bc33fda00e4f81a54d Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:38:52 +0200 Subject: [PATCH 7/8] test(storage): budget early cleanup SQLite setup on Windows CI The oldest-first preview case timed out at Bun's 5s default under windows-latest load; siblings already use STORE_BUDGET_MS. --- tests/storage-cleanup.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/storage-cleanup.test.ts b/tests/storage-cleanup.test.ts index eecdf5e19..1403dc7a9 100644 --- a/tests/storage-cleanup.test.ts +++ b/tests/storage-cleanup.test.ts @@ -220,7 +220,7 @@ describe("previewArchivedCleanup", () => { "archived_sessions/rollout-new.jsonl", ]); expect(listed.some(c => c.relPath.includes("sessions/2026"))).toBe(false); - }); + }, { timeout: STORE_BUDGET_MS }); test("percent selects oldest subset and includes digest", () => { home = buildHome(); @@ -236,7 +236,7 @@ describe("previewArchivedCleanup", () => { expect(preview.bytes).toBe(preview.candidates[0]!.bytes); expect(preview.digest).toBe(computePreviewDigest(preview.candidates, 50)); expect(preview.digest).toMatch(/^[a-f0-9]{64}$/); - }); + }, { timeout: STORE_BUDGET_MS }); test("treats .jsonl and .jsonl.zst as one logical rollout", () => { home = buildHome(); @@ -250,7 +250,7 @@ describe("previewArchivedCleanup", () => { "archived_sessions/rollout-old.jsonl.zst", ]); expect(listed.filter(c => c.relPath.includes("rollout-old"))).toHaveLength(1); - }); + }, { timeout: STORE_BUDGET_MS }); }); describe("normalizeArchivedRolloutPath", () => { @@ -267,7 +267,7 @@ describe("normalizeArchivedRolloutPath", () => { // ISO timestamps in filenames must not be treated as Windows drive letters. expect(normalizeArchivedRolloutPath("archived_sessions/rollout-2026-01-01T10:00:00.jsonl", home)) .toBe("archived_sessions/rollout-2026-01-01T10:00:00.jsonl"); - }); + }, { timeout: STORE_BUDGET_MS }); }); describe("executeArchivedCleanup", () => { From 0538cc1889e4676a8b6486f5a069dfcf2b2bd754 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:39:13 +0200 Subject: [PATCH 8/8] fix(winsw): restore interactive install TTY guard after rebase The ACL pivot cleanup commit dropped tip's stdin.isTTY check; keep it. --- src/lib/winsw.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/lib/winsw.ts b/src/lib/winsw.ts index 059aed522..5222e1228 100644 --- a/src/lib/winsw.ts +++ b/src/lib/winsw.ts @@ -172,6 +172,12 @@ function runWinsw(args: string[]): string { /** `install /p` prompts for the service-account password on the console — stdin must be inherited. */ function runWinswInteractive(args: string[]): void { + if (!process.stdin.isTTY) { + throw new Error( + "WinSW install requires an interactive console to prompt for the service account password. " + + "Run `ocx service install --native` from an elevated Command Prompt or PowerShell window, not a hidden or piped session.", + ); + } execFileSync(winswExePath(), args, { stdio: "inherit" }); }