diff --git a/scripts/diagnostics/windows-version-control/README.md b/scripts/diagnostics/windows-version-control/README.md index 6f6402553a1..d931da4098f 100644 --- a/scripts/diagnostics/windows-version-control/README.md +++ b/scripts/diagnostics/windows-version-control/README.md @@ -1,7 +1,7 @@ # Windows synchronous runtime-selection control This optional Rust control exercises `GET /v1/models` in a separate Bun process -using the repository's unchanged `tests/helpers/native-main-owner-child.ts`. +using the repository's `tests/helpers/native-main-owner-child.ts`. It does not connect to a running proxy, install accounts, or replay turns. It is a reproducer, not a runtime fix or proof of a historical production cause. @@ -10,6 +10,14 @@ authentication values, and an ephemeral loopback listener. It is configured with no real credentials and one unavailable loopback provider. Other network egress is not blocked or audited. +Every native-owner fixture server injects `nativeOwnerRestartService` from +`tests/helpers/native-owner-restart-service.ts`. Codex desktop/app-server state +queries return a synthetic unknown result and restart requests fail without +catalog synchronization, host process enumeration or process signalling. This +boundary applies even when a client presents the fixture authentication values; +redirected homes and descendant Jobs alone are not process-control isolation. +The diagnostic's model-list and native-profile operations are unchanged. + ```powershell cargo test --locked --manifest-path scripts/diagnostics/windows-version-control/Cargo.toml --test async_contracts -- --test-threads=4 cargo clippy --locked --manifest-path scripts/diagnostics/windows-version-control/Cargo.toml --all-targets -- -D warnings @@ -54,7 +62,7 @@ call chain. Keep profiles and fixture artifacts private; publish bounded scalar results and repository-relative function names instead. To compare installed source with the repository fixture, use a separately owned -scratch mirror containing a byte-identical copy of the helper and package +scratch mirror containing byte-identical copies of both helper files and package metadata, with `src` and `node_modules` links to the desired installation. The fixture must perform no writes to that installation. Links alone do not enforce read-only access. Verify the profile's source module paths and source diff --git a/tests/codex-integration/codex-restart-route.test.ts b/tests/codex-integration/codex-restart-route.test.ts index 949597c3c50..643ca19cff3 100644 --- a/tests/codex-integration/codex-restart-route.test.ts +++ b/tests/codex-integration/codex-restart-route.test.ts @@ -8,6 +8,12 @@ * Plan: devlog/_fin/260815_gui_codex_restart/010_phase1_backend_endpoint.md */ import { describe, expect, test } from "bun:test"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { startServer } from "../../src/server"; +import { helperPath } from "../helpers/repo-root"; +import { nativeOwnerRestartService, NATIVE_OWNER_RESTART_REFUSAL } from "../helpers/native-owner-restart-service"; import { handleSystemRoutes } from "../../src/server/management/system-routes"; import type { ManagementContext } from "../../src/server/management/context"; import { @@ -162,3 +168,107 @@ describe("POST /api/system/codex-restart", () => { }); }); + +describe("native-owner fixture process-control boundary", () => { + test("the fixture installs its non-operating service on the common server startup path", () => { + // This source-wiring guard complements the real route dispatch checks below. Do not import + // the helper here: its top level starts a server and reads the stdin control protocol. + const source = readFileSync(helperPath("native-main-owner-child.ts"), "utf8"); + const from = source.indexOf("function start("); + const to = source.indexOf("const primary = start();"); + expect(from).toBeGreaterThanOrEqual(0); + expect(to).toBeGreaterThan(from); + expect(source.slice(from, to)).toContain("codexRestartService: nativeOwnerRestartService"); + }); + + test("state queries use a synthetic unknown result without host process enumeration", async () => { + const response = await handleSystemRoutes(contextFor(CODEX_APP_SERVER_STATE_PATH, "GET", nativeOwnerRestartService)); + expect(response?.status).toBe(200); + expect(await response!.json()).toEqual({ state: "unknown", runningCount: 0 }); + }); + + test("restart is refused instead of invoking the live service or claiming a successful restart", async () => { + await expect(handleSystemRoutes(contextFor(CODEX_RESTART_PATH, "POST", nativeOwnerRestartService))) + .rejects.toThrow(NATIVE_OWNER_RESTART_REFUSAL); + // A refused call does not poison a later read or make the next restart succeed. + expect(await nativeOwnerRestartService.readState()).toEqual({ state: "unknown", runningCount: 0 }); + await expect(nativeOwnerRestartService.performRestart()).rejects.toThrow(NATIVE_OWNER_RESTART_REFUSAL); + }); + + test("servers on the common startup path serve the injected service over authenticated HTTP", async () => { + // The child's start() is a thin wrapper over this same startServer call: driving real + // listeners proves the managementApi -> ctx.deps forwarding cannot silently drop the + // boundary while the source guard above still passes. Primary and extra fixture + // servers share one injection path, so both are exercised here. The sentinel restart + // body is one the live implementation could never emit: it does not fabricate a pid. + const root = mkdtempSync(join(tmpdir(), "ocx-restart-boundary-")); + const previousHome = process.env.OPENCODEX_HOME; + const previousCodexHome = process.env.CODEX_HOME; + const previousAdminToken = process.env.OPENCODEX_ADMIN_AUTH_TOKEN; + process.env.OPENCODEX_HOME = root; + process.env.CODEX_HOME = root; + process.env.OPENCODEX_ADMIN_AUTH_TOKEN = "restart-boundary-admin"; + const sentinel: CodexRestartResponse = { + success: true, + stateBefore: "unknown", + synced: false, + requested: [424242], + stopped: [424242], + surviving: [], + failed: [], + code: "stopped", + }; + const calls: string[] = []; + const observed = { + readState: async () => { + calls.push("readState"); + return nativeOwnerRestartService.readState(); + }, + performRestart: async () => { + calls.push("performRestart"); + return sentinel; + }, + }; + // startServer probes service ownership on every boot; without this seam it inspects the + // developer machine's installed service state — the host coupling this fixture exists to + // remove (the same seam native-main-owner-child.ts injects). "foreign" is deliberate: it + // fences native-main admission with no lifecycle machinery, which this restart-route test + // does not exercise; "owned" would start a real owner drive on the fixture home. + const deps = { + inspectNativeCodexOwnership: () => ({ + ownership: "foreign", + reason: "restart route test fixture", + }), + managementApi: { codexRestartService: observed }, + }; + const primary = startServer(0, deps); + const extra = startServer(0, deps); + try { + const headers = { "x-opencodex-api-key": "restart-boundary-admin" }; + for (const server of [primary, extra]) { + const state = await fetch(new URL(CODEX_APP_SERVER_STATE_PATH, server.url), { headers }); + expect(state.status).toBe(200); + expect(await state.json()).toEqual({ state: "unknown", runningCount: 0 }); + const restart = await fetch(new URL(CODEX_RESTART_PATH, server.url), { method: "POST", headers }); + expect(restart.status).toBe(200); + expect(await restart.json()).toEqual(sentinel); + } + expect(calls).toEqual([ + "readState", "performRestart", + "readState", "performRestart", + ]); + } finally { + await primary.stop(true); + await extra.stop(true); + for (const [name, value] of [ + ["OPENCODEX_HOME", previousHome], + ["CODEX_HOME", previousCodexHome], + ["OPENCODEX_ADMIN_AUTH_TOKEN", previousAdminToken], + ] as const) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 }); + } + }); +}); diff --git a/tests/helpers/native-main-owner-child.ts b/tests/helpers/native-main-owner-child.ts index b70db3dde04..250a4529d75 100644 --- a/tests/helpers/native-main-owner-child.ts +++ b/tests/helpers/native-main-owner-child.ts @@ -12,6 +12,7 @@ import { import type { NativeProfileKey, NativeProfileKeyProvider } from "../../src/codex/native-profile-types"; import { drainAndShutdown } from "../../src/server"; import { startServer } from "../../src/server"; +import { nativeOwnerRestartService } from "./native-owner-restart-service"; const required = (name: string): string => { const value = process.env[name]; @@ -137,7 +138,9 @@ function start(home = codexHome): ReturnType { } : undefined, }, - managementApi: { nativeProfileApi: { manager } }, + // Synthetic homes do not isolate OS process-control APIs. Every server, including extra + // fixture instances, must inject the entire restart service rather than using live defaults. + managementApi: { nativeProfileApi: { manager }, codexRestartService: nativeOwnerRestartService }, }); servers.push(server); return server; diff --git a/tests/helpers/native-owner-restart-service.ts b/tests/helpers/native-owner-restart-service.ts new file mode 100644 index 00000000000..518237a0fc9 --- /dev/null +++ b/tests/helpers/native-owner-restart-service.ts @@ -0,0 +1,17 @@ +/** The native-owner fixture must not enumerate or restart the operator's desktop. */ +import type { + CodexAppServerStateResponse, + CodexRestartResponse, +} from "../../src/lib/codex-restart-contract"; + +export const NATIVE_OWNER_RESTART_REFUSAL = "Codex desktop restart is disabled in the native-owner fixture"; + +/** Complete service injection: neither state reads nor restart requests reach production I/O. */ +export const nativeOwnerRestartService = Object.freeze({ + async readState(): Promise { + return { state: "unknown", runningCount: 0 }; + }, + async performRestart(): Promise { + throw new Error(NATIVE_OWNER_RESTART_REFUSAL); + }, +});