diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index f8bd26a990..679a0bb95c 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1656,6 +1656,7 @@ "server-request-body-size.test.ts": "server", "server-search.test.ts": "server", "server-startup-reconcile-resilience.test.ts": "server", + "server-startup-codex-off.test.ts": "server", "server-stop-config-hardening.test.ts": "server", "server-xai-chat-reasoning-streaming.test.ts": "server", "server-xai-header-parity.test.ts": "server", diff --git a/src/server/index.ts b/src/server/index.ts index d7e60c1c95..0850f3c176 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -267,17 +267,17 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe startupOwnershipHomes = homes; startupOwnershipStatePaths = statePaths; } catch { /* inspection below stays unknown */ } - const startupCacheOwnership = inspectStartupOwnership( + const startupCacheOwnership = shouldSyncCodexOnStart(config) ? inspectStartupOwnership( deps, startupOwnershipHomes, startupOwnershipStatePaths, startupWindowsTaskListingCache, - ); + ) : null; // Startup cache invalidation is best-effort and applies only when startup sync is enabled. // Check OFF before taking K: on Windows K resolves SID + LocalAppData through PowerShell, // and run 35093667426 exceeded healthy controls by 33.8 s against that 30 s child budget. // The permit callback still re-reads intent under K to close a concurrent disable race. - if (shouldSyncCodexOnStart(config) && startupCacheOwnership.ownership === "owned" && startupOwnershipHomes !== null) { + if (startupCacheOwnership?.ownership === "owned" && startupOwnershipHomes !== null) { try { const startupCodexHome = startupOwnershipHomes.codexHome; // #1046: record whether this actually rewrote the cache. `handleStart` ORs this @@ -549,12 +549,12 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe // clients; no Codex request can use this lifecycle in that state. // Re-probe here instead of trusting the earlier cache decision: startup work // between the two sites must not widen the service-install race. - const nativeOwnership = inspectStartupOwnership( + const nativeOwnership = shouldSyncCodexOnStart(config) ? inspectStartupOwnership( deps, startupOwnershipHomes, startupOwnershipStatePaths, - ); - const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign" + ) : null; + const preparedNativeMainLifecycle = nativeOwnership !== null && nativeOwnership.ownership !== "foreign" && startupOwnershipHomes !== null ? prepareNativeMainStartupLifecycle( deps.nativeMainStartup, @@ -600,7 +600,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe return retryPreparedNativeMainLifecycle.start(); }, }; - const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config) + const nativeMainLifecycle: NativeMainStartupLifecycle = nativeOwnership !== null ? nativeOwnership.ownership === "owned" ? preparedNativeMainLifecycle ? preparedNativeMainLifecycle.start() diff --git a/structure/runtime.md b/structure/runtime.md index 57f8ea1c82..669a2fb256 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -202,8 +202,8 @@ independent. Ordinary stop drains the spend reporters after listener teardown be reference. `src/server/index/spend-ledger-lifecycle.ts` also holds the lease through failed-start listener rollback. Enforced seeds remain live until reporters close and settlement plus send-ID forgetting are durable; scope totals survive forgetting. SQLite and the OS release a crashed owner; no PID, timestamp, TTL or lock-file deletion participates in recovery. Startup passes the provider roster and top-level pool aliases with the spend policy; live state reconciliation refreshes both without opening an unused ledger. Under the [request-start policy](transports/responses-spend.md#durable-spend-reservations), existing requests retain their initial enforcement mode, numeric ceilings and L; changed limits apply to later requests. HTTP admission checks [historical continuity](transports/responses-spend.md#historical-pool-continuity-and-rollback) even without a root; failed accounting reads release the active-turn lease. Rooted continuity denials record exactly one workflow refusal event before response formatting. -An explicit Codex integration OFF skips startup cache invalidation before the user-scoped catalog -serialization lock is resolved. Explicit `sync` and `sync-cache` retain their catalog-only override. +An explicit Codex integration OFF skips startup ownership probes, native-main preparation and cache invalidation before resolving the catalog lock. +Enabled startup retains separate fresh ownership decisions. Explicit `sync` and `sync-cache` retain their catalog-only override. `startServer` composes up to four sockets in one synchronous startup transaction: the public data listener, optional unauthenticated data-loopback and hub-management listeners, and the optional `hub-link` listener, which opens only for a recorded link and persists its concrete `127.0.0.1:`. HTTP Responses/Messages apply the [caller header contract](data-planes/inbound-compat.md#http-caller-conversation-identity) before turn admission and dispatch. Linked-machine data uses the [connection-bound relay contract](remote-link.md#connection-bound-relay-authentication); client-local credentials and routing policy remain unchanged. The data-loopback socket serves a fixed data-plane allowlist: Responses and its compact sibling, diff --git a/tests/codex-integration/bearer-admission-routed-provider.test.ts b/tests/codex-integration/bearer-admission-routed-provider.test.ts index 2ab77af91b..21d036a054 100644 --- a/tests/codex-integration/bearer-admission-routed-provider.test.ts +++ b/tests/codex-integration/bearer-admission-routed-provider.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test"; import { mkdtempSync, writeFileSync } from "node:fs"; import http2 from "node:http2"; import { tmpdir } from "node:os"; @@ -17,10 +17,13 @@ import { handleNativeProfileAPI } from "../../src/codex/native-profile-api"; import type { NativeProfileManager } from "../../src/codex/native-profile-manager"; import type { OcxConfig } from "../../src/types"; import { ownedServiceHomeInspection } from "../helpers/owned-service-home-inspection"; -import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { removeTreeAfterBackgroundWork } from "../helpers/remove-tree"; import { fakeChatGptJwt } from "../helpers/fake-chatgpt-jwt"; import { resetVisionDescriptionCache } from "../../src/vision"; import { SERVER_BUDGET_MS } from "../helpers/test-budget"; + +// Every case starts a real server; on Windows its config-dir ACL work alone can exceed 5 s. +setDefaultTimeout(SERVER_BUDGET_MS); import { saveCodexAccountCredential } from "../../src/codex/account-store"; /** @@ -195,7 +198,7 @@ beforeEach(() => { }) as typeof fetch; }); -afterEach(() => { +afterEach(async () => { resetVisionDescriptionCache(); closeRequestHistoryIndex(); clearComboTargetCooldowns(); @@ -209,8 +212,9 @@ afterEach(() => { else process.env.CODEX_HOME = previousCodexHome; if (previousDataToken === undefined) delete process.env.OPENCODEX_API_AUTH_TOKEN; else process.env.OPENCODEX_API_AUTH_TOKEN = previousDataToken; - if (ocxHome) removeTreeWithRetry(ocxHome); - if (codexHome) removeTreeWithRetry(codexHome); + // Startup's config-dir ACL hardening may still hold the homes on Windows; settle it first. + if (ocxHome) await removeTreeAfterBackgroundWork(ocxHome); + if (codexHome) await removeTreeAfterBackgroundWork(codexHome); ocxHome = ""; codexHome = ""; }); diff --git a/tests/codex-integration/codex-service-manager-probe-hardening.test.ts b/tests/codex-integration/codex-service-manager-probe-hardening.test.ts index d7b8736ef7..9b1ea42da0 100644 --- a/tests/codex-integration/codex-service-manager-probe-hardening.test.ts +++ b/tests/codex-integration/codex-service-manager-probe-hardening.test.ts @@ -230,7 +230,7 @@ describe("Windows ownership probe hardening regressions", () => { ...getDefaultConfig(), port: 0, hostname: "127.0.0.1", - clientIntegrations: { codex: false }, + clientIntegrations: { codex: true }, subagentModels: [], }, null, 2)); @@ -278,7 +278,10 @@ describe("Windows ownership probe hardening regressions", () => { winswStatus: () => "nonexistent", }); ownerships.push(answer.ownership); - return answer; + // Exercise enabled startup's two real inspections without taking the unrelated + // user-scoped catalog lock (which resolves Windows identity through PowerShell). + // Preserve each raw verdict above so a stale second listing still fails this test. + return ownerships.length === 1 ? { ownership: "unknown", reason: "cache writes outside this fixture" } : answer; } finally { setTrustedWindowsSystemDirectoryResolverForTests(null); } diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index fcca881b05..e8e81b968a 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1126,6 +1126,7 @@ "server-opencode-go-goal-streaming.test.ts": "server", "server-rate-limit-retry-e2e.test.ts": "server", "server-request-body-size.test.ts": "server", "server-search.test.ts": "server", "server-startup-reconcile-resilience.test.ts": "server", "server-stop-config-hardening.test.ts": "server", + "server-startup-codex-off.test.ts": "server", "server-xai-chat-reasoning-streaming.test.ts": "server", "server-xai-header-parity.test.ts": "server", "server-xai-oauth-401-replay.test.ts": "server", "server-xai-responses-streaming.test.ts": "server", "service-auth-qualified-localhost.test.ts": "service", "service-child-ownership.test.ts": "service", diff --git a/tests/server/server-startup-codex-off.test.ts b/tests/server/server-startup-codex-off.test.ts new file mode 100644 index 0000000000..699ce9421b --- /dev/null +++ b/tests/server/server-startup-codex-off.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { saveConfig } from "../../src/config"; +import { startServer } from "../../src/server"; +import { installIsolatedCodexHome } from "../helpers/isolated-codex-home"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +test("Codex OFF starts without service-manager probes or native-main preparation", async () => { + const priorHome = process.env.OPENCODEX_HOME; + const home = mkdtempSync(join(tmpdir(), "ocx-start-codex-off-")); + const codex = installIsolatedCodexHome("ocx-start-codex-off-client-"); + process.env.OPENCODEX_HOME = home; + let inspections = 0; + let preparations = 0; + let server: ReturnType | undefined; + try { + saveConfig({ + providers: { fixture: { adapter: "openai-chat", baseUrl: "http://127.0.0.1:1/v1", allowPrivateNetwork: true } }, + defaultProvider: "fixture", port: 0, hostname: "127.0.0.1", + clientIntegrations: { codex: false }, checkForUpdates: false, + syncResumeHistory: false, claudeCode: { intercept: { enabled: false } }, + }); + server = startServer(0, { + inspectNativeCodexOwnership: () => { + inspections++; + return { ownership: "owned", reason: "unused Codex OFF probe" }; + }, + nativeMainStartup: { + get manager() { + preparations++; + throw new Error("Codex OFF must not prepare a native manager"); + }, + }, + }); + expect(inspections).toBe(0); + expect(preparations).toBe(0); + expect((await fetch(`http://127.0.0.1:${server.port}/healthz`)).status).toBe(200); + } finally { + await server?.stop(true); + codex.restore(); + if (priorHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = priorHome; + removeTreeWithRetry(home); + } +});