From 0a1aad19a9c94942eeca38d05ba8b40f9197258c Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 07:45:08 +0900 Subject: [PATCH 01/11] test(claude): wait for the tunnel decision instead of sleeping before settle --- tests/claude-integration/claude-intercept-proxy.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/claude-integration/claude-intercept-proxy.test.ts b/tests/claude-integration/claude-intercept-proxy.test.ts index 7fb32bf722b..7cde88079df 100644 --- a/tests/claude-integration/claude-intercept-proxy.test.ts +++ b/tests/claude-integration/claude-intercept-proxy.test.ts @@ -180,18 +180,22 @@ test("pending async choice holds pipelined bytes, then connects; rejection falls const echo = await startEchoUpstream(); cleanups.push(echo.close); let settle!: (choice: { kind: "intercept"; port: number }) => void; + // Resolved inside selectTunnel itself: the proxy has parsed CONNECT and is now holding the + // pipelined bytes. A fixed sleep only guessed at that moment and called an unset settle when + // the proxy had not consulted the choice yet. + const consulted = Promise.withResolvers(); let dialCount = 0; const proxy = await startConnectProxy(0, { interceptPort: 1, authToken: AUTH_TOKEN, selectTunnel: host => host === "claude.ai" - ? new Promise(resolve => { settle = resolve; }) + ? new Promise(resolve => { settle = resolve; consulted.resolve(); }) : Promise.reject(new Error("decision failed")), dialUpstream: () => { dialCount += 1; return connect({ host: "127.0.0.1", port: echo.port }); }, }); cleanups.push(proxy.close); const delayed = tunnelPayload(proxy.port, "claude.ai", "pending"); - await Bun.sleep(20); + await consulted.promise; expect(dialCount).toBe(0); settle({ kind: "intercept", port: echo.port }); expect(await delayed).toContain("echo:pending"); From 0e5795cd20ea7361db0700b3a16d89e72bd20f9b Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 07:45:33 +0900 Subject: [PATCH 02/11] test(codex): publish the prompt-lock child result by rename before the parent parses it --- tests/codex-integration/codex-prompt-lock.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/codex-integration/codex-prompt-lock.test.ts b/tests/codex-integration/codex-prompt-lock.test.ts index 7cc149660e5..7828a12fe12 100644 --- a/tests/codex-integration/codex-prompt-lock.test.ts +++ b/tests/codex-integration/codex-prompt-lock.test.ts @@ -249,7 +249,10 @@ describe("interleavings", () => { const fs = await import('node:fs'); const {tryAcquire, stillHeld} = await import(${JSON.stringify(modulePath)}); const result = tryAcquire(${JSON.stringify(path)}); - fs.writeFileSync(${JSON.stringify(path + ".b")}, JSON.stringify(result)); + // Publish by rename: the parent polls for this name and parses it at once, so the + // name must never be visible before the content is complete. + fs.writeFileSync(${JSON.stringify(path + ".b.tmp")}, JSON.stringify(result)); + fs.renameSync(${JSON.stringify(path + ".b.tmp")}, ${JSON.stringify(path + ".b")}); const until = Date.now() + 3000; while (!fs.existsSync(${JSON.stringify(stop)})) { if (Date.now() > until) throw Error('barrier timeout'); From 8ba37a74f48ae648144b44b0895426951487561b Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 07:46:44 +0900 Subject: [PATCH 03/11] test(codex): keep catalog audit cases off the cold Windows SID lookup and icacls --- .../codex-catalog-write-audit.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/codex-integration/codex-catalog-write-audit.test.ts b/tests/codex-integration/codex-catalog-write-audit.test.ts index f8f4cee61f5..e3b1a2a7a77 100644 --- a/tests/codex-integration/codex-catalog-write-audit.test.ts +++ b/tests/codex-integration/codex-catalog-write-audit.test.ts @@ -29,6 +29,7 @@ import { initializeConfigOwnership, CONFIG_UNINSTALL_MANIFEST } from "../../src/ import type { CatalogWriteAuditEvent } from "../../src/codex/catalog/write-audit-contract"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { redactUserPath } from "../../src/lib/redact"; +import { setSyntheticWindowsPrincipalForTests } from "../../src/lib/windows-user-principal"; let root = ""; let codexHome = ""; @@ -38,6 +39,7 @@ let previousOpencodexHome: string | undefined; const native = { slug: "gpt-5.5", description: "native" }; const routed = { slug: "ark/glm-5.3", description: "Routed via opencodex → ark/glm-5.3 (ark)." }; const catalogBytes = (...models: object[]) => `${JSON.stringify({ models }, null, 2)}\n`; +const aclSuccess = () => ({ success: true, exitCode: 0, timedOut: false, stdout: "" }); function auditPath(): string { return join(codexHome, CODEX_CATALOG_AUDIT_FILE); } const auditEvent = (): CatalogWriteAuditEvent => ({ @@ -78,6 +80,13 @@ function replaceAs(intent: CatalogWriteIntent, content: string) { } beforeEach(() => { + // A fresh audit file is hardened inside a 1s product deadline. On Windows that window held a + // cold PowerShell SID lookup plus real icacls, so a slow runner reported "skipped" where the + // audit contract says "created". These cases test the audit's publication rules; the ACL + // sequence has its own suite (tests/windows/windows-secret-acl.test.ts). + setSyntheticWindowsPrincipalForTests("*S-1-5-21-1-2-3-1001"); + secretAcl.setIcaclsRunnerForTests(aclSuccess); + secretAcl.setAsyncIcaclsRunnerForTests(async () => aclSuccess()); previousOpencodexHome = process.env.OPENCODEX_HOME; root = realpathSync.native(mkdtempSync(join(tmpdir(), "ocx-catalog-audit-"))); codexHome = join(root, "codex"); @@ -88,6 +97,9 @@ beforeEach(() => { }); afterEach(() => { + secretAcl.setIcaclsRunnerForTests(null); + secretAcl.setAsyncIcaclsRunnerForTests(null); + setSyntheticWindowsPrincipalForTests(null); if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousOpencodexHome; const path = resolveCodexCatalogSerializationDatabasePath(resolveEffectiveUserIdentity(), codexHome); From cbe9da5494c798c29fa5579ce490ef801fe67b8e Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 07:47:14 +0900 Subject: [PATCH 04/11] test(responses): keep native-main refresh cases off cold Windows ACL setup --- .../responses/responses-native-main-refresh.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/responses/responses-native-main-refresh.test.ts b/tests/responses/responses-native-main-refresh.test.ts index 64afbb92e74..1167e609896 100644 --- a/tests/responses/responses-native-main-refresh.test.ts +++ b/tests/responses/responses-native-main-refresh.test.ts @@ -24,6 +24,8 @@ import { captureCallerDirectAuth } from "../../src/providers/caller-authorizatio import { fakeChatGptJwt } from "../helpers/agent-task-recovery"; import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { setAsyncIcaclsRunnerForTests, setIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl"; +import { setSyntheticWindowsPrincipalForTests } from "../../src/lib/windows-user-principal"; /** Smallest complete Anthropic stream: the combo's second target has to actually answer. */ const ANTHROPIC_SSE = [ @@ -41,6 +43,7 @@ let previousOcxHome: string | undefined; let previousCodexHome: string | undefined; let releaseSpendHome: (() => void) | undefined; const OTHER_ACCOUNT_ID = "other"; +const aclSuccess = () => ({ success: true, exitCode: 0, timedOut: false, stdout: "" }); function config(options: { secondAccount?: boolean } = {}): OcxConfig { return { @@ -72,6 +75,12 @@ function request(path: "/v1/responses" | "/v1/responses/compact", signal?: Abort } beforeEach(() => { + // Every case starts a fresh home, so on Windows each one hardened a new spend journal and a + // rewritten auth.json through a PowerShell SID lookup and real icacls. That is ACL cost, not + // refresh/replay behavior, and it pushed these cases past their budget on cold runners. + setSyntheticWindowsPrincipalForTests("*S-1-5-21-1-2-3-1001"); + setIcaclsRunnerForTests(aclSuccess); + setAsyncIcaclsRunnerForTests(async () => aclSuccess()); home = mkdtempSync(join(tmpdir(), "ocx-responses-main-refresh-")); previousOcxHome = process.env.OPENCODEX_HOME; previousCodexHome = process.env.CODEX_HOME; @@ -112,6 +121,9 @@ afterEach(() => { if (previousCodexHome === undefined) delete process.env.CODEX_HOME; else process.env.CODEX_HOME = previousCodexHome; removeTreeWithRetry(home); + setIcaclsRunnerForTests(null); + setAsyncIcaclsRunnerForTests(null); + setSyntheticWindowsPrincipalForTests(null); }); function install401ThenRefreshHarness(): { sends: string[]; refreshes: string[] } { From a6564b76b901be01bd408846f71fbe8225f25725 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 07:48:34 +0900 Subject: [PATCH 05/11] test(cli): warm the routing-editor child graph before timing it --- tests/cli/cli-route-policy-write.test.ts | 42 +++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/tests/cli/cli-route-policy-write.test.ts b/tests/cli/cli-route-policy-write.test.ts index 8df7f104681..e215eb82c06 100644 --- a/tests/cli/cli-route-policy-write.test.ts +++ b/tests/cli/cli-route-policy-write.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; +import { afterEach, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -6,6 +6,7 @@ import { Readable } from "node:stream"; import { handleRoutePolicyWriteCommand } from "../../src/cli/route-policy-write"; import type { RuntimeApiDeps } from "../../src/cli/runtime-api"; import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; +import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmModuleGraph } from "../helpers/cold-spawn-warmup"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { repoPath, repoRoot } from "../helpers/repo-root"; import { spawnTestChild } from "../helpers/spawn-test-child"; @@ -191,8 +192,7 @@ describe("routing profile wire contract and output", () => { // A separate process contains Lab's process-global slots and all server imports. // Its empty homes keep automation disabled; injected transports never open a listener. -test("actual routing editor preserves stale/create/update/alias/delete authority", async () => { - const script = ` +const ROUTING_EDITOR_SCRIPT = ` import assert from "node:assert/strict"; import { writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; @@ -257,15 +257,27 @@ test("actual routing editor preserves stale/create/update/alias/delete authority originalLog(JSON.stringify({ scenarios: 9, saves, refreshes, requests: requests.length, automationEnabled: false })); } finally { resetLabActivationForTests(); } `; - const childHome = join(home, "real-handler"); - childCompleted = false; - const result = await spawnTestChild([process.execPath, "--eval", script], { cwd: repoRoot(), deadlineMs: 20_000, - env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, NODE_ENV: "test", HOME: childHome, - USERPROFILE: childHome, APPDATA: childHome, XDG_CONFIG_HOME: childHome, OPENCODEX_HOME: childHome, - CODEX_HOME: join(childHome, "codex"), NO_COLOR: "1" }, - }); - childCompleted = !result.timedOut && result.reaped && result.pipesClosed; - // Keep child diagnostics in the failure instead of leaving a live process after Bun times out. - expect(result).toMatchObject({ exitCode: 0, stderr: "", timedOut: false, reaped: true, pipesClosed: true }); - expect(JSON.parse(result.stdout.trim())).toEqual({ scenarios: 9, saves: 3, refreshes: 3, requests: 9, automationEnabled: false }); -}, SPAWN_BUDGET_MS); + +describe("routing editor child", () => { + // The child loads the management API, routing and Lab graphs. On a cold Windows runner that + // load alone used most of the 20s child deadline, so pay it once here and time a warm child. + beforeAll(async () => { + await warmModuleGraph({ graph: "cli/route-policy-write-editor", source: ROUTING_EDITOR_SCRIPT }); + }, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS); + + test("actual routing editor preserves stale/create/update/alias/delete authority", async () => { + const childHome = join(home, "real-handler"); + childCompleted = false; + const result = await spawnTestChild([process.execPath, "--eval", ROUTING_EDITOR_SCRIPT], { cwd: repoRoot(), deadlineMs: 20_000, + env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, NODE_ENV: "test", HOME: childHome, + USERPROFILE: childHome, APPDATA: childHome, XDG_CONFIG_HOME: childHome, OPENCODEX_HOME: childHome, + CODEX_HOME: join(childHome, "codex"), NO_COLOR: "1", + // The run-private cache the warm-up filled; without it this child transpiles cold again. + BUN_RUNTIME_TRANSPILER_CACHE_PATH: process.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH }, + }); + childCompleted = !result.timedOut && result.reaped && result.pipesClosed; + // Keep child diagnostics in the failure instead of leaving a live process after Bun times out. + expect(result).toMatchObject({ exitCode: 0, stderr: "", timedOut: false, reaped: true, pipesClosed: true }); + expect(JSON.parse(result.stdout.trim())).toEqual({ scenarios: 9, saves: 3, refreshes: 3, requests: 9, automationEnabled: false }); + }, SPAWN_BUDGET_MS); +}); From f942edf795c82bbea2a0e215721fed87c5c5cd0c Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 07:49:46 +0900 Subject: [PATCH 06/11] test(gui): warm the CLI start graph before timing standalone pairing readiness --- tests/gui/gui-pair-http.test.ts | 354 +++++++++++++++++--------------- 1 file changed, 183 insertions(+), 171 deletions(-) diff --git a/tests/gui/gui-pair-http.test.ts b/tests/gui/gui-pair-http.test.ts index 0af7c46d7a3..56791297653 100644 --- a/tests/gui/gui-pair-http.test.ts +++ b/tests/gui/gui-pair-http.test.ts @@ -1,9 +1,10 @@ -import { expect, test } from "bun:test"; +import { beforeAll, describe, expect, test } from "bun:test"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { findAvailablePort } from "../../src/server/ports"; import { watchdogMs } from "../helpers/ci-watchdog"; +import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmModuleGraph } from "../helpers/cold-spawn-warmup"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { repoPath } from "../helpers/repo-root"; import { randomBytes } from "node:crypto"; @@ -33,177 +34,188 @@ async function bootstrap(response: Response, origin: string) { return { token: token!, csrf: csrf! }; } -test("standalone CLI pairing crosses real HTTP admission once and stops before SSH", async () => { - const root = mkdtempSync(join(tmpdir(), "ocx-gui-pair-http-")); - const home = join(root, "home"), ocxHome = join(root, "ocx"); - const env: Record = { - HOME: home, USERPROFILE: home, OPENCODEX_HOME: ocxHome, CODEX_HOME: join(root, "codex"), - XDG_CONFIG_HOME: join(root, "xdg-config"), XDG_CACHE_HOME: join(root, "xdg-cache"), - XDG_DATA_HOME: join(root, "xdg-data"), XDG_STATE_HOME: join(root, "xdg-state"), - XDG_RUNTIME_DIR: join(root, "xdg-runtime"), APPDATA: join(home, "AppData", "Roaming"), - LOCALAPPDATA: join(home, "AppData", "Local"), TMPDIR: join(root, "tmp"), - TMP: join(root, "tmp"), TEMP: join(root, "tmp"), PATH: join(root, "empty-bin"), - OCX_OWNER_REGISTRY_DIR: join(root, "owner-registry"), NO_PROXY: "127.0.0.1,localhost", - OCX_TEST_HOME_GUARD: "1", OCX_DISABLE_UPDATE_CHECK: "1", OPENCODEX_KIRO_MODEL_DISCOVERY: "0", - CODEX_CI: "1", - }; - // Preserve only the guard's deny-list and Windows OS locator, never the parent environment. - if (process.env.OCX_REAL_HOME) env.OCX_REAL_HOME = process.env.OCX_REAL_HOME; - if (process.platform === "win32" && process.env.SystemRoot) env.SystemRoot = process.env.SystemRoot; - const children = new Set(); - const controller = new AbortController(); - const deadline = setTimeout(() => { - controller.abort(); - for (const child of children) if (child.exitCode === null) child.kill("SIGKILL"); - }, DEADLINE); - try { - for (const name of ["HOME", "OPENCODEX_HOME", "CODEX_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", - "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR", "APPDATA", "LOCALAPPDATA", "TMPDIR", - "PATH", "OCX_OWNER_REGISTRY_DIR"]) mkdirSync(env[name]!, { recursive: true, mode: 0o700 }); - const port = await findAvailablePort(0, "127.0.0.1"); - const origin = `http://127.0.0.1:${port}`; - writeFileSync(join(ocxHome, "config.json"), JSON.stringify({ - port, hostname: "127.0.0.1", runtimeRole: "standalone", providers: {}, defaultProvider: "openai", - apiKeys: [{ id: "paired-read-fixture", name: "Fixture", key: "ocx_data_paired_read_fixture", - createdAt: "2026-10-06T00:00:00.000Z" }], - codexAutoStart: false, syncResumeHistory: false, - clientIntegrations: { codex: false, grok: false, "claude-desktop": false }, - claudeCode: { enabled: false, systemEnv: false }, - })); - const server = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(port)], { - cwd: root, env, - stdin: "ignore", stdout: "ignore", stderr: "ignore", - }); - children.add(server); - const request = (path: string, init: RequestInit = {}) => fetch(`${origin}${path}`, { - ...init, redirect: "error", proxy: null, signal: controller.signal, - }); - // Health must identify our child, and the runtime publication must agree before CLI discovery. - let ready = false; - while (!controller.signal.aborted && server.exitCode === null) { - try { - const health = await request("/healthz"); - const identity = parseObject(await health.text()); - if (health.status === 200 && identity.service === "opencodex" - && identity.pid === server.pid && identity.port === port) { - const runtime = parseObject(readFileSync(join(ocxHome, "runtime-port.json"), "utf8")); - const readiness = await request("/readyz"); - await readiness.body?.cancel(); - ready = runtime.pid === server.pid && runtime.port === port && readiness.status === 200; - if (ready) break; - } - } catch { /* Connection refusal/publication absence is expected only during bounded startup. */ } - await Bun.sleep(25); - } - expect(ready, `owned CLI readiness (exit ${server.exitCode ?? "running"})`).toBe(true); - const ordinary = await bootstrap(await request("/opencodex-session", { headers: { Origin: origin } }), origin); - const headers = (session: typeof ordinary) => ({ - Origin: origin, "content-type": "application/json", "x-opencodex-gui-origin": origin, - "x-opencodex-api-key": session.token, "x-opencodex-csrf-token": session.csrf, - }); - const joinRequest = (sessionHeaders: Record) => request("/api/link/join", { - method: "POST", headers: sessionHeaders, body: JSON.stringify({ alias: "pair-http-unconfirmed" }), - }); - const expectError = async (response: Response, status: number, code: string) => { - expect(response.status, `expected ${code} HTTP status`).toBe(status); - const body = parseObject(await response.text()); - const error = body.error; - expect(!!error && typeof error === "object" && "code" in error && error.code === code, - `expected error code ${code} (body withheld)`).toBe(true); - }; - const expectStatus = async (session: typeof ordinary, paired: boolean) => { - const response = await request("/api/link/status", { headers: headers(session) }); - expect(response.status, "link status HTTP status").toBe(200); - const status = parseObject(await response.text()); - expect(status.joinAvailable === paired, "join availability matches session issuance").toBe(true); - expect(status.joinDenied === (paired ? null : "pairing_required"), "exact join denial").toBe(true); - expect(Array.isArray(status.links) && status.links.length === 0, "no durable links").toBe(true); +describe("standalone CLI pairing", () => { + // Both children run src/cli/index.ts, whose start path loads the whole server graph. On a cold + // Windows runner that load took most of the readiness watchdog; pay it here, not in the case. + beforeAll(async () => { + await warmModuleGraph({ graph: "cli-index/gui-pair-http", entry: repoPath("src/cli/index.ts") }); + }, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS); + + test("standalone CLI pairing crosses real HTTP admission once and stops before SSH", async () => { + const root = mkdtempSync(join(tmpdir(), "ocx-gui-pair-http-")); + const home = join(root, "home"), ocxHome = join(root, "ocx"); + const env: Record = { + HOME: home, USERPROFILE: home, OPENCODEX_HOME: ocxHome, CODEX_HOME: join(root, "codex"), + XDG_CONFIG_HOME: join(root, "xdg-config"), XDG_CACHE_HOME: join(root, "xdg-cache"), + XDG_DATA_HOME: join(root, "xdg-data"), XDG_STATE_HOME: join(root, "xdg-state"), + XDG_RUNTIME_DIR: join(root, "xdg-runtime"), APPDATA: join(home, "AppData", "Roaming"), + LOCALAPPDATA: join(home, "AppData", "Local"), TMPDIR: join(root, "tmp"), + TMP: join(root, "tmp"), TEMP: join(root, "tmp"), PATH: join(root, "empty-bin"), + OCX_OWNER_REGISTRY_DIR: join(root, "owner-registry"), NO_PROXY: "127.0.0.1,localhost", + OCX_TEST_HOME_GUARD: "1", OCX_DISABLE_UPDATE_CHECK: "1", OPENCODEX_KIRO_MODEL_DISCOVERY: "0", + CODEX_CI: "1", }; - await expectStatus(ordinary, false); - await expectError(await joinRequest(headers(ordinary)), 403, "forbidden"); - const reveal = (session: typeof ordinary) => request("/api/keys/reveal", { - method: "POST", headers: headers(session), body: JSON.stringify({ id: "paired-read-fixture" }), - }); - const ordinaryReveal = await reveal(ordinary); - expect(ordinaryReveal.status, "automatic session cannot read a stored key").toBe(403); - await ordinaryReveal.body?.cancel(); + // Preserve only the guard's deny-list, the Windows OS locator and the run-private transpiler + // cache, never the rest of the parent environment. + if (process.env.OCX_REAL_HOME) env.OCX_REAL_HOME = process.env.OCX_REAL_HOME; + if (process.platform === "win32" && process.env.SystemRoot) env.SystemRoot = process.env.SystemRoot; + // The warm-up filled that cache; without it both CLI children would transpile cold again. + if (process.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH) env.BUN_RUNTIME_TRANSPILER_CACHE_PATH = process.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH; + const children = new Set(); + const controller = new AbortController(); + const deadline = setTimeout(() => { + controller.abort(); + for (const child of children) if (child.exitCode === null) child.kill("SIGKILL"); + }, DEADLINE); + try { + for (const name of ["HOME", "OPENCODEX_HOME", "CODEX_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", + "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR", "APPDATA", "LOCALAPPDATA", "TMPDIR", + "PATH", "OCX_OWNER_REGISTRY_DIR"]) mkdirSync(env[name]!, { recursive: true, mode: 0o700 }); + const port = await findAvailablePort(0, "127.0.0.1"); + const origin = `http://127.0.0.1:${port}`; + writeFileSync(join(ocxHome, "config.json"), JSON.stringify({ + port, hostname: "127.0.0.1", runtimeRole: "standalone", providers: {}, defaultProvider: "openai", + apiKeys: [{ id: "paired-read-fixture", name: "Fixture", key: "ocx_data_paired_read_fixture", + createdAt: "2026-10-06T00:00:00.000Z" }], + codexAutoStart: false, syncResumeHistory: false, + clientIntegrations: { codex: false, grok: false, "claude-desktop": false }, + claudeCode: { enabled: false, systemEnv: false }, + })); + const server = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "start", "--port", String(port)], { + cwd: root, env, + stdin: "ignore", stdout: "ignore", stderr: "ignore", + }); + children.add(server); + const request = (path: string, init: RequestInit = {}) => fetch(`${origin}${path}`, { + ...init, redirect: "error", proxy: null, signal: controller.signal, + }); + // Health must identify our child, and the runtime publication must agree before CLI discovery. + let ready = false; + while (!controller.signal.aborted && server.exitCode === null) { + try { + const health = await request("/healthz"); + const identity = parseObject(await health.text()); + if (health.status === 200 && identity.service === "opencodex" + && identity.pid === server.pid && identity.port === port) { + const runtime = parseObject(readFileSync(join(ocxHome, "runtime-port.json"), "utf8")); + const readiness = await request("/readyz"); + await readiness.body?.cancel(); + ready = runtime.pid === server.pid && runtime.port === port && readiness.status === 200; + if (ready) break; + } + } catch { /* Connection refusal/publication absence is expected only during bounded startup. */ } + await Bun.sleep(25); + } + expect(ready, `owned CLI readiness (exit ${server.exitCode ?? "running"})`).toBe(true); + const ordinary = await bootstrap(await request("/opencodex-session", { headers: { Origin: origin } }), origin); + const headers = (session: typeof ordinary) => ({ + Origin: origin, "content-type": "application/json", "x-opencodex-gui-origin": origin, + "x-opencodex-api-key": session.token, "x-opencodex-csrf-token": session.csrf, + }); + const joinRequest = (sessionHeaders: Record) => request("/api/link/join", { + method: "POST", headers: sessionHeaders, body: JSON.stringify({ alias: "pair-http-unconfirmed" }), + }); + const expectError = async (response: Response, status: number, code: string) => { + expect(response.status, `expected ${code} HTTP status`).toBe(status); + const body = parseObject(await response.text()); + const error = body.error; + expect(!!error && typeof error === "object" && "code" in error && error.code === code, + `expected error code ${code} (body withheld)`).toBe(true); + }; + const expectStatus = async (session: typeof ordinary, paired: boolean) => { + const response = await request("/api/link/status", { headers: headers(session) }); + expect(response.status, "link status HTTP status").toBe(200); + const status = parseObject(await response.text()); + expect(status.joinAvailable === paired, "join availability matches session issuance").toBe(true); + expect(status.joinDenied === (paired ? null : "pairing_required"), "exact join denial").toBe(true); + expect(Array.isArray(status.links) && status.links.length === 0, "no durable links").toBe(true); + }; + await expectStatus(ordinary, false); + await expectError(await joinRequest(headers(ordinary)), 403, "forbidden"); + const reveal = (session: typeof ordinary) => request("/api/keys/reveal", { + method: "POST", headers: headers(session), body: JSON.stringify({ id: "paired-read-fixture" }), + }); + const ordinaryReveal = await reveal(ordinary); + expect(ordinaryReveal.status, "automatic session cannot read a stored key").toBe(403); + await ordinaryReveal.body?.cancel(); - // A process which reads runtime-state and reproduces its HMAC still lacks CLI write intent. - // No approved record is published here; no real user credential or external server is used. - const runtime = parseObject(readFileSync(join(ocxHome, "runtime-port.json"), "utf8")); - for (const fakeIntent of [undefined, "B".repeat(43)]) { - const nonce = randomBytes(32).toString("base64url"), expiresAt = Date.now() + 10_000; - const capability = createGuiPairCapability(String(runtime.attestationSecret), nonce, "POST", GUI_PAIR_PATH, - origin, server.pid, port, expiresAt)!; - const refused = await request(GUI_PAIR_PATH, { method: "POST", headers: { - "Content-Length": "0", [GUI_PAIR_EXPECTED_PID_HEADER]: String(server.pid), - [GUI_PAIR_NONCE_HEADER]: nonce, [GUI_PAIR_EXPIRES_AT_HEADER]: String(expiresAt), - [GUI_PAIR_BROWSER_ORIGIN_HEADER]: origin, [GUI_PAIR_CAPABILITY_HEADER]: capability, - ...(fakeIntent ? { [GUI_PAIR_INTENT_HEADER]: fakeIntent } : {}), - } }); - expect(refused.status, "runtime-only pairing refuses without minting").toBe(403); - const denied = parseObject(await refused.text()); - expect(denied.code).toBe("local_pairing_intent_required"); - } - await expectStatus(ordinary, false); + // A process which reads runtime-state and reproduces its HMAC still lacks CLI write intent. + // No approved record is published here; no real user credential or external server is used. + const runtime = parseObject(readFileSync(join(ocxHome, "runtime-port.json"), "utf8")); + for (const fakeIntent of [undefined, "B".repeat(43)]) { + const nonce = randomBytes(32).toString("base64url"), expiresAt = Date.now() + 10_000; + const capability = createGuiPairCapability(String(runtime.attestationSecret), nonce, "POST", GUI_PAIR_PATH, + origin, server.pid, port, expiresAt)!; + const refused = await request(GUI_PAIR_PATH, { method: "POST", headers: { + "Content-Length": "0", [GUI_PAIR_EXPECTED_PID_HEADER]: String(server.pid), + [GUI_PAIR_NONCE_HEADER]: nonce, [GUI_PAIR_EXPIRES_AT_HEADER]: String(expiresAt), + [GUI_PAIR_BROWSER_ORIGIN_HEADER]: origin, [GUI_PAIR_CAPABILITY_HEADER]: capability, + ...(fakeIntent ? { [GUI_PAIR_INTENT_HEADER]: fakeIntent } : {}), + } }); + expect(refused.status, "runtime-only pairing refuses without minting").toBe(403); + const denied = parseObject(await refused.text()); + expect(denied.code).toBe("local_pairing_intent_required"); + } + await expectStatus(ordinary, false); - const mint = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "gui", "pair", "--origin", origin, "--json"], { - cwd: root, env, stdin: "ignore", stdout: "pipe", stderr: "ignore", - }); - children.add(mint); - const [mintExit, output] = await Promise.all([mint.exited, new Response(mint.stdout).text()]); - expect(mintExit, "headless gui pair CLI succeeds (output withheld)").toBe(0); - const minted = parseObject(output); - expect(minted.kind === "created", "CLI retains its original grant response").toBe(true); - expect(minted.browserOrigin === origin && minted.serverOrigin === origin, "CLI exact origin binding").toBe(true); - const grant = String(minted.grant); - expect(/^ocx_pair_[A-Za-z0-9_-]{43}$/.test(grant), "one-use code returned only to the CLI with write intent").toBe(true); - expect(readdirSync(join(ocxHome, "gui-pair-intents")).length, "one-use commitment removed").toBe(0); - const redeem = (extraHeaders: Record = {}) => request("/opencodex-session", { - method: "POST", headers: { Origin: origin, "content-type": "application/json", ...extraHeaders }, - body: JSON.stringify({ grant }), - }); - // Refused attempts must not consume the valid grant. - for (const extra of [{ Origin: "https://foreign.example.test" }, { Authorization: "Bearer invalid-test-credential" }]) { - const refused = await redeem(extra); - expect(refused.status, "wrong origin/alternate credential redemption refused").toBe(401); - await refused.body?.cancel(); - } - const paired = await bootstrap(await redeem(), origin); - const pairedReveal = await reveal(paired); - expect(pairedReveal.status, "config-write-authorized session may read a stored key").toBe(200); - expect(pairedReveal.headers.get("cache-control")).toBe("no-store"); - expect(parseObject(await pairedReveal.text()).key === "ocx_data_paired_read_fixture", "exact fixture key returned").toBe(true); - const replay = await redeem(); - expect(replay.status, "single-use grant replay refused").toBe(401); - await replay.body?.cancel(); - await expectStatus(paired, true); - await expectStatus(ordinary, false); - for (const csrf of [undefined, "invalid-test-csrf"]) { - const withoutCsrf = new Headers(headers(paired)); - if (csrf === undefined) withoutCsrf.delete("x-opencodex-csrf-token"); - else withoutCsrf.set("x-opencodex-csrf-token", csrf); - const refused = await joinRequest(Object.fromEntries(withoutCsrf)); - expect(refused.status, "missing/wrong CSRF refused before join").toBe(401); - await refused.body?.cancel(); - } - // Fresh state has no confirmed host: production joinHome rejects BEFORE allocating a port or SSH. - await expectError(await joinRequest(headers(paired)), 409, "host_not_confirmed"); - await expectStatus(paired, true); - for (const file of ["links.json", "client-link.json", "child-initiated.json", "known_hosts"]) { - expect(existsSync(join(ocxHome, "link", file)), `no durable ${file} after refused joins`).toBe(false); + const mint = Bun.spawn([process.execPath, repoPath("src/cli/index.ts"), "gui", "pair", "--origin", origin, "--json"], { + cwd: root, env, stdin: "ignore", stdout: "pipe", stderr: "ignore", + }); + children.add(mint); + const [mintExit, output] = await Promise.all([mint.exited, new Response(mint.stdout).text()]); + expect(mintExit, "headless gui pair CLI succeeds (output withheld)").toBe(0); + const minted = parseObject(output); + expect(minted.kind === "created", "CLI retains its original grant response").toBe(true); + expect(minted.browserOrigin === origin && minted.serverOrigin === origin, "CLI exact origin binding").toBe(true); + const grant = String(minted.grant); + expect(/^ocx_pair_[A-Za-z0-9_-]{43}$/.test(grant), "one-use code returned only to the CLI with write intent").toBe(true); + expect(readdirSync(join(ocxHome, "gui-pair-intents")).length, "one-use commitment removed").toBe(0); + const redeem = (extraHeaders: Record = {}) => request("/opencodex-session", { + method: "POST", headers: { Origin: origin, "content-type": "application/json", ...extraHeaders }, + body: JSON.stringify({ grant }), + }); + // Refused attempts must not consume the valid grant. + for (const extra of [{ Origin: "https://foreign.example.test" }, { Authorization: "Bearer invalid-test-credential" }]) { + const refused = await redeem(extra); + expect(refused.status, "wrong origin/alternate credential redemption refused").toBe(401); + await refused.body?.cancel(); + } + const paired = await bootstrap(await redeem(), origin); + const pairedReveal = await reveal(paired); + expect(pairedReveal.status, "config-write-authorized session may read a stored key").toBe(200); + expect(pairedReveal.headers.get("cache-control")).toBe("no-store"); + expect(parseObject(await pairedReveal.text()).key === "ocx_data_paired_read_fixture", "exact fixture key returned").toBe(true); + const replay = await redeem(); + expect(replay.status, "single-use grant replay refused").toBe(401); + await replay.body?.cancel(); + await expectStatus(paired, true); + await expectStatus(ordinary, false); + for (const csrf of [undefined, "invalid-test-csrf"]) { + const withoutCsrf = new Headers(headers(paired)); + if (csrf === undefined) withoutCsrf.delete("x-opencodex-csrf-token"); + else withoutCsrf.set("x-opencodex-csrf-token", csrf); + const refused = await joinRequest(Object.fromEntries(withoutCsrf)); + expect(refused.status, "missing/wrong CSRF refused before join").toBe(401); + await refused.body?.cancel(); + } + // Fresh state has no confirmed host: production joinHome rejects BEFORE allocating a port or SSH. + await expectError(await joinRequest(headers(paired)), 409, "host_not_confirmed"); + await expectStatus(paired, true); + for (const file of ["links.json", "client-link.json", "child-initiated.json", "known_hosts"]) { + expect(existsSync(join(ocxHome, "link", file)), `no durable ${file} after refused joins`).toBe(false); + } + expect(server.exitCode, "join denial must not restart the server").toBeNull(); + } finally { + controller.abort(); + clearTimeout(deadline); + const force = setTimeout(() => { + for (const child of children) if (child.exitCode === null) child.kill("SIGKILL"); + }, 2_000); + try { + for (const child of children) if (child.exitCode === null) child.kill(); + await Promise.all([...children].map(child => child.exited)); + } finally { clearTimeout(force); } + removeTreeWithRetry(root); } - expect(server.exitCode, "join denial must not restart the server").toBeNull(); - } finally { - controller.abort(); - clearTimeout(deadline); - const force = setTimeout(() => { - for (const child of children) if (child.exitCode === null) child.kill("SIGKILL"); - }, 2_000); - try { - for (const child of children) if (child.exitCode === null) child.kill(); - await Promise.all([...children].map(child => child.exited)); - } finally { clearTimeout(force); } - removeTreeWithRetry(root); - } -}, DEADLINE + 10_000); + }, DEADLINE + 10_000); +}); From 50ef1c9d03557ef64323d313038c075cc9f6fc40 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 07:50:21 +0900 Subject: [PATCH 07/11] fix(codex): keep the PowerShell DPAPI helper out of cmdlet module discovery --- .../windows-key-protection.ts | 24 ++++++++++++++----- structure/clients/codex-desktop.md | 8 ++++--- .../desktop-compatibility-authority.test.ts | 9 ++++++- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/codex/desktop-compatibility/windows-key-protection.ts b/src/codex/desktop-compatibility/windows-key-protection.ts index fc1a11b54f6..a4df04267c3 100644 --- a/src/codex/desktop-compatibility/windows-key-protection.ts +++ b/src/codex/desktop-compatibility/windows-key-protection.ts @@ -10,17 +10,24 @@ const ENTROPY = new TextEncoder().encode(PURPOSE); // Only the PowerShell fallback is bounded by a clock. A cold Windows PowerShell that must also load // System.Security has been measured past 10s on a loaded machine (Windows CI run 38591626343). const DPAPI_TIMEOUT_MS = 20_000; +// Cmdlet-free on purpose. The first cmdlet a PowerShell session runs (Add-Type, ConvertFrom-Json) +// sends it through command discovery and module auto-loading, which without a module-analysis cache +// for the profile scans the modules on PSModulePath. The test runner gives every run a fresh profile +// and CI images carry large module sets, so that scan, not DPAPI, is what cold runs waited on. +// Assembly loading, string splitting and base64 are plain .NET calls outside command discovery. +// The request is "\n" on stdin. const SCRIPT = [ "$ErrorActionPreference='Stop'", - "Add-Type -AssemblyName System.Security", - "$r=[Console]::In.ReadToEnd()|ConvertFrom-Json", - "$data=[Convert]::FromBase64String($r.data)", + "[void][Reflection.Assembly]::Load('System.Security, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')", + "$r=[Console]::In.ReadToEnd().Split([char]10)", + "if($r.Length -ne 2){throw 'Invalid request'}", + "$data=[Convert]::FromBase64String($r[1])", `if($data.Length -eq 0 -or $data.Length -gt ${MAX_INPUT}){throw 'Invalid data size'}`, `$entropy=[Text.Encoding]::UTF8.GetBytes('${PURPOSE}')`, "$scope=[Security.Cryptography.DataProtectionScope]::CurrentUser", - "if($r.operation -eq 'protect'){", + "if($r[0] -ceq 'protect'){", " $out=[Security.Cryptography.ProtectedData]::Protect($data,$entropy,$scope)", - "}elseif($r.operation -eq 'unprotect'){", + "}elseif($r[0] -ceq 'unprotect'){", " $out=[Security.Cryptography.ProtectedData]::Unprotect($data,$entropy,$scope)", "}else{throw 'Invalid operation'}", "[Console]::Out.Write([Convert]::ToBase64String($out))", @@ -126,7 +133,7 @@ function runPowerShellDpapi(operation: Operation, data: Uint8Array, timeoutMs = resolve(bytes); }); child.stdin?.on("error", () => { /* execFile's completion reports the failed child. */ }); - child.stdin?.end(JSON.stringify({ operation, data: Buffer.from(data).toString("base64") })); + child.stdin?.end(`${operation}\n${Buffer.from(data).toString("base64")}`); }); } @@ -163,3 +170,8 @@ export function powerShellAuthorityKeyProtectionForTests(timeoutMs: number): Aut export function usesNativeDpapiForTests(): boolean { return process.platform === "win32" && loadNativeDpapi() !== null; } + +/** Test-only: the PowerShell helper's script, so its cmdlet-free contract can be checked anywhere. */ +export function powerShellDpapiScriptForTests(): string { + return SCRIPT; +} diff --git a/structure/clients/codex-desktop.md b/structure/clients/codex-desktop.md index 06772631f73..b78c427be7c 100644 --- a/structure/clients/codex-desktop.md +++ b/structure/clients/codex-desktop.md @@ -69,12 +69,14 @@ registration refuse it, while exact trust removal and deliberate renewal remain `CryptProtectData`/`CryptUnprotectData` in-process on Windows x64, loading `crypt32.dll` from the `GetSystemDirectoryW` path and clearing unprotected output before `LocalFree`. Windows ARM64, where Bun FFI is unavailable, keeps the trusted PowerShell helper with -bounded stdin/stdout. Both write the same blob format; neither puts secrets on a command +bounded stdin/stdout. The helper uses only .NET calls and never a cmdlet, because the first +cmdlet in a session pays module discovery, which a cold profile turns into the whole budget. +Both write the same blob format; neither puts secrets on a command line or falls back to plaintext. CurrentUser protects against other OS identities, not another process using the same credentials. `tests/clients/desktop-compatibility-authority.test.ts` covers persistence, refusal, -a real Windows DPAPI round trip with synthetic data, and a PowerShell-written key opening -in-process. +a real Windows DPAPI round trip with synthetic data, a PowerShell-written key opening +in-process, and the helper's cmdlet-free script on every platform. ## Certificate setup API diff --git a/tests/clients/desktop-compatibility-authority.test.ts b/tests/clients/desktop-compatibility-authority.test.ts index ea8582a6451..a5d3e5054f3 100644 --- a/tests/clients/desktop-compatibility-authority.test.ts +++ b/tests/clients/desktop-compatibility-authority.test.ts @@ -6,7 +6,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { ensureDesktopCompatibilityAuthority, inspectDesktopCompatibilityAuthority, loadDesktopCompatibilityAuthority, renewDesktopCompatibilityAuthority } from "../../src/codex/desktop-compatibility/certificate-store"; import { - powerShellAuthorityKeyProtectionForTests, usesNativeDpapiForTests, windowsAuthorityKeyProtection, type AuthorityKeyProtection, + powerShellAuthorityKeyProtectionForTests, powerShellDpapiScriptForTests, usesNativeDpapiForTests, + windowsAuthorityKeyProtection, type AuthorityKeyProtection, } from "../../src/codex/desktop-compatibility/windows-key-protection"; import { setAsyncIcaclsRunnerForTests, setIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl"; @@ -165,6 +166,12 @@ describe("Codex Desktop compatibility authority lifecycle", () => { expect(readFileSync(path, "utf8")).toBe("broken state"); }); + test("the PowerShell DPAPI helper never invokes a cmdlet", () => { + // A cmdlet (Verb-Noun) triggers command discovery and module auto-loading, which a cold + // profile pays for before DPAPI runs at all. The helper must stay on .NET calls. + expect(powerShellDpapiScriptForTests().match(/\b[A-Z][A-Za-z]+-[A-Z][A-Za-z]+\b/g)).toBeNull(); + }); + test.skipIf(process.platform !== "win32")("real CurrentUser DPAPI survives separate helper invocations and rejects corruption", async () => { // x64 protects in-process; spawning PowerShell per call is what exceeded its bound on CI. expect(usesNativeDpapiForTests()).toBe(process.arch === "x64"); From 857a6c3e8f20c51cf9853c95f9ca3657f6ab58d2 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 11:21:33 +0900 Subject: [PATCH 08/11] fix(cli): identify update-restart homes by exact file id, not a rounded double --- src/cli/update-restart-child.ts | 7 +++--- src/cli/update-restart-home.ts | 28 ++++++++++++++++------- structure/ops/service-and-sidecars.md | 2 +- tests/cli/cli-update-restart-home.test.ts | 16 +++++++++++++ 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/cli/update-restart-child.ts b/src/cli/update-restart-child.ts index 24198a3e9f9..e2fcc37bc76 100644 --- a/src/cli/update-restart-child.ts +++ b/src/cli/update-restart-child.ts @@ -20,10 +20,11 @@ function parseMarker(raw: string): UpdateRestartChildMarker { || typeof value.home.serviceRecord.digest !== "string" || !/^[a-f0-9]{64}$/.test(value.home.serviceRecord.digest)) throw new Error("update_restart_child_marker_invalid"); for (const directory of [value.home.config, value.home.codex]) { // Windows reports 64-bit NTFS file ids (MFT sequence in the high 16 bits) as doubles above - // 2^53. Parent and child read them through the same stat API, so any finite integer is a - // faithful identity token; assertUpdateRestartHome still compares path, dev and ino exactly. + // 2^53, where neighbouring ids collapse. dev and ino may be large finite integers; fileId + // carries the exact "dev:ino" that assertUpdateRestartHome actually relies on. if (!directory || typeof directory.path !== "string" || !directory.path - || !Number.isInteger(directory.dev) || !Number.isInteger(directory.ino)) { + || !Number.isInteger(directory.dev) || !Number.isInteger(directory.ino) + || typeof directory.fileId !== "string" || !/^\d{1,20}:\d{1,20}$/.test(directory.fileId)) { throw new Error("update_restart_child_marker_invalid"); } } diff --git a/src/cli/update-restart-home.ts b/src/cli/update-restart-home.ts index bf387b73fa9..842f2cbcb26 100644 --- a/src/cli/update-restart-home.ts +++ b/src/cli/update-restart-home.ts @@ -1,4 +1,4 @@ -import { realpathSync, statSync } from "node:fs"; +import { realpathSync, statSync, type BigIntStats } from "node:fs"; import { readClientConnectionState } from "../client/state"; import { readConfigDiagnostics } from "../config/diagnostics"; import { getConfigDir } from "../config/paths"; @@ -6,23 +6,34 @@ import { currentCodexHome } from "../service/state"; import { captureUpdateRestartServiceRecord, type UpdateRestartServiceRecord, type UpdateRestartServiceRecordDeps } from "./update-restart-service-record"; import { probeUpdateRestartSupervision, type UpdateRestartSupervisionDeps } from "./update-restart-supervision"; +/** + * `fileId` is the exact identity ("dev:ino" from a bigint stat). `dev` and `ino` are doubles and + * cannot be: an NTFS file id carries the MFT sequence in its high bits, so above 2^53 a replacement + * directory with a nearby MFT index can round to the same `ino` as the one it replaced. + */ +export interface UpdateRestartDirectory { path: string; dev: number; ino: number; fileId: string } + export interface UpdateRestartHome { - config: { path: string; dev: number; ino: number }; - codex: { path: string; dev: number; ino: number }; + config: UpdateRestartDirectory; + codex: UpdateRestartDirectory; revision: number; serviceRecord: UpdateRestartServiceRecord; } -function physicalDirectory(path: string): UpdateRestartHome["config"] { +export type UpdateRestartDirectoryStat = Pick; + +function physicalDirectory(path: string, stat: (path: string) => UpdateRestartDirectoryStat): UpdateRestartDirectory { const canonical = realpathSync.native(path); - const stat = statSync(canonical); - if (!stat.isDirectory()) throw new Error("update_restart_home_unverified"); - return { path: canonical, dev: stat.dev, ino: stat.ino }; + const observed = stat(canonical); + if (!observed.isDirectory()) throw new Error("update_restart_home_unverified"); + return { path: canonical, dev: Number(observed.dev), ino: Number(observed.ino), fileId: `${observed.dev}:${observed.ino}` }; } export interface UpdateRestartHomeDeps { record?: UpdateRestartServiceRecordDeps; supervision?: UpdateRestartSupervisionDeps; + /** Test seam for the directory identity source; production reads a bigint stat. */ + statDirectory?: (path: string) => UpdateRestartDirectoryStat; } /** Capture eligibility from the same bytes that supply the frozen fingerprint. */ @@ -32,7 +43,8 @@ export function readUpdateRestartHome(deps: UpdateRestartHomeDeps = {}): UpdateR if (owner.kind !== "none" || (state.kind === "state" && platform !== "darwin" && platform !== "linux")) { throw new Error("update_restart_owner_unverified"); } - return { config: physicalDirectory(getConfigDir()), codex: physicalDirectory(currentCodexHome()), revision: owner.revision, serviceRecord }; + const stat = deps.statDirectory ?? (path => statSync(path, { bigint: true })); + return { config: physicalDirectory(getConfigDir(), stat), codex: physicalDirectory(currentCodexHome(), stat), revision: owner.revision, serviceRecord }; } export function assertUpdateRestartHome(expected: UpdateRestartHome, deadlineAt = Date.now() + 2000, deps: UpdateRestartHomeDeps = {}): void { diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index 1e91f9956ac..7182595e80a 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -58,7 +58,7 @@ without running handlers is not covered by this forwarding mechanism. systemd unit. State and ownership resolve from those captured bytes. Each present regular file contributes SHA-256 content and bigint device, inode, size, nanosecond mtime/ctime, mode, uid and gid. The opened descriptor must be regular and match device/inode before any read. Each record is limited to 1 MiB, read in bounded chunks with growth beyond that limit refused. Descriptor stats bracket the read; a final pathname stat must agree. Symlinks, inconsistent reads, malformed state, unreadable candidates and membership drift -refuse. Canonical directory aliases remain equivalent. Lock, PID and runtime records are excluded. +refuse. Homes compare canonical path plus exact bigint `dev:ino` (`fileId`); rounded doubles merge NTFS ids above 2^53. Aliases stay equivalent. Lock, PID and runtime records are excluded. `src/cli/update-restart.ts` requires known versions and a detached POSIX target with PID-1 parent, no ownership claim and no client/sibling role. An installed record without a claim diff --git a/tests/cli/cli-update-restart-home.test.ts b/tests/cli/cli-update-restart-home.test.ts index f8cc487f5cd..d3485d522c4 100644 --- a/tests/cli/cli-update-restart-home.test.ts +++ b/tests/cli/cli-update-restart-home.test.ts @@ -28,6 +28,22 @@ test("physical home identity detects same-path directory replacement", () => { renameSync(h.config, h.config + "-old"); mkdirSync(h.config); expect(() => checkHome(captured)).toThrow(); }); +test("file ids beyond double precision still distinguish a replaced directory", () => { + // NTFS ids carry the MFT sequence in the high bits; a replacement directory often gets the + // neighbouring MFT index, and above 2^53 a double cannot tell the two ids apart. + home(); + const original = 2n ** 58n + 4096n; + expect(Number(original + 1n)).toBe(Number(original)); + let ino = original; + const deps = { + statDirectory: () => ({ dev: 7n, ino, isDirectory: () => true }), + supervision: { stat: () => ({ isFile: () => true, mode: 0o100755 }), platform: "darwin" as const, run: () => ({ status: 113, stdout: "", stderr: "" }) }, + }; + const captured = readUpdateRestartHome(deps); + expect(() => assertUpdateRestartHome(captured, Date.now() + 5000, deps)).not.toThrow(); + ino = original + 1n; + expect(() => assertUpdateRestartHome(captured, Date.now() + 5000, deps)).toThrow("update_restart_home_changed"); +}); test("canonical aliases identify the same directory", () => { const h = home(); const captured = readUpdateRestartHome(); const alias = join(h.root, "alias"); symlinkSync(h.config, alias, process.platform === "win32" ? "junction" : "dir"); From 7cafc0ae437839db96c84546d03c0fce88671bb2 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 11:22:15 +0900 Subject: [PATCH 09/11] test(clients): warm the real Windows activation validation before bounding it --- .../desktop-compatibility-launch.test.ts | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/clients/desktop-compatibility-launch.test.ts b/tests/clients/desktop-compatibility-launch.test.ts index 769b2971852..015a31e4740 100644 --- a/tests/clients/desktop-compatibility-launch.test.ts +++ b/tests/clients/desktop-compatibility-launch.test.ts @@ -1,5 +1,6 @@ -import { afterEach, describe, expect, test } from "bun:test"; +import { afterEach, beforeAll, describe, expect, test } from "bun:test"; import { execFileSync } from "node:child_process"; +import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmColdSpawn } from "../helpers/cold-spawn-warmup"; import { compatibilityActivationScript, launchWindowsCodexCompatibility, validatedCompatibilityPacUrl } from "../../src/codex/desktop-compatibility/windows-package-launch"; import { WINDOWS_ACTIVATION_SOURCE } from "../../src/codex/desktop-compatibility/windows-activation-source"; import { setTrustedWindowsElevationExecutablesForTests } from "../../src/lib/windows-elevation"; @@ -147,15 +148,25 @@ describe("Codex Desktop compatibility package launch", () => { expect(launchWindowsCodexCompatibility(pac, { platform: "linux" })).toEqual({ status: "refused", reason: "unsupported_platform" }); expect(launchWindowsCodexCompatibility(pac, { platform: "win32" })).toEqual({ status: "refused", reason: "test_environment" }); }); +}); + +describe.skipIf(process.platform !== "win32")("Codex Desktop compatibility activation on real Windows", () => { + const encoded = Buffer.from(compatibilityActivationScript(install, pac), "utf8").toString("base64"); + const validation = ["$ErrorActionPreference='Stop'", + `$null=[scriptblock]::Create([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')))`, + "Add-Type -TypeDefinition @'", WINDOWS_ACTIVATION_SOURCE, "'@", + "[OpenCodexPackageActivation]::ValidateActivationService()", "'validated-without-launch'"].join("\n"); + const validate = (timeout: number) => execFileSync(powershell, ["-NoProfile", "-NonInteractive", "-Command", validation], + { encoding: "utf8", timeout, windowsHide: true }); + + // The first run in a fresh test profile pays PowerShell start, Add-Type's module discovery, + // the C# compiler and COM activation, which reached the 10s bound on cold runners. Replaying + // the same call once here leaves the bound below measuring a warm validation. + beforeAll(async () => { + await warmColdSpawn("powershell/desktop-activation-validation", deadlineMs => { validate(deadlineMs); }); + }, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS); - test.skipIf(process.platform !== "win32")("the real Windows parser and COM service accept the adapter without launching an app", () => { - const script = compatibilityActivationScript(install, pac); - const encoded = Buffer.from(script, "utf8").toString("base64"); - const validation = ["$ErrorActionPreference='Stop'", - `$null=[scriptblock]::Create([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')))`, - "Add-Type -TypeDefinition @'", WINDOWS_ACTIVATION_SOURCE, "'@", - "[OpenCodexPackageActivation]::ValidateActivationService()", "'validated-without-launch'"].join("\n"); - const output = execFileSync(powershell, ["-NoProfile", "-NonInteractive", "-Command", validation], { encoding: "utf8", timeout: 10_000, windowsHide: true }); - expect(output.trim()).toBe("validated-without-launch"); + test("the real Windows parser and COM service accept the adapter without launching an app", () => { + expect(validate(10_000).trim()).toBe("validated-without-launch"); }, 15_000); }); From 1adac49ad4116059b9291997eeaabe9251c82909 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 11:24:19 +0900 Subject: [PATCH 10/11] test(server): keep Anthropic billing cases off cold Windows ACL work inside the request signal --- tests/server/server-anthropic-billing.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/server/server-anthropic-billing.test.ts b/tests/server/server-anthropic-billing.test.ts index cf0abb9f243..23d9ec82ec2 100644 --- a/tests/server/server-anthropic-billing.test.ts +++ b/tests/server/server-anthropic-billing.test.ts @@ -12,6 +12,8 @@ import { createRequestExecutionBudget, CODEX_TEXT_GUARDED_BUDGET_POLICY } from " import type { OcxConfig } from "../../src/types"; import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { setAsyncIcaclsRunnerForTests, setIcaclsRunnerForTests } from "../../src/lib/windows-secret-acl"; +import { setSyntheticWindowsPrincipalForTests } from "../../src/lib/windows-user-principal"; const creditMessage = "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits."; const refusal = (status = 400, message = creditMessage, type = "invalid_request_error") => Response.json({ type: "error", error: { type, message } }, { status }); @@ -34,8 +36,15 @@ let home = ""; let releaseSpend: (() => void) | undefined; let upstream: ReturnType | undefined; let seen: { key: string | null; authorization: string | null; apiKey: string | null; body: unknown }[]; +const aclSuccess = () => ({ success: true, exitCode: 0, timedOut: false, stdout: "" }); beforeEach(() => { + // Each case's fresh home hardens its spend journal on first write, which lands inside the + // request's 2s signal. On Windows that was a PowerShell SID lookup plus real icacls, and a cold + // runner spent the signal on it: the handler then saw the caller abort and answered 499. + setSyntheticWindowsPrincipalForTests("*S-1-5-21-1-2-3-1001"); + setIcaclsRunnerForTests(aclSuccess); + setAsyncIcaclsRunnerForTests(async () => aclSuccess()); previousHome = process.env.OPENCODEX_HOME; home = mkdtempSync(join(tmpdir(), "ocx-billing-fixture-")); process.env.OPENCODEX_HOME = home; @@ -54,6 +63,9 @@ afterEach(async () => { if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; removeTreeWithRetry(home); + setIcaclsRunnerForTests(null); + setAsyncIcaclsRunnerForTests(null); + setSyntheticWindowsPrincipalForTests(null); }); function fixture(reply: (index: number, stream: boolean) => Response, keys = 2): OcxConfig { From d7427e7522ece216aa9a1d7f7be4b81810d82f2e Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 22 Oct 2026 11:31:08 +0900 Subject: [PATCH 11/11] test(cli): give update-restart marker fixtures the exact fileId --- tests/cli/cli-update-restart-child.test.ts | 2 +- tests/cli/cli-update-restart.test.ts | 4 ++-- tests/cli/system-restart-client.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/cli/cli-update-restart-child.test.ts b/tests/cli/cli-update-restart-child.test.ts index cbc2448e369..101fa61b28e 100644 --- a/tests/cli/cli-update-restart-child.test.ts +++ b/tests/cli/cli-update-restart-child.test.ts @@ -6,7 +6,7 @@ function setup() { const calls: string[] = []; let now = 1000; let exit: (() => void) | undefined; - const marker = { home: { config: { path: "/test/ocx", dev: 1, ino: 2 }, codex: { path: "/test/codex", dev: 1, ino: 3 }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }, version: "2.77.0", port: 10100, hostname: "127.0.0.1", deadlineAt: 5000 }; + const marker = { home: { config: { path: "/test/ocx", dev: 1, ino: 2, fileId: "1:2" }, codex: { path: "/test/codex", dev: 1, ino: 3, fileId: "1:3" }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }, version: "2.77.0", port: 10100, hostname: "127.0.0.1", deadlineAt: 5000 }; const env = { [UPDATE_RESTART_CHILD_ENV]: JSON.stringify(marker), [OWNERSHIP_MUTATION_LEASE_TOKEN_ENV]: "parent-token" }; const io: UpdateRestartChildIo = { checkState: () => {}, diff --git a/tests/cli/cli-update-restart.test.ts b/tests/cli/cli-update-restart.test.ts index 94954db1744..eee339f1288 100644 --- a/tests/cli/cli-update-restart.test.ts +++ b/tests/cli/cli-update-restart.test.ts @@ -6,7 +6,7 @@ import { updateRestartVeto } from "../../src/update/restart-ownership"; import type { LiveProxy } from "../../src/server/proxy-liveness"; const candidate: UpdateRestartCandidate = { - home: { config: { path: "/test/ocx", dev: 1, ino: 2 }, codex: { path: "/test/codex", dev: 1, ino: 3 }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }, + home: { config: { path: "/test/ocx", dev: 1, ino: 2, fileId: "1:2" }, codex: { path: "/test/codex", dev: 1, ino: 3, fileId: "1:3" }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }, target: { pid: 123, port: 10100, hostname: "127.0.0.1", source: "runtime", version: "2.76.0" }, runtime: { pid: 123, port: 10100, hostname: "127.0.0.1", attestationSecret: "a".repeat(43) }, cliVersion: "2.77.0", @@ -14,7 +14,7 @@ const candidate: UpdateRestartCandidate = { function setup() { const calls: string[] = []; let now = 1000; - const home = { config: { path: "/test/ocx", dev: 1, ino: 2 }, codex: { path: "/test/codex", dev: 1, ino: 3 }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }; + const home = { config: { path: "/test/ocx", dev: 1, ino: 2, fileId: "1:2" }, codex: { path: "/test/codex", dev: 1, ino: 3, fileId: "1:3" }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }; const live: LiveProxy = { ...candidate.target, pid: 456, version: "2.77.0" }; const io: UpdateRestartIo = { now: () => now, acquire: () => { calls.push("acquire"); return { release: () => { calls.push("release"); } }; }, diff --git a/tests/cli/system-restart-client.test.ts b/tests/cli/system-restart-client.test.ts index 54d50976e8e..1116d65467a 100644 --- a/tests/cli/system-restart-client.test.ts +++ b/tests/cli/system-restart-client.test.ts @@ -55,7 +55,7 @@ function successfulDeps() { // Matches the /healthz fixture version below so the skew guard stays out of the way; // the dedicated skew tests override it explicitly. cliVersion: "test", - readUpdateHome: () => ({ config: { path: "/test/ocx", dev: 1, ino: 2 }, codex: { path: "/test/codex", dev: 1, ino: 3 }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }), + readUpdateHome: () => ({ config: { path: "/test/ocx", dev: 1, ino: 2, fileId: "1:2" }, codex: { path: "/test/codex", dev: 1, ino: 3, fileId: "1:3" }, revision: 0, serviceRecord: { schema: 1 as const, digest: "a".repeat(64) } }), }, }; }